From 1c2690a36e376d9b5dbae91e43a538013dd4764d Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Mon, 2 Oct 2023 16:28:03 +0200 Subject: [PATCH 001/391] 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 002/391] 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 de66326d622a902ec6c3519b70941bf082139bf8 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Mon, 27 Nov 2023 16:44:30 +0800 Subject: [PATCH 003/391] only archive chat room, expense chat, and task --- src/pages/workspace/WorkspaceInitialPage.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/pages/workspace/WorkspaceInitialPage.js b/src/pages/workspace/WorkspaceInitialPage.js index 77e831e62b63..fa29cf3ded22 100644 --- a/src/pages/workspace/WorkspaceInitialPage.js +++ b/src/pages/workspace/WorkspaceInitialPage.js @@ -67,6 +67,14 @@ function dismissError(policyID) { Policy.removeWorkspace(policyID); } +/** + * Whether the policy report should be deleted when we delete the policy. + * @param {Object} report + */ +function shouldDeleteReport(report) { + return ReportUtils.isChatRoom(report) || ReportUtils.isPolicyExpenseChat(report) || ReportUtils.isTaskReport(report); +} + function WorkspaceInitialPage(props) { const styles = useThemeStyles(); const policy = props.policyDraft && props.policyDraft.id ? props.policyDraft : props.policy; @@ -111,7 +119,7 @@ function WorkspaceInitialPage(props) { * Call the delete policy and hide the modal */ const confirmDeleteAndHideModal = useCallback(() => { - Policy.deleteWorkspace(policyID, policyReports, policy.name); + Policy.deleteWorkspace(policyID, _.filter(policyReports, shouldDeleteReport), policy.name); setIsDeleteModalOpen(false); // Pop the deleted workspace page before opening workspace settings. Navigation.goBack(ROUTES.SETTINGS_WORKSPACES); From 79aaa4b766e5ad8402f11dbadc22acf3c5b0b6aa Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Mon, 27 Nov 2023 16:53:42 +0800 Subject: [PATCH 004/391] update func name and comment --- src/pages/workspace/WorkspaceInitialPage.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pages/workspace/WorkspaceInitialPage.js b/src/pages/workspace/WorkspaceInitialPage.js index fa29cf3ded22..76e8d30093cc 100644 --- a/src/pages/workspace/WorkspaceInitialPage.js +++ b/src/pages/workspace/WorkspaceInitialPage.js @@ -68,10 +68,10 @@ function dismissError(policyID) { } /** - * Whether the policy report should be deleted when we delete the policy. + * Whether the policy report should be archived when we delete the policy. * @param {Object} report */ -function shouldDeleteReport(report) { +function shouldArchiveReport(report) { return ReportUtils.isChatRoom(report) || ReportUtils.isPolicyExpenseChat(report) || ReportUtils.isTaskReport(report); } @@ -119,7 +119,7 @@ function WorkspaceInitialPage(props) { * Call the delete policy and hide the modal */ const confirmDeleteAndHideModal = useCallback(() => { - Policy.deleteWorkspace(policyID, _.filter(policyReports, shouldDeleteReport), policy.name); + Policy.deleteWorkspace(policyID, _.filter(policyReports, shouldArchiveReport), policy.name); setIsDeleteModalOpen(false); // Pop the deleted workspace page before opening workspace settings. Navigation.goBack(ROUTES.SETTINGS_WORKSPACES); From 86fa1820c0b6b16d55a9f259b573682a0a7e776d Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Mon, 27 Nov 2023 16:56:11 +0800 Subject: [PATCH 005/391] add jsdoc returns --- src/pages/workspace/WorkspaceInitialPage.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pages/workspace/WorkspaceInitialPage.js b/src/pages/workspace/WorkspaceInitialPage.js index 76e8d30093cc..c899fffff4e2 100644 --- a/src/pages/workspace/WorkspaceInitialPage.js +++ b/src/pages/workspace/WorkspaceInitialPage.js @@ -70,6 +70,7 @@ function dismissError(policyID) { /** * Whether the policy report should be archived when we delete the policy. * @param {Object} report + * @returns {Boolean} */ function shouldArchiveReport(report) { return ReportUtils.isChatRoom(report) || ReportUtils.isPolicyExpenseChat(report) || ReportUtils.isTaskReport(report); From 2346a024ded1c593ee6df83276f88c11d1a3243a Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Mon, 27 Nov 2023 17:00:07 +0800 Subject: [PATCH 006/391] prettier --- src/pages/workspace/WorkspaceInitialPage.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/workspace/WorkspaceInitialPage.js b/src/pages/workspace/WorkspaceInitialPage.js index c899fffff4e2..66d9f2f7f518 100644 --- a/src/pages/workspace/WorkspaceInitialPage.js +++ b/src/pages/workspace/WorkspaceInitialPage.js @@ -69,7 +69,7 @@ function dismissError(policyID) { /** * Whether the policy report should be archived when we delete the policy. - * @param {Object} report + * @param {Object} report * @returns {Boolean} */ function shouldArchiveReport(report) { From 2a09811e8833f478b8f4aed976b27156dff6ff5a Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Tue, 28 Nov 2023 13:33:47 +0100 Subject: [PATCH 007/391] update import --- src/components/Composer/index.android.js | 4 ++-- src/components/Composer/index.ios.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/Composer/index.android.js b/src/components/Composer/index.android.js index 7b72e17ae5fe..a1d6d514149b 100644 --- a/src/components/Composer/index.android.js +++ b/src/components/Composer/index.android.js @@ -3,7 +3,7 @@ import React, {useCallback, useEffect, useMemo, useRef} from 'react'; import _ from 'underscore'; import RNTextInput from '@components/RNTextInput'; import * as ComposerUtils from '@libs/ComposerUtils'; -import {getComposerMaxHeightStyle} from '@styles/StyleUtils'; +import * as StyleUtils from '@styles/StyleUtils'; import themeColors from '@styles/themes/default'; const propTypes = { @@ -92,7 +92,7 @@ function Composer({shouldClear, onClear, isDisabled, maxLines, forwardedRef, isC onClear(); }, [shouldClear, onClear]); - const maxHeightStyle = useMemo(() => getComposerMaxHeightStyle(maxLines, isComposerFullSize), [isComposerFullSize, maxLines]); + const maxHeightStyle = useMemo(() => StyleUtils.getComposerMaxHeightStyle(maxLines, isComposerFullSize), [isComposerFullSize, maxLines]); return ( getComposerMaxHeightStyle(maxLines, isComposerFullSize), [isComposerFullSize, maxLines]); + const maxHeightStyle = useMemo(() => StyleUtils.getComposerMaxHeightStyle(maxLines, isComposerFullSize), [isComposerFullSize, maxLines]); // On native layers we like to have the Text Input not focused so the // user can read new chats without the keyboard in the way of the view. From db57bf3ecc192ac805ed37e2d3367327c7332a96 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Tue, 28 Nov 2023 13:42:17 +0100 Subject: [PATCH 008/391] simplify android and ios implementations --- src/components/Composer/index.ios.js | 130 ------------------ .../{index.android.js => index.native.js} | 7 +- 2 files changed, 4 insertions(+), 133 deletions(-) delete mode 100644 src/components/Composer/index.ios.js rename src/components/Composer/{index.android.js => index.native.js} (99%) diff --git a/src/components/Composer/index.ios.js b/src/components/Composer/index.ios.js deleted file mode 100644 index 18f625c25a0d..000000000000 --- a/src/components/Composer/index.ios.js +++ /dev/null @@ -1,130 +0,0 @@ -import PropTypes from 'prop-types'; -import React, {useCallback, useEffect, useMemo, useRef} from 'react'; -import _ from 'underscore'; -import RNTextInput from '@components/RNTextInput'; -import * as ComposerUtils from '@libs/ComposerUtils'; -import * as StyleUtils from '@styles/StyleUtils'; -import themeColors from '@styles/themes/default'; - -const propTypes = { - /** If the input should clear, it actually gets intercepted instead of .clear() */ - shouldClear: PropTypes.bool, - - /** A ref to forward to the text input */ - forwardedRef: PropTypes.func, - - /** When the input has cleared whoever owns this input should know about it */ - onClear: PropTypes.func, - - /** Set focus to this component the first time it renders. - * Override this in case you need to set focus on one field out of many, or when you want to disable autoFocus */ - autoFocus: PropTypes.bool, - - /** Prevent edits and interactions like focus for this input. */ - isDisabled: PropTypes.bool, - - /** Selection Object */ - selection: PropTypes.shape({ - start: PropTypes.number, - end: PropTypes.number, - }), - - /** Whether the full composer can be opened */ - isFullComposerAvailable: PropTypes.bool, - - /** Maximum number of lines in the text input */ - maxLines: PropTypes.number, - - /** Allow the full composer to be opened */ - setIsFullComposerAvailable: PropTypes.func, - - /** Whether the composer is full size */ - isComposerFullSize: PropTypes.bool, - - /** General styles to apply to the text input */ - // eslint-disable-next-line react/forbid-prop-types - style: PropTypes.any, -}; - -const defaultProps = { - shouldClear: false, - onClear: () => {}, - autoFocus: false, - isDisabled: false, - forwardedRef: null, - selection: { - start: 0, - end: 0, - }, - maxLines: undefined, - isFullComposerAvailable: false, - setIsFullComposerAvailable: () => {}, - isComposerFullSize: false, - style: null, -}; - -function Composer({shouldClear, onClear, isDisabled, maxLines, forwardedRef, isComposerFullSize, setIsFullComposerAvailable, ...props}) { - const textInput = useRef(null); - - /** - * Set the TextInput Ref - * @param {Element} el - */ - const setTextInputRef = useCallback((el) => { - textInput.current = el; - if (!_.isFunction(forwardedRef) || textInput.current === null) { - return; - } - - // This callback prop is used by the parent component using the constructor to - // get a ref to the inner textInput element e.g. if we do - // this.textInput = el} /> this will not - // return a ref to the component, but rather the HTML element by default - forwardedRef(textInput.current); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - useEffect(() => { - if (!shouldClear) { - return; - } - textInput.current.clear(); - onClear(); - }, [shouldClear, onClear]); - - const maxHeightStyle = useMemo(() => StyleUtils.getComposerMaxHeightStyle(maxLines, isComposerFullSize), [isComposerFullSize, maxLines]); - - // On native layers we like to have the Text Input not focused so the - // user can read new chats without the keyboard in the way of the view. - // On Android the selection prop is required on the TextInput but this prop has issues on IOS - const propsToPass = _.omit(props, 'selection'); - return ( - ComposerUtils.updateNumberOfLines({maxLines, isComposerFullSize, isDisabled, setIsFullComposerAvailable}, e)} - rejectResponderTermination={false} - smartInsertDelete={false} - style={[...props.style, maxHeightStyle]} - readOnly={isDisabled} - /> - ); -} - -Composer.propTypes = propTypes; -Composer.defaultProps = defaultProps; - -const ComposerWithRef = React.forwardRef((props, ref) => ( - -)); - -ComposerWithRef.displayName = 'ComposerWithRef'; - -export default ComposerWithRef; diff --git a/src/components/Composer/index.android.js b/src/components/Composer/index.native.js similarity index 99% rename from src/components/Composer/index.android.js rename to src/components/Composer/index.native.js index a1d6d514149b..5bec0f701ec5 100644 --- a/src/components/Composer/index.android.js +++ b/src/components/Composer/index.native.js @@ -7,9 +7,6 @@ import * as StyleUtils from '@styles/StyleUtils'; import themeColors from '@styles/themes/default'; const propTypes = { - /** Maximum number of lines in the text input */ - maxLines: PropTypes.number, - /** If the input should clear, it actually gets intercepted instead of .clear() */ shouldClear: PropTypes.bool, @@ -32,6 +29,9 @@ const propTypes = { end: PropTypes.number, }), + /** Maximum number of lines in the text input */ + maxLines: PropTypes.number, + /** Whether the full composer can be opened */ isFullComposerAvailable: PropTypes.bool, @@ -103,6 +103,7 @@ function Composer({shouldClear, onClear, isDisabled, maxLines, forwardedRef, isC ref={setTextInputRef} onContentSizeChange={(e) => ComposerUtils.updateNumberOfLines({maxLines, isComposerFullSize, isDisabled, setIsFullComposerAvailable}, e)} rejectResponderTermination={false} + smartInsertDelete={false} textAlignVertical="center" style={[...props.style, maxHeightStyle]} readOnly={isDisabled} From 170729c859d9daa171094c6c41b7b8014af45d5d Mon Sep 17 00:00:00 2001 From: MitchExpensify <36425901+MitchExpensify@users.noreply.github.com> Date: Tue, 21 Nov 2023 15:30:41 -0800 Subject: [PATCH 009/391] Update Introducing-Expensify-Chat.md Fixing so the help steps are numbered --- .../chat/Introducing-Expensify-Chat.md | 69 ++++++++++--------- 1 file changed, 36 insertions(+), 33 deletions(-) diff --git a/docs/articles/new-expensify/chat/Introducing-Expensify-Chat.md b/docs/articles/new-expensify/chat/Introducing-Expensify-Chat.md index 669d960275e6..25ccdefad261 100644 --- a/docs/articles/new-expensify/chat/Introducing-Expensify-Chat.md +++ b/docs/articles/new-expensify/chat/Introducing-Expensify-Chat.md @@ -24,30 +24,30 @@ After downloading the app, log into your new.expensify.com account (you’ll use ## How to send messages -Click **+** then **Send message** in New Expensify -Choose **Chat** -Search for any name, email or phone number -Select the individual to begin chatting +1. Click **+** then **Send message** in New Expensify +2. Choose **Chat** +3. Search for any name, email or phone number +4. Select the individual to begin chatting ## How to create a group -Click **+**, then **Send message** in New Expensify -Search for any name, email or phone number -Click **Add to group** -Group participants are listed with a green check -Repeat steps 1-3 to add more participants to the group -Click **Create chat** to start chatting +1. Click **+**, then **Send message** in New Expensify +2. Search for any name, email or phone number +3. Click **Add to group** +4. Group participants are listed with a green check +5. Repeat steps 1-3 to add more participants to the group +6. Click **Create chat** to start chatting ## How to create a room -Click **+**, then **Send message** in New Expensify -Click **Room** -Enter a room name that doesn’t already exist on the intended Workspace -Choose the Workspace you want to associate the room with. -Choose the room’s visibility setting: -Private: Only people explicitly invited can find the room* -Restricted: Workspace members can find the room* -Public: Anyone can find the room +1. Click **+**, then **Send message** in New Expensify +2. Click **Room** +3. Enter a room name that doesn’t already exist on the intended Workspace +4. Choose the Workspace you want to associate the room with. +5. Choose the room’s visibility setting: +6. Private: Only people explicitly invited can find the room* +7. Restricted: Workspace members can find the room* +8. Public: Anyone can find the room *Anyone, including non-Workspace Members, can be invited to a Private or Restricted room. @@ -56,26 +56,29 @@ Public: Anyone can find the room You can invite people to a Group or Room by @mentioning them or from the Members pane. ## Mentions: -Type **@** and start typing the person’s name or email address -Choose one or more contacts -Input message, if desired, then send + +1. Type **@** and start typing the person’s name or email address +2. Choose one or more contacts +3. Input message, if desired, then send ## Members pane invites: -Click the **Room** or **Group** header -Select **Members** -Click **Invite** -Find and select any contact/s you’d like to invite -Click **Next** -Write a custom invitation if you like -Click **Invite** + +1. Click the **Room** or **Group** header +2. Select **Members** +3. Click **Invite** +4. Find and select any contact/s you’d like to invite +5. Click **Next** +6. Write a custom invitation if you like +7. Click **Invite** ## Members pane removals: -Click the **Room** or **Group** header -Select **Members** -Find and select any contact/s you’d like to remove -Click **Remove** -Click **Remove members** + +1. Click the **Room** or **Group** header +2. Select **Members** +3. Find and select any contact/s you’d like to remove +4. Click **Remove** +5. Click **Remove members** ## How to format text From bbafbeabff805857f0f60ac4b24c010c25921aae Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Fri, 3 Nov 2023 09:23:20 +0100 Subject: [PATCH 010/391] start migrating PlaidLink to TypeScript --- .../{index.native.js => index.native.tsx} | 17 ++++++++-------- .../PlaidLink/{index.js => index.tsx} | 20 ++++++++----------- 2 files changed, 17 insertions(+), 20 deletions(-) rename src/components/PlaidLink/{index.native.js => index.native.tsx} (66%) rename src/components/PlaidLink/{index.js => index.tsx} (70%) diff --git a/src/components/PlaidLink/index.native.js b/src/components/PlaidLink/index.native.tsx similarity index 66% rename from src/components/PlaidLink/index.native.js rename to src/components/PlaidLink/index.native.tsx index 7d995d03926b..e1e9e7756620 100644 --- a/src/components/PlaidLink/index.native.js +++ b/src/components/PlaidLink/index.native.tsx @@ -1,27 +1,28 @@ import {useEffect} from 'react'; -import {dismissLink, openLink, useDeepLinkRedirector, usePlaidEmitter} from 'react-native-plaid-link-sdk'; +import {dismissLink, LinkEvent, openLink, useDeepLinkRedirector, usePlaidEmitter} from 'react-native-plaid-link-sdk'; import Log from '@libs/Log'; import CONST from '@src/CONST'; import {plaidLinkDefaultProps, plaidLinkPropTypes} from './plaidLinkPropTypes'; +import PlaidLinkProps from './types'; -function PlaidLink(props) { +function PlaidLink({token, onSuccess = () => {}, onExit = () => {}, onEvent}: PlaidLinkProps) { useDeepLinkRedirector(); - usePlaidEmitter((event) => { + usePlaidEmitter((event: LinkEvent) => { Log.info('[PlaidLink] Handled Plaid Event: ', false, event); - props.onEvent(event.eventName, event.metadata); + onEvent?.(event.eventName, event.metadata); }); useEffect(() => { - props.onEvent(CONST.BANK_ACCOUNT.PLAID.EVENTS_NAME.OPEN, {}); + onEvent?.(CONST.BANK_ACCOUNT.PLAID.EVENTS_NAME.OPEN, {}); openLink({ tokenConfig: { - token: props.token, + token, }, onSuccess: ({publicToken, metadata}) => { - props.onSuccess({publicToken, metadata}); + onSuccess({publicToken, metadata}); }, onExit: (exitError, metadata) => { Log.info('[PlaidLink] Exit: ', false, {exitError, metadata}); - props.onExit(); + onExit(); }, }); diff --git a/src/components/PlaidLink/index.js b/src/components/PlaidLink/index.tsx similarity index 70% rename from src/components/PlaidLink/index.js rename to src/components/PlaidLink/index.tsx index 790206f34ce7..39b9ffda54b2 100644 --- a/src/components/PlaidLink/index.js +++ b/src/components/PlaidLink/index.tsx @@ -1,35 +1,33 @@ import {useCallback, useEffect, useState} from 'react'; -import {usePlaidLink} from 'react-plaid-link'; +import {PlaidLinkOnSuccessMetadata, usePlaidLink} from 'react-plaid-link'; import Log from '@libs/Log'; -import {plaidLinkDefaultProps, plaidLinkPropTypes} from './plaidLinkPropTypes'; +import PlaidLinkProps from './types'; -function PlaidLink(props) { +function PlaidLink({token, onSuccess = () => {}, onError = () => {}, onExit = () => {}, onEvent, receivedRedirectURI}: PlaidLinkProps) { const [isPlaidLoaded, setIsPlaidLoaded] = useState(false); - const onSuccess = props.onSuccess; - const onError = props.onError; const successCallback = useCallback( - (publicToken, metadata) => { + (publicToken: string, metadata: PlaidLinkOnSuccessMetadata) => { onSuccess({publicToken, metadata}); }, [onSuccess], ); const {open, ready, error} = usePlaidLink({ - token: props.token, + token, onSuccess: successCallback, onExit: (exitError, metadata) => { Log.info('[PlaidLink] Exit: ', false, {exitError, metadata}); - props.onExit(); + onExit(); }, onEvent: (event, metadata) => { Log.info('[PlaidLink] Event: ', false, {event, metadata}); - props.onEvent(event, metadata); + onEvent?.(event, metadata); }, onLoad: () => setIsPlaidLoaded(true), // The redirect URI with an OAuth state ID. Needed to re-initialize the PlaidLink after directing the // user to their respective bank platform - receivedRedirectUri: props.receivedRedirectURI, + receivedRedirectUri: receivedRedirectURI, }); useEffect(() => { @@ -52,7 +50,5 @@ function PlaidLink(props) { return null; } -PlaidLink.propTypes = plaidLinkPropTypes; -PlaidLink.defaultProps = plaidLinkDefaultProps; PlaidLink.displayName = 'PlaidLink'; export default PlaidLink; From 231211a0d5408bfca96875528298e21c6feb24b3 Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Fri, 3 Nov 2023 09:23:54 +0100 Subject: [PATCH 011/391] migrate PlaidLinks native module to TypeScript, create a file for types --- .../{index.android.js => index.android.ts} | 0 .../{index.ios.js => index.ios.ts} | 0 src/components/PlaidLink/types.ts | 24 +++++++++++++++++++ 3 files changed, 24 insertions(+) rename src/components/PlaidLink/nativeModule/{index.android.js => index.android.ts} (100%) rename src/components/PlaidLink/nativeModule/{index.ios.js => index.ios.ts} (100%) create mode 100644 src/components/PlaidLink/types.ts diff --git a/src/components/PlaidLink/nativeModule/index.android.js b/src/components/PlaidLink/nativeModule/index.android.ts similarity index 100% rename from src/components/PlaidLink/nativeModule/index.android.js rename to src/components/PlaidLink/nativeModule/index.android.ts diff --git a/src/components/PlaidLink/nativeModule/index.ios.js b/src/components/PlaidLink/nativeModule/index.ios.ts similarity index 100% rename from src/components/PlaidLink/nativeModule/index.ios.js rename to src/components/PlaidLink/nativeModule/index.ios.ts diff --git a/src/components/PlaidLink/types.ts b/src/components/PlaidLink/types.ts new file mode 100644 index 000000000000..06b81d06b5c9 --- /dev/null +++ b/src/components/PlaidLink/types.ts @@ -0,0 +1,24 @@ +import {PlaidLinkOnEvent, PlaidLinkOnSuccessMetadata} from 'react-plaid-link'; + +type PlaidLinkProps = { + // Plaid Link SDK public token used to initialize the Plaid SDK + token: string; + + // Callback to execute once the user taps continue after successfully entering their account information + onSuccess?: (args: {publicToken?: string; metadata: PlaidLinkOnSuccessMetadata}) => void; + + // Callback to execute when there is an error event emitted by the Plaid SDK + onError?: (error: ErrorEvent | null) => void; + + // Callback to execute when the user leaves the Plaid widget flow without entering any information + onExit?: () => void; + + // Callback to execute whenever a Plaid event occurs + onEvent?: PlaidLinkOnEvent; + + // The redirect URI with an OAuth state ID. Needed to re-initialize the PlaidLink after directing the + // user to their respective bank platform + receivedRedirectURI?: string; +}; + +export default PlaidLinkProps; From a88d04761c46b651bd1f270edc5f29eb86a048b4 Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Mon, 6 Nov 2023 15:59:55 +0100 Subject: [PATCH 012/391] migrate native PlaidLink to TypeScript --- src/components/PlaidLink/index.native.tsx | 12 +++---- .../PlaidLink/plaidLinkPropTypes.js | 31 ------------------- src/components/PlaidLink/types.ts | 7 +++-- 3 files changed, 9 insertions(+), 41 deletions(-) delete mode 100644 src/components/PlaidLink/plaidLinkPropTypes.js diff --git a/src/components/PlaidLink/index.native.tsx b/src/components/PlaidLink/index.native.tsx index e1e9e7756620..874d7c77414c 100644 --- a/src/components/PlaidLink/index.native.tsx +++ b/src/components/PlaidLink/index.native.tsx @@ -2,26 +2,26 @@ import {useEffect} from 'react'; import {dismissLink, LinkEvent, openLink, useDeepLinkRedirector, usePlaidEmitter} from 'react-native-plaid-link-sdk'; import Log from '@libs/Log'; import CONST from '@src/CONST'; -import {plaidLinkDefaultProps, plaidLinkPropTypes} from './plaidLinkPropTypes'; import PlaidLinkProps from './types'; function PlaidLink({token, onSuccess = () => {}, onExit = () => {}, onEvent}: PlaidLinkProps) { useDeepLinkRedirector(); usePlaidEmitter((event: LinkEvent) => { - Log.info('[PlaidLink] Handled Plaid Event: ', false, event); + Log.info('[PlaidLink] Handled Plaid Event: ', false, event.eventName); onEvent?.(event.eventName, event.metadata); }); useEffect(() => { - onEvent?.(CONST.BANK_ACCOUNT.PLAID.EVENTS_NAME.OPEN, {}); + onEvent?.(CONST.BANK_ACCOUNT.PLAID.EVENTS_NAME.OPEN); openLink({ tokenConfig: { token, + noLoadingState: false, }, onSuccess: ({publicToken, metadata}) => { onSuccess({publicToken, metadata}); }, - onExit: (exitError, metadata) => { - Log.info('[PlaidLink] Exit: ', false, {exitError, metadata}); + onExit: ({error, metadata}) => { + Log.info('[PlaidLink] Exit: ', false, {error, metadata}); onExit(); }, }); @@ -36,8 +36,6 @@ function PlaidLink({token, onSuccess = () => {}, onExit = () => {}, onEvent}: Pl return null; } -PlaidLink.propTypes = plaidLinkPropTypes; -PlaidLink.defaultProps = plaidLinkDefaultProps; PlaidLink.displayName = 'PlaidLink'; export default PlaidLink; diff --git a/src/components/PlaidLink/plaidLinkPropTypes.js b/src/components/PlaidLink/plaidLinkPropTypes.js deleted file mode 100644 index 6d647d26f17e..000000000000 --- a/src/components/PlaidLink/plaidLinkPropTypes.js +++ /dev/null @@ -1,31 +0,0 @@ -import PropTypes from 'prop-types'; - -const plaidLinkPropTypes = { - // Plaid Link SDK public token used to initialize the Plaid SDK - token: PropTypes.string.isRequired, - - // Callback to execute once the user taps continue after successfully entering their account information - onSuccess: PropTypes.func, - - // Callback to execute when there is an error event emitted by the Plaid SDK - onError: PropTypes.func, - - // Callback to execute when the user leaves the Plaid widget flow without entering any information - onExit: PropTypes.func, - - // Callback to execute whenever a Plaid event occurs - onEvent: PropTypes.func, - - // The redirect URI with an OAuth state ID. Needed to re-initialize the PlaidLink after directing the - // user to their respective bank platform - receivedRedirectURI: PropTypes.string, -}; - -const plaidLinkDefaultProps = { - onSuccess: () => {}, - onError: () => {}, - onExit: () => {}, - receivedRedirectURI: null, -}; - -export {plaidLinkPropTypes, plaidLinkDefaultProps}; diff --git a/src/components/PlaidLink/types.ts b/src/components/PlaidLink/types.ts index 06b81d06b5c9..4fc44cbf9b9c 100644 --- a/src/components/PlaidLink/types.ts +++ b/src/components/PlaidLink/types.ts @@ -1,11 +1,12 @@ -import {PlaidLinkOnEvent, PlaidLinkOnSuccessMetadata} from 'react-plaid-link'; +import {LinkEventMetadata, LinkSuccessMetadata} from 'react-native-plaid-link-sdk'; +import {PlaidLinkOnEventMetadata, PlaidLinkOnSuccessMetadata, PlaidLinkStableEvent} from 'react-plaid-link'; type PlaidLinkProps = { // Plaid Link SDK public token used to initialize the Plaid SDK token: string; // Callback to execute once the user taps continue after successfully entering their account information - onSuccess?: (args: {publicToken?: string; metadata: PlaidLinkOnSuccessMetadata}) => void; + onSuccess?: (args: {publicToken?: string; metadata: PlaidLinkOnSuccessMetadata | LinkSuccessMetadata}) => void; // Callback to execute when there is an error event emitted by the Plaid SDK onError?: (error: ErrorEvent | null) => void; @@ -14,7 +15,7 @@ type PlaidLinkProps = { onExit?: () => void; // Callback to execute whenever a Plaid event occurs - onEvent?: PlaidLinkOnEvent; + onEvent?: (eventName: PlaidLinkStableEvent | string, metadata?: PlaidLinkOnEventMetadata | LinkEventMetadata) => void; // The redirect URI with an OAuth state ID. Needed to re-initialize the PlaidLink after directing the // user to their respective bank platform From eacebd2c6e12d71540d7bab3ef4adf5065fa1b7e Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Tue, 7 Nov 2023 12:00:22 +0100 Subject: [PATCH 013/391] make onEvent a required prop to avoid optional chaining --- src/components/PlaidLink/index.native.tsx | 4 ++-- src/components/PlaidLink/index.tsx | 2 +- src/components/PlaidLink/types.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/PlaidLink/index.native.tsx b/src/components/PlaidLink/index.native.tsx index 874d7c77414c..b9accb0c0ad7 100644 --- a/src/components/PlaidLink/index.native.tsx +++ b/src/components/PlaidLink/index.native.tsx @@ -8,10 +8,10 @@ function PlaidLink({token, onSuccess = () => {}, onExit = () => {}, onEvent}: Pl useDeepLinkRedirector(); usePlaidEmitter((event: LinkEvent) => { Log.info('[PlaidLink] Handled Plaid Event: ', false, event.eventName); - onEvent?.(event.eventName, event.metadata); + onEvent(event.eventName, event.metadata); }); useEffect(() => { - onEvent?.(CONST.BANK_ACCOUNT.PLAID.EVENTS_NAME.OPEN); + onEvent(CONST.BANK_ACCOUNT.PLAID.EVENTS_NAME.OPEN); openLink({ tokenConfig: { token, diff --git a/src/components/PlaidLink/index.tsx b/src/components/PlaidLink/index.tsx index 39b9ffda54b2..2109771473aa 100644 --- a/src/components/PlaidLink/index.tsx +++ b/src/components/PlaidLink/index.tsx @@ -21,7 +21,7 @@ function PlaidLink({token, onSuccess = () => {}, onError = () => {}, onExit = () }, onEvent: (event, metadata) => { Log.info('[PlaidLink] Event: ', false, {event, metadata}); - onEvent?.(event, metadata); + onEvent(event, metadata); }, onLoad: () => setIsPlaidLoaded(true), diff --git a/src/components/PlaidLink/types.ts b/src/components/PlaidLink/types.ts index 4fc44cbf9b9c..fe23e09151ca 100644 --- a/src/components/PlaidLink/types.ts +++ b/src/components/PlaidLink/types.ts @@ -15,7 +15,7 @@ type PlaidLinkProps = { onExit?: () => void; // Callback to execute whenever a Plaid event occurs - onEvent?: (eventName: PlaidLinkStableEvent | string, metadata?: PlaidLinkOnEventMetadata | LinkEventMetadata) => void; + onEvent: (eventName: PlaidLinkStableEvent | string, metadata?: PlaidLinkOnEventMetadata | LinkEventMetadata) => void; // The redirect URI with an OAuth state ID. Needed to re-initialize the PlaidLink after directing the // user to their respective bank platform From 8e62b62b6eab86925fe37a6c58e94c15e331d28f Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Fri, 17 Nov 2023 09:02:10 +0100 Subject: [PATCH 014/391] remove unused nativeModule --- src/components/PlaidLink/nativeModule/index.android.ts | 3 --- src/components/PlaidLink/nativeModule/index.ios.ts | 3 --- 2 files changed, 6 deletions(-) delete mode 100644 src/components/PlaidLink/nativeModule/index.android.ts delete mode 100644 src/components/PlaidLink/nativeModule/index.ios.ts diff --git a/src/components/PlaidLink/nativeModule/index.android.ts b/src/components/PlaidLink/nativeModule/index.android.ts deleted file mode 100644 index d4280feddb8e..000000000000 --- a/src/components/PlaidLink/nativeModule/index.android.ts +++ /dev/null @@ -1,3 +0,0 @@ -import {NativeModules} from 'react-native'; - -export default NativeModules.PlaidAndroid; diff --git a/src/components/PlaidLink/nativeModule/index.ios.ts b/src/components/PlaidLink/nativeModule/index.ios.ts deleted file mode 100644 index 78d4315eac2d..000000000000 --- a/src/components/PlaidLink/nativeModule/index.ios.ts +++ /dev/null @@ -1,3 +0,0 @@ -import {NativeModules} from 'react-native'; - -export default NativeModules.RNLinksdk; From a966eb3392d765491070ea070c02e69f67608510 Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Mon, 27 Nov 2023 13:41:14 +0100 Subject: [PATCH 015/391] pass event to Log.info --- src/components/PlaidLink/index.native.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/PlaidLink/index.native.tsx b/src/components/PlaidLink/index.native.tsx index b9accb0c0ad7..02d4669bc861 100644 --- a/src/components/PlaidLink/index.native.tsx +++ b/src/components/PlaidLink/index.native.tsx @@ -7,7 +7,7 @@ import PlaidLinkProps from './types'; function PlaidLink({token, onSuccess = () => {}, onExit = () => {}, onEvent}: PlaidLinkProps) { useDeepLinkRedirector(); usePlaidEmitter((event: LinkEvent) => { - Log.info('[PlaidLink] Handled Plaid Event: ', false, event.eventName); + Log.info('[PlaidLink] Handled Plaid Event: ', false, {...event}); onEvent(event.eventName, event.metadata); }); useEffect(() => { From d7553080efebcda8e4f2fc02631a1c12ca58bf95 Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Mon, 27 Nov 2023 13:45:11 +0100 Subject: [PATCH 016/391] allow metadata to be undefined in onEvent --- src/components/AddPlaidBankAccount.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/AddPlaidBankAccount.js b/src/components/AddPlaidBankAccount.js index ec4ddd623929..0b23704b5b26 100644 --- a/src/components/AddPlaidBankAccount.js +++ b/src/components/AddPlaidBankAccount.js @@ -209,7 +209,7 @@ function AddPlaidBankAccount({ // Handle Plaid login errors (will potentially reset plaid token and item depending on the error) if (event === 'ERROR') { Log.hmmm('[PlaidLink] Error: ', metadata); - if (bankAccountID && metadata.error_code) { + if (bankAccountID && metadata && metadata.error_code) { BankAccounts.handlePlaidError(bankAccountID, metadata.error_code, metadata.error_message, metadata.request_id); } } From 624875cc26a1d0365e01f4eccffb52219cfa16ed Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Mon, 27 Nov 2023 13:49:00 +0100 Subject: [PATCH 017/391] make publicToken required param --- src/components/PlaidLink/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/PlaidLink/types.ts b/src/components/PlaidLink/types.ts index fe23e09151ca..dda6d9d869cb 100644 --- a/src/components/PlaidLink/types.ts +++ b/src/components/PlaidLink/types.ts @@ -6,7 +6,7 @@ type PlaidLinkProps = { token: string; // Callback to execute once the user taps continue after successfully entering their account information - onSuccess?: (args: {publicToken?: string; metadata: PlaidLinkOnSuccessMetadata | LinkSuccessMetadata}) => void; + onSuccess?: (args: {publicToken: string; metadata: PlaidLinkOnSuccessMetadata | LinkSuccessMetadata}) => void; // Callback to execute when there is an error event emitted by the Plaid SDK onError?: (error: ErrorEvent | null) => void; From a8af74a8761d5e060858b702a92457ec7fec0ee1 Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Mon, 27 Nov 2023 14:11:36 +0100 Subject: [PATCH 018/391] change eventName type to string --- src/components/PlaidLink/types.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/PlaidLink/types.ts b/src/components/PlaidLink/types.ts index dda6d9d869cb..1034eb935f74 100644 --- a/src/components/PlaidLink/types.ts +++ b/src/components/PlaidLink/types.ts @@ -1,5 +1,5 @@ import {LinkEventMetadata, LinkSuccessMetadata} from 'react-native-plaid-link-sdk'; -import {PlaidLinkOnEventMetadata, PlaidLinkOnSuccessMetadata, PlaidLinkStableEvent} from 'react-plaid-link'; +import {PlaidLinkOnEventMetadata, PlaidLinkOnSuccessMetadata} from 'react-plaid-link'; type PlaidLinkProps = { // Plaid Link SDK public token used to initialize the Plaid SDK @@ -15,7 +15,7 @@ type PlaidLinkProps = { onExit?: () => void; // Callback to execute whenever a Plaid event occurs - onEvent: (eventName: PlaidLinkStableEvent | string, metadata?: PlaidLinkOnEventMetadata | LinkEventMetadata) => void; + onEvent: (eventName: string, metadata?: PlaidLinkOnEventMetadata | LinkEventMetadata) => void; // The redirect URI with an OAuth state ID. Needed to re-initialize the PlaidLink after directing the // user to their respective bank platform From 019cd14886dfa8921a133f5d0c86436dc5c1ecec Mon Sep 17 00:00:00 2001 From: Monil Bhavsar Date: Mon, 27 Nov 2023 17:41:41 +0530 Subject: [PATCH 019/391] Correct return types --- src/pages/settings/InitialSettingsPage.js | 4 ++-- src/pages/workspace/WorkspacesListPage.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pages/settings/InitialSettingsPage.js b/src/pages/settings/InitialSettingsPage.js index 1bd57bcab32b..d6decff5a208 100755 --- a/src/pages/settings/InitialSettingsPage.js +++ b/src/pages/settings/InitialSettingsPage.js @@ -281,9 +281,9 @@ function InitialSettingsPage(props) { const getMenuItems = useMemo(() => { /** * @param {Boolean} isPaymentItem whether the item being rendered is the payments menu item - * @returns {Number} the user wallet balance + * @returns {String|undefined} the user's wallet balance */ - const getWalletBalance = (isPaymentItem) => isPaymentItem && CurrencyUtils.convertToDisplayString(props.userWallet.currentBalance); + const getWalletBalance = (isPaymentItem) => isPaymentItem ? CurrencyUtils.convertToDisplayString(props.userWallet.currentBalance) : undefined; return ( <> diff --git a/src/pages/workspace/WorkspacesListPage.js b/src/pages/workspace/WorkspacesListPage.js index 1e51c64a711c..2749ccb52b96 100755 --- a/src/pages/workspace/WorkspacesListPage.js +++ b/src/pages/workspace/WorkspacesListPage.js @@ -114,7 +114,7 @@ function WorkspacesListPage({policies, allPolicyMembers, reimbursementAccount, u /** * @param {Boolean} isPaymentItem whether the item being rendered is the payments menu item - * @returns {Number} the user wallet balance + * @returns {String|undefined} the user's wallet balance */ function getWalletBalance(isPaymentItem) { return isPaymentItem ? CurrencyUtils.convertToDisplayString(userWallet.currentBalance) : undefined; From b2f9eab37124a4f5cc6d23a50942218eed0eb2ca Mon Sep 17 00:00:00 2001 From: Monil Bhavsar Date: Mon, 27 Nov 2023 20:36:20 +0530 Subject: [PATCH 020/391] Fix lint --- src/pages/settings/InitialSettingsPage.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/settings/InitialSettingsPage.js b/src/pages/settings/InitialSettingsPage.js index d6decff5a208..61950e14337f 100755 --- a/src/pages/settings/InitialSettingsPage.js +++ b/src/pages/settings/InitialSettingsPage.js @@ -283,7 +283,7 @@ function InitialSettingsPage(props) { * @param {Boolean} isPaymentItem whether the item being rendered is the payments menu item * @returns {String|undefined} the user's wallet balance */ - const getWalletBalance = (isPaymentItem) => isPaymentItem ? CurrencyUtils.convertToDisplayString(props.userWallet.currentBalance) : undefined; + const getWalletBalance = (isPaymentItem) => (isPaymentItem ? CurrencyUtils.convertToDisplayString(props.userWallet.currentBalance) : undefined); return ( <> From 54b2c9b34666ac6263e0524f04a2a49c94b7e3fa Mon Sep 17 00:00:00 2001 From: Yauheni Pasiukevich Date: Wed, 22 Nov 2023 13:35:11 +0100 Subject: [PATCH 021/391] fix type for activate card flow --- src/libs/actions/Card.js | 2 +- src/pages/settings/Wallet/ActivatePhysicalCardPage.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libs/actions/Card.js b/src/libs/actions/Card.js index 9adcd3803766..68642bd8fdf1 100644 --- a/src/libs/actions/Card.js +++ b/src/libs/actions/Card.js @@ -93,7 +93,7 @@ function requestReplacementExpensifyCard(cardId, reason) { /** * Activates the physical Expensify card based on the last four digits of the card number * - * @param {Number} cardLastFourDigits + * @param {String} cardLastFourDigits * @param {Number} cardID */ function activatePhysicalExpensifyCard(cardLastFourDigits, cardID) { diff --git a/src/pages/settings/Wallet/ActivatePhysicalCardPage.js b/src/pages/settings/Wallet/ActivatePhysicalCardPage.js index e20721b5db4a..3534ef5c064c 100644 --- a/src/pages/settings/Wallet/ActivatePhysicalCardPage.js +++ b/src/pages/settings/Wallet/ActivatePhysicalCardPage.js @@ -123,7 +123,7 @@ function ActivatePhysicalCardPage({ return; } - CardSettings.activatePhysicalExpensifyCard(Number(lastFourDigits), cardID); + CardSettings.activatePhysicalExpensifyCard(lastFourDigits, cardID); }, [lastFourDigits, cardID, translate]); if (_.isEmpty(physicalCard)) { From c5584ecc1d6ab8d015847eeb29d8254204822535 Mon Sep 17 00:00:00 2001 From: Artem Makushov Date: Tue, 28 Nov 2023 13:46:48 +0100 Subject: [PATCH 022/391] removed an extra space --- src/languages/en.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index d661ee1ad97b..7bc9c985ad66 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -1952,7 +1952,7 @@ export default { buttonText1: 'Request money, ', buttonText2: `get $${CONST.REFERRAL_PROGRAM.REVENUE}.`, header: `Request money, get $${CONST.REFERRAL_PROGRAM.REVENUE}`, - body1: `Request money from a new Expensify account. Get $${CONST.REFERRAL_PROGRAM.REVENUE} once they start an annual subscription with two or more active members and make the first two payments toward their Expensify bill.`, + body1: `Request money from a new Expensify account. Get $${CONST.REFERRAL_PROGRAM.REVENUE} once they start an annual subscription with two or more active members and make the first two payments toward their Expensify bill.`, }, [CONST.REFERRAL_PROGRAM.CONTENT_TYPES.SEND_MONEY]: { buttonText1: 'Send money, ', From dcc202ad7739d03e834d500c357aa553bc91e447 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Tue, 28 Nov 2023 18:11:48 +0100 Subject: [PATCH 023/391] update patches --- ...eact-native+0.72.4+002+NumberOfLines.patch | 632 ++++++++++++++++++ ...ive+0.72.4+004+ModalKeyboardFlashing.patch | 18 + 2 files changed, 650 insertions(+) create mode 100644 patches/react-native+0.72.4+002+NumberOfLines.patch create mode 100644 patches/react-native+0.72.4+004+ModalKeyboardFlashing.patch diff --git a/patches/react-native+0.72.4+002+NumberOfLines.patch b/patches/react-native+0.72.4+002+NumberOfLines.patch new file mode 100644 index 000000000000..16fec4bc8363 --- /dev/null +++ b/patches/react-native+0.72.4+002+NumberOfLines.patch @@ -0,0 +1,632 @@ +diff --git a/node_modules/react-native/Libraries/Components/TextInput/RCTTextInputViewConfig.js b/node_modules/react-native/Libraries/Components/TextInput/RCTTextInputViewConfig.js +index 6f69329..d531bee 100644 +--- a/node_modules/react-native/Libraries/Components/TextInput/RCTTextInputViewConfig.js ++++ b/node_modules/react-native/Libraries/Components/TextInput/RCTTextInputViewConfig.js +@@ -144,6 +144,8 @@ const RCTTextInputViewConfig = { + placeholder: true, + autoCorrect: true, + multiline: true, ++ numberOfLines: true, ++ maximumNumberOfLines: true, + textContentType: true, + maxLength: true, + autoCapitalize: true, +diff --git a/node_modules/react-native/Libraries/Components/TextInput/TextInput.d.ts b/node_modules/react-native/Libraries/Components/TextInput/TextInput.d.ts +index 8badb2a..b19f197 100644 +--- a/node_modules/react-native/Libraries/Components/TextInput/TextInput.d.ts ++++ b/node_modules/react-native/Libraries/Components/TextInput/TextInput.d.ts +@@ -347,12 +347,6 @@ export interface TextInputAndroidProps { + */ + inlineImagePadding?: number | undefined; + +- /** +- * Sets the number of lines for a TextInput. +- * Use it with multiline set to true to be able to fill the lines. +- */ +- numberOfLines?: number | undefined; +- + /** + * Sets the return key to the label. Use it instead of `returnKeyType`. + * @platform android +@@ -663,11 +657,30 @@ export interface TextInputProps + */ + maxLength?: number | undefined; + ++ /** ++ * Sets the maximum number of lines for a TextInput. ++ * Use it with multiline set to true to be able to fill the lines. ++ */ ++ maxNumberOfLines?: number | undefined; ++ + /** + * If true, the text input can be multiple lines. The default value is false. + */ + multiline?: boolean | undefined; + ++ /** ++ * Sets the number of lines for a TextInput. ++ * Use it with multiline set to true to be able to fill the lines. ++ */ ++ numberOfLines?: number | undefined; ++ ++ /** ++ * Sets the number of rows for a TextInput. ++ * Use it with multiline set to true to be able to fill the lines. ++ */ ++ rows?: number | undefined; ++ ++ + /** + * Callback that is called when the text input is blurred + */ +diff --git a/node_modules/react-native/Libraries/Components/TextInput/TextInput.flow.js b/node_modules/react-native/Libraries/Components/TextInput/TextInput.flow.js +index 7ed4579..b1d994e 100644 +--- a/node_modules/react-native/Libraries/Components/TextInput/TextInput.flow.js ++++ b/node_modules/react-native/Libraries/Components/TextInput/TextInput.flow.js +@@ -343,26 +343,12 @@ type AndroidProps = $ReadOnly<{| + */ + inlineImagePadding?: ?number, + +- /** +- * Sets the number of lines for a `TextInput`. Use it with multiline set to +- * `true` to be able to fill the lines. +- * @platform android +- */ +- numberOfLines?: ?number, +- + /** + * Sets the return key to the label. Use it instead of `returnKeyType`. + * @platform android + */ + returnKeyLabel?: ?string, + +- /** +- * Sets the number of rows for a `TextInput`. Use it with multiline set to +- * `true` to be able to fill the lines. +- * @platform android +- */ +- rows?: ?number, +- + /** + * When `false`, it will prevent the soft keyboard from showing when the field is focused. + * Defaults to `true`. +@@ -632,6 +618,12 @@ export type Props = $ReadOnly<{| + */ + keyboardType?: ?KeyboardType, + ++ /** ++ * Sets the maximum number of lines for a `TextInput`. Use it with multiline set to ++ * `true` to be able to fill the lines. ++ */ ++ maxNumberOfLines?: ?number, ++ + /** + * Specifies largest possible scale a font can reach when `allowFontScaling` is enabled. + * Possible values: +@@ -653,6 +645,12 @@ export type Props = $ReadOnly<{| + */ + multiline?: ?boolean, + ++ /** ++ * Sets the number of lines for a `TextInput`. Use it with multiline set to ++ * `true` to be able to fill the lines. ++ */ ++ numberOfLines?: ?number, ++ + /** + * Callback that is called when the text input is blurred. + */ +@@ -814,6 +812,12 @@ export type Props = $ReadOnly<{| + */ + returnKeyType?: ?ReturnKeyType, + ++ /** ++ * Sets the number of rows for a `TextInput`. Use it with multiline set to ++ * `true` to be able to fill the lines. ++ */ ++ rows?: ?number, ++ + /** + * If `true`, the text input obscures the text entered so that sensitive text + * like passwords stay secure. The default value is `false`. Does not work with 'multiline={true}'. +diff --git a/node_modules/react-native/Libraries/Components/TextInput/TextInput.js b/node_modules/react-native/Libraries/Components/TextInput/TextInput.js +index 2127191..542fc06 100644 +--- a/node_modules/react-native/Libraries/Components/TextInput/TextInput.js ++++ b/node_modules/react-native/Libraries/Components/TextInput/TextInput.js +@@ -390,7 +390,6 @@ type AndroidProps = $ReadOnly<{| + /** + * Sets the number of lines for a `TextInput`. Use it with multiline set to + * `true` to be able to fill the lines. +- * @platform android + */ + numberOfLines?: ?number, + +@@ -403,10 +402,14 @@ type AndroidProps = $ReadOnly<{| + /** + * Sets the number of rows for a `TextInput`. Use it with multiline set to + * `true` to be able to fill the lines. +- * @platform android + */ + rows?: ?number, + ++ /** ++ * Sets the maximum number of lines the TextInput can have. ++ */ ++ maxNumberOfLines?: ?number, ++ + /** + * When `false`, it will prevent the soft keyboard from showing when the field is focused. + * Defaults to `true`. +@@ -1069,6 +1072,9 @@ function InternalTextInput(props: Props): React.Node { + accessibilityState, + id, + tabIndex, ++ rows, ++ numberOfLines, ++ maxNumberOfLines, + selection: propsSelection, + ...otherProps + } = props; +@@ -1427,6 +1433,8 @@ function InternalTextInput(props: Props): React.Node { + focusable={tabIndex !== undefined ? !tabIndex : focusable} + mostRecentEventCount={mostRecentEventCount} + nativeID={id ?? props.nativeID} ++ numberOfLines={props.rows ?? props.numberOfLines} ++ maximumNumberOfLines={maxNumberOfLines} + onBlur={_onBlur} + onKeyPressSync={props.unstable_onKeyPressSync} + onChange={_onChange} +@@ -1482,6 +1490,7 @@ function InternalTextInput(props: Props): React.Node { + mostRecentEventCount={mostRecentEventCount} + nativeID={id ?? props.nativeID} + numberOfLines={props.rows ?? props.numberOfLines} ++ maximumNumberOfLines={maxNumberOfLines} + onBlur={_onBlur} + onChange={_onChange} + onFocus={_onFocus} +diff --git a/node_modules/react-native/Libraries/Text/Text.js b/node_modules/react-native/Libraries/Text/Text.js +index df548af..e02f5da 100644 +--- a/node_modules/react-native/Libraries/Text/Text.js ++++ b/node_modules/react-native/Libraries/Text/Text.js +@@ -18,7 +18,11 @@ import processColor from '../StyleSheet/processColor'; + import {getAccessibilityRoleFromRole} from '../Utilities/AcessibilityMapping'; + import Platform from '../Utilities/Platform'; + import TextAncestor from './TextAncestor'; +-import {NativeText, NativeVirtualText} from './TextNativeComponent'; ++import { ++ CONTAINS_MAX_NUMBER_OF_LINES_RENAME, ++ NativeText, ++ NativeVirtualText, ++} from './TextNativeComponent'; + import * as React from 'react'; + import {useContext, useMemo, useState} from 'react'; + +@@ -59,6 +63,7 @@ const Text: React.AbstractComponent< + pressRetentionOffset, + role, + suppressHighlighting, ++ numberOfLines, + ...restProps + } = props; + +@@ -192,14 +197,33 @@ const Text: React.AbstractComponent< + } + } + +- let numberOfLines = restProps.numberOfLines; ++ let numberOfLinesValue = numberOfLines; + if (numberOfLines != null && !(numberOfLines >= 0)) { + console.error( + `'numberOfLines' in must be a non-negative number, received: ${numberOfLines}. The value will be set to 0.`, + ); +- numberOfLines = 0; ++ numberOfLinesValue = 0; + } + ++ const numberOfLinesProps = useMemo((): { ++ maximumNumberOfLines?: ?number, ++ numberOfLines?: ?number, ++ } => { ++ // FIXME: Current logic is breaking all Text components. ++ // if (CONTAINS_MAX_NUMBER_OF_LINES_RENAME) { ++ // return { ++ // maximumNumberOfLines: numberOfLinesValue, ++ // }; ++ // } else { ++ // return { ++ // numberOfLines: numberOfLinesValue, ++ // }; ++ // } ++ return { ++ maximumNumberOfLines: numberOfLinesValue, ++ }; ++ }, [numberOfLinesValue]); ++ + const hasTextAncestor = useContext(TextAncestor); + + const _accessible = Platform.select({ +@@ -241,7 +265,6 @@ const Text: React.AbstractComponent< + isHighlighted={isHighlighted} + isPressable={isPressable} + nativeID={id ?? nativeID} +- numberOfLines={numberOfLines} + ref={forwardedRef} + selectable={_selectable} + selectionColor={selectionColor} +@@ -252,6 +275,7 @@ const Text: React.AbstractComponent< + + #import ++#import ++#import + + @implementation RCTMultilineTextInputViewManager + +@@ -17,8 +19,21 @@ - (UIView *)view + return [[RCTMultilineTextInputView alloc] initWithBridge:self.bridge]; + } + ++- (RCTShadowView *)shadowView ++{ ++ RCTBaseTextInputShadowView *shadowView = (RCTBaseTextInputShadowView *)[super shadowView]; ++ ++ shadowView.maximumNumberOfLines = 0; ++ shadowView.exactNumberOfLines = 0; ++ ++ return shadowView; ++} ++ + #pragma mark - Multiline (aka TextView) specific properties + + RCT_REMAP_VIEW_PROPERTY(dataDetectorTypes, backedTextInputView.dataDetectorTypes, UIDataDetectorTypes) + ++RCT_EXPORT_SHADOW_PROPERTY(maximumNumberOfLines, NSInteger) ++RCT_REMAP_SHADOW_PROPERTY(numberOfLines, exactNumberOfLines, NSInteger) ++ + @end +diff --git a/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputShadowView.h b/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputShadowView.h +index 8f4cf7e..6238ebc 100644 +--- a/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputShadowView.h ++++ b/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputShadowView.h +@@ -16,6 +16,7 @@ NS_ASSUME_NONNULL_BEGIN + @property (nonatomic, copy, nullable) NSString *text; + @property (nonatomic, copy, nullable) NSString *placeholder; + @property (nonatomic, assign) NSInteger maximumNumberOfLines; ++@property (nonatomic, assign) NSInteger exactNumberOfLines; + @property (nonatomic, copy, nullable) RCTDirectEventBlock onContentSizeChange; + + - (void)uiManagerWillPerformMounting; +diff --git a/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputShadowView.m b/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputShadowView.m +index 04d2446..9d77743 100644 +--- a/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputShadowView.m ++++ b/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputShadowView.m +@@ -218,7 +218,22 @@ - (NSAttributedString *)measurableAttributedText + + - (CGSize)sizeThatFitsMinimumSize:(CGSize)minimumSize maximumSize:(CGSize)maximumSize + { +- NSAttributedString *attributedText = [self measurableAttributedText]; ++ NSMutableAttributedString *attributedText = [[self measurableAttributedText] mutableCopy]; ++ ++ /* ++ * The block below is responsible for setting the exact height of the view in lines ++ * Unfortunatelly, iOS doesn't export any easy way to do it. So we set maximumNumberOfLines ++ * prop and then add random lines at the front. However, they are only used for layout ++ * so they are not visible on the screen. ++ */ ++ if (self.exactNumberOfLines) { ++ NSMutableString *newLines = [NSMutableString stringWithCapacity:self.exactNumberOfLines]; ++ for (NSUInteger i = 0UL; i < self.exactNumberOfLines; ++i) { ++ [newLines appendString:@"\n"]; ++ } ++ [attributedText insertAttributedString:[[NSAttributedString alloc] initWithString:newLines attributes:self.textAttributes.effectiveTextAttributes] atIndex:0]; ++ _maximumNumberOfLines = self.exactNumberOfLines; ++ } + + if (!_textStorage) { + _textContainer = [NSTextContainer new]; +diff --git a/node_modules/react-native/Libraries/Text/TextInput/Singleline/RCTSinglelineTextInputViewManager.m b/node_modules/react-native/Libraries/Text/TextInput/Singleline/RCTSinglelineTextInputViewManager.m +index 413ac42..56d039c 100644 +--- a/node_modules/react-native/Libraries/Text/TextInput/Singleline/RCTSinglelineTextInputViewManager.m ++++ b/node_modules/react-native/Libraries/Text/TextInput/Singleline/RCTSinglelineTextInputViewManager.m +@@ -19,6 +19,7 @@ - (RCTShadowView *)shadowView + RCTBaseTextInputShadowView *shadowView = (RCTBaseTextInputShadowView *)[super shadowView]; + + shadowView.maximumNumberOfLines = 1; ++ shadowView.exactNumberOfLines = 0; + + return shadowView; + } +diff --git a/node_modules/react-native/Libraries/Text/TextNativeComponent.js b/node_modules/react-native/Libraries/Text/TextNativeComponent.js +index 0d59904..3216e43 100644 +--- a/node_modules/react-native/Libraries/Text/TextNativeComponent.js ++++ b/node_modules/react-native/Libraries/Text/TextNativeComponent.js +@@ -9,6 +9,7 @@ + */ + + import {createViewConfig} from '../NativeComponent/ViewConfig'; ++import getNativeComponentAttributes from '../ReactNative/getNativeComponentAttributes'; + import UIManager from '../ReactNative/UIManager'; + import createReactNativeComponentClass from '../Renderer/shims/createReactNativeComponentClass'; + import {type HostComponent} from '../Renderer/shims/ReactNativeTypes'; +@@ -18,6 +19,7 @@ import {type TextProps} from './TextProps'; + + type NativeTextProps = $ReadOnly<{ + ...TextProps, ++ maximumNumberOfLines?: ?number, + isHighlighted?: ?boolean, + selectionColor?: ?ProcessedColorValue, + onClick?: ?(event: PressEvent) => mixed, +@@ -31,7 +33,7 @@ const textViewConfig = { + validAttributes: { + isHighlighted: true, + isPressable: true, +- numberOfLines: true, ++ maximumNumberOfLines: true, + ellipsizeMode: true, + allowFontScaling: true, + dynamicTypeRamp: true, +@@ -73,6 +75,12 @@ export const NativeText: HostComponent = + createViewConfig(textViewConfig), + ): any); + ++const jestIsDefined = typeof jest !== 'undefined'; ++export const CONTAINS_MAX_NUMBER_OF_LINES_RENAME: boolean = jestIsDefined ++ ? true ++ : getNativeComponentAttributes('RCTText')?.NativeProps ++ ?.maximumNumberOfLines === 'number'; ++ + export const NativeVirtualText: HostComponent = + !global.RN$Bridgeless && !UIManager.hasViewManagerConfig('RCTVirtualText') + ? NativeText +diff --git a/node_modules/react-native/ReactCommon/react/renderer/attributedstring/ParagraphAttributes.cpp b/node_modules/react-native/ReactCommon/react/renderer/attributedstring/ParagraphAttributes.cpp +index 2994aca..fff0d5e 100644 +--- a/node_modules/react-native/ReactCommon/react/renderer/attributedstring/ParagraphAttributes.cpp ++++ b/node_modules/react-native/ReactCommon/react/renderer/attributedstring/ParagraphAttributes.cpp +@@ -16,6 +16,7 @@ namespace facebook::react { + + bool ParagraphAttributes::operator==(const ParagraphAttributes &rhs) const { + return std::tie( ++ numberOfLines, + maximumNumberOfLines, + ellipsizeMode, + textBreakStrategy, +@@ -23,6 +24,7 @@ bool ParagraphAttributes::operator==(const ParagraphAttributes &rhs) const { + includeFontPadding, + android_hyphenationFrequency) == + std::tie( ++ rhs.numberOfLines, + rhs.maximumNumberOfLines, + rhs.ellipsizeMode, + rhs.textBreakStrategy, +@@ -42,6 +44,7 @@ bool ParagraphAttributes::operator!=(const ParagraphAttributes &rhs) const { + #if RN_DEBUG_STRING_CONVERTIBLE + SharedDebugStringConvertibleList ParagraphAttributes::getDebugProps() const { + return { ++ debugStringConvertibleItem("numberOfLines", numberOfLines), + debugStringConvertibleItem("maximumNumberOfLines", maximumNumberOfLines), + debugStringConvertibleItem("ellipsizeMode", ellipsizeMode), + debugStringConvertibleItem("textBreakStrategy", textBreakStrategy), +diff --git a/node_modules/react-native/ReactCommon/react/renderer/attributedstring/ParagraphAttributes.h b/node_modules/react-native/ReactCommon/react/renderer/attributedstring/ParagraphAttributes.h +index f5f87c6..b7d1e90 100644 +--- a/node_modules/react-native/ReactCommon/react/renderer/attributedstring/ParagraphAttributes.h ++++ b/node_modules/react-native/ReactCommon/react/renderer/attributedstring/ParagraphAttributes.h +@@ -30,6 +30,11 @@ class ParagraphAttributes : public DebugStringConvertible { + public: + #pragma mark - Fields + ++ /* ++ * Number of lines which paragraph takes. ++ */ ++ int numberOfLines{}; ++ + /* + * Maximum number of lines which paragraph can take. + * Zero value represents "no limit". +@@ -92,6 +97,7 @@ struct hash { + const facebook::react::ParagraphAttributes &attributes) const { + return folly::hash::hash_combine( + 0, ++ attributes.numberOfLines, + attributes.maximumNumberOfLines, + attributes.ellipsizeMode, + attributes.textBreakStrategy, +diff --git a/node_modules/react-native/ReactCommon/react/renderer/attributedstring/conversions.h b/node_modules/react-native/ReactCommon/react/renderer/attributedstring/conversions.h +index 8687b89..eab75f4 100644 +--- a/node_modules/react-native/ReactCommon/react/renderer/attributedstring/conversions.h ++++ b/node_modules/react-native/ReactCommon/react/renderer/attributedstring/conversions.h +@@ -835,10 +835,16 @@ inline ParagraphAttributes convertRawProp( + ParagraphAttributes const &defaultParagraphAttributes) { + auto paragraphAttributes = ParagraphAttributes{}; + +- paragraphAttributes.maximumNumberOfLines = convertRawProp( ++ paragraphAttributes.numberOfLines = convertRawProp( + context, + rawProps, + "numberOfLines", ++ sourceParagraphAttributes.numberOfLines, ++ defaultParagraphAttributes.numberOfLines); ++ paragraphAttributes.maximumNumberOfLines = convertRawProp( ++ context, ++ rawProps, ++ "maximumNumberOfLines", + sourceParagraphAttributes.maximumNumberOfLines, + defaultParagraphAttributes.maximumNumberOfLines); + paragraphAttributes.ellipsizeMode = convertRawProp( +@@ -913,6 +919,7 @@ inline std::string toString(AttributedString::Range const &range) { + inline folly::dynamic toDynamic( + const ParagraphAttributes ¶graphAttributes) { + auto values = folly::dynamic::object(); ++ values("numberOfLines", paragraphAttributes.numberOfLines); + values("maximumNumberOfLines", paragraphAttributes.maximumNumberOfLines); + values("ellipsizeMode", toString(paragraphAttributes.ellipsizeMode)); + values("textBreakStrategy", toString(paragraphAttributes.textBreakStrategy)); +@@ -1118,6 +1125,7 @@ constexpr static MapBuffer::Key PA_KEY_TEXT_BREAK_STRATEGY = 2; + constexpr static MapBuffer::Key PA_KEY_ADJUST_FONT_SIZE_TO_FIT = 3; + constexpr static MapBuffer::Key PA_KEY_INCLUDE_FONT_PADDING = 4; + constexpr static MapBuffer::Key PA_KEY_HYPHENATION_FREQUENCY = 5; ++constexpr static MapBuffer::Key PA_KEY_NUMBER_OF_LINES = 6; + + inline MapBuffer toMapBuffer(const ParagraphAttributes ¶graphAttributes) { + auto builder = MapBufferBuilder(); +@@ -1135,6 +1143,8 @@ inline MapBuffer toMapBuffer(const ParagraphAttributes ¶graphAttributes) { + builder.putString( + PA_KEY_HYPHENATION_FREQUENCY, + toString(paragraphAttributes.android_hyphenationFrequency)); ++ builder.putInt( ++ PA_KEY_NUMBER_OF_LINES, paragraphAttributes.numberOfLines); + + return builder.build(); + } +diff --git a/node_modules/react-native/ReactCommon/react/renderer/components/textinput/androidtextinput/react/renderer/components/androidtextinput/AndroidTextInputProps.cpp b/node_modules/react-native/ReactCommon/react/renderer/components/textinput/androidtextinput/react/renderer/components/androidtextinput/AndroidTextInputProps.cpp +index 9953e22..98eb3da 100644 +--- a/node_modules/react-native/ReactCommon/react/renderer/components/textinput/androidtextinput/react/renderer/components/androidtextinput/AndroidTextInputProps.cpp ++++ b/node_modules/react-native/ReactCommon/react/renderer/components/textinput/androidtextinput/react/renderer/components/androidtextinput/AndroidTextInputProps.cpp +@@ -56,6 +56,10 @@ AndroidTextInputProps::AndroidTextInputProps( + "numberOfLines", + sourceProps.numberOfLines, + {0})), ++ maximumNumberOfLines(CoreFeatures::enablePropIteratorSetter? sourceProps.maximumNumberOfLines : convertRawProp(context, rawProps, ++ "maximumNumberOfLines", ++ sourceProps.maximumNumberOfLines, ++ {0})), + disableFullscreenUI(CoreFeatures::enablePropIteratorSetter? sourceProps.disableFullscreenUI : convertRawProp(context, rawProps, + "disableFullscreenUI", + sourceProps.disableFullscreenUI, +@@ -281,6 +285,12 @@ void AndroidTextInputProps::setProp( + value, + paragraphAttributes, + maximumNumberOfLines, ++ "maximumNumberOfLines"); ++ REBUILD_FIELD_SWITCH_CASE( ++ paDefaults, ++ value, ++ paragraphAttributes, ++ numberOfLines, + "numberOfLines"); + REBUILD_FIELD_SWITCH_CASE( + paDefaults, value, paragraphAttributes, ellipsizeMode, "ellipsizeMode"); +@@ -323,6 +333,7 @@ void AndroidTextInputProps::setProp( + } + + switch (hash) { ++ RAW_SET_PROP_SWITCH_CASE_BASIC(maximumNumberOfLines); + RAW_SET_PROP_SWITCH_CASE_BASIC(autoComplete); + RAW_SET_PROP_SWITCH_CASE_BASIC(returnKeyLabel); + RAW_SET_PROP_SWITCH_CASE_BASIC(numberOfLines); +@@ -422,6 +433,7 @@ void AndroidTextInputProps::setProp( + // TODO T53300085: support this in codegen; this was hand-written + folly::dynamic AndroidTextInputProps::getDynamic() const { + folly::dynamic props = folly::dynamic::object(); ++ props["maximumNumberOfLines"] = maximumNumberOfLines; + props["autoComplete"] = autoComplete; + props["returnKeyLabel"] = returnKeyLabel; + props["numberOfLines"] = numberOfLines; +diff --git a/node_modules/react-native/ReactCommon/react/renderer/components/textinput/androidtextinput/react/renderer/components/androidtextinput/AndroidTextInputProps.h b/node_modules/react-native/ReactCommon/react/renderer/components/textinput/androidtextinput/react/renderer/components/androidtextinput/AndroidTextInputProps.h +index ba39ebb..ead28e3 100644 +--- a/node_modules/react-native/ReactCommon/react/renderer/components/textinput/androidtextinput/react/renderer/components/androidtextinput/AndroidTextInputProps.h ++++ b/node_modules/react-native/ReactCommon/react/renderer/components/textinput/androidtextinput/react/renderer/components/androidtextinput/AndroidTextInputProps.h +@@ -84,6 +84,7 @@ class AndroidTextInputProps final : public ViewProps, public BaseTextProps { + std::string autoComplete{}; + std::string returnKeyLabel{}; + int numberOfLines{0}; ++ int maximumNumberOfLines{0}; + bool disableFullscreenUI{false}; + std::string textBreakStrategy{}; + SharedColor underlineColorAndroid{}; +diff --git a/node_modules/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.mm b/node_modules/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.mm +index 368c334..a1bb33e 100644 +--- a/node_modules/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.mm ++++ b/node_modules/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.mm +@@ -244,26 +244,51 @@ - (void)getRectWithAttributedString:(AttributedString)attributedString + + #pragma mark - Private + +-- (NSTextStorage *)_textStorageForNSAttributesString:(NSAttributedString *)attributedString +++- (NSTextStorage *)_textStorageForNSAttributesString:(NSAttributedString *)inputAttributedString + paragraphAttributes:(ParagraphAttributes)paragraphAttributes + size:(CGSize)size + { +- NSTextContainer *textContainer = [[NSTextContainer alloc] initWithSize:size]; ++ NSMutableAttributedString *attributedString = [ inputAttributedString mutableCopy]; ++ ++ /* ++ * The block below is responsible for setting the exact height of the view in lines ++ * Unfortunatelly, iOS doesn't export any easy way to do it. So we set maximumNumberOfLines ++ * prop and then add random lines at the front. However, they are only used for layout ++ * so they are not visible on the screen. This method is used for drawing only for Paragraph component ++ * but we set exact height in lines only on TextInput that doesn't use it. ++ */ ++ if (paragraphAttributes.numberOfLines) { ++ paragraphAttributes.maximumNumberOfLines = paragraphAttributes.numberOfLines; ++ NSMutableString *newLines = [NSMutableString stringWithCapacity: paragraphAttributes.numberOfLines]; ++ for (NSUInteger i = 0UL; i < paragraphAttributes.numberOfLines; ++i) { ++ // K is added on purpose. New line seems to be not enough for NTtextContainer ++ [newLines appendString:@"K\n"]; ++ } ++ NSDictionary * attributesOfFirstCharacter = [inputAttributedString attributesAtIndex:0 effectiveRange:NULL]; + +- textContainer.lineFragmentPadding = 0.0; // Note, the default value is 5. +- textContainer.lineBreakMode = paragraphAttributes.maximumNumberOfLines > 0 +- ? RCTNSLineBreakModeFromEllipsizeMode(paragraphAttributes.ellipsizeMode) +- : NSLineBreakByClipping; +- textContainer.maximumNumberOfLines = paragraphAttributes.maximumNumberOfLines; ++ [attributedString insertAttributedString:[[NSAttributedString alloc] initWithString:newLines attributes:attributesOfFirstCharacter] atIndex:0]; ++ } ++ ++ NSTextContainer *textContainer = [NSTextContainer new]; + + NSLayoutManager *layoutManager = [NSLayoutManager new]; + layoutManager.usesFontLeading = NO; + [layoutManager addTextContainer:textContainer]; + +- NSTextStorage *textStorage = [[NSTextStorage alloc] initWithAttributedString:attributedString]; ++ NSTextStorage *textStorage = [NSTextStorage new]; + + [textStorage addLayoutManager:layoutManager]; + ++ textContainer.lineFragmentPadding = 0.0; // Note, the default value is 5. ++ textContainer.lineBreakMode = paragraphAttributes.maximumNumberOfLines > 0 ++ ? RCTNSLineBreakModeFromEllipsizeMode(paragraphAttributes.ellipsizeMode) ++ : NSLineBreakByClipping; ++ textContainer.size = size; ++ textContainer.maximumNumberOfLines = paragraphAttributes.maximumNumberOfLines; ++ ++ [textStorage replaceCharactersInRange:(NSRange){0, textStorage.length} withAttributedString:attributedString]; ++ ++ + if (paragraphAttributes.adjustsFontSizeToFit) { + CGFloat minimumFontSize = !isnan(paragraphAttributes.minimumFontSize) ? paragraphAttributes.minimumFontSize : 4.0; + CGFloat maximumFontSize = !isnan(paragraphAttributes.maximumFontSize) ? paragraphAttributes.maximumFontSize : 96.0; diff --git a/patches/react-native+0.72.4+004+ModalKeyboardFlashing.patch b/patches/react-native+0.72.4+004+ModalKeyboardFlashing.patch new file mode 100644 index 000000000000..84a233894f94 --- /dev/null +++ b/patches/react-native+0.72.4+004+ModalKeyboardFlashing.patch @@ -0,0 +1,18 @@ +diff --git a/node_modules/react-native/React/Views/RCTModalHostViewManager.m b/node_modules/react-native/React/Views/RCTModalHostViewManager.m +index 4b9f9ad..b72984c 100644 +--- a/node_modules/react-native/React/Views/RCTModalHostViewManager.m ++++ b/node_modules/react-native/React/Views/RCTModalHostViewManager.m +@@ -79,6 +79,13 @@ RCT_EXPORT_MODULE() + if (self->_presentationBlock) { + self->_presentationBlock([modalHostView reactViewController], viewController, animated, completionBlock); + } else { ++ // In our App, If an input is blurred and a modal is opened, the rootView will become the firstResponder, which ++ // will cause system to retain a wrong keyboard state, and then the keyboard to flicker when the modal is closed. ++ // We first resign the rootView to avoid this problem. ++ UIWindow *window = RCTKeyWindow(); ++ if (window && window.rootViewController && [window.rootViewController.view isFirstResponder]) { ++ [window.rootViewController.view resignFirstResponder]; ++ } + [[modalHostView reactViewController] presentViewController:viewController + animated:animated + completion:completionBlock]; From 09e2c45787ab5a84abf87fb859b73a66845fafd5 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Tue, 28 Nov 2023 18:15:33 +0100 Subject: [PATCH 024/391] fix ios --- .../{index.native.js => index.android.js} | 3 +- src/components/Composer/index.ios.js | 140 ++++++++++++++++++ .../updateNumberOfLines/index.native.ts | 3 + .../updateNumberOfLines/types.ts | 2 +- 4 files changed, 146 insertions(+), 2 deletions(-) rename src/components/Composer/{index.native.js => index.android.js} (98%) create mode 100644 src/components/Composer/index.ios.js diff --git a/src/components/Composer/index.native.js b/src/components/Composer/index.android.js similarity index 98% rename from src/components/Composer/index.native.js rename to src/components/Composer/index.android.js index 5bec0f701ec5..3a8011b33f6a 100644 --- a/src/components/Composer/index.native.js +++ b/src/components/Composer/index.android.js @@ -98,12 +98,13 @@ function Composer({shouldClear, onClear, isDisabled, maxLines, forwardedRef, isC ComposerUtils.updateNumberOfLines({maxLines, isComposerFullSize, isDisabled, setIsFullComposerAvailable}, e)} rejectResponderTermination={false} - smartInsertDelete={false} + smartInsertDelete textAlignVertical="center" style={[...props.style, maxHeightStyle]} readOnly={isDisabled} diff --git a/src/components/Composer/index.ios.js b/src/components/Composer/index.ios.js new file mode 100644 index 000000000000..8204d38c3406 --- /dev/null +++ b/src/components/Composer/index.ios.js @@ -0,0 +1,140 @@ +import PropTypes from 'prop-types'; +import React, {useCallback, useEffect, useMemo, useRef} from 'react'; +import _ from 'underscore'; +import RNTextInput from '@components/RNTextInput'; +import * as ComposerUtils from '@libs/ComposerUtils'; +import styles from '@styles/styles'; +import themeColors from '@styles/themes/default'; + +const propTypes = { + /** If the input should clear, it actually gets intercepted instead of .clear() */ + shouldClear: PropTypes.bool, + + /** A ref to forward to the text input */ + forwardedRef: PropTypes.func, + + /** When the input has cleared whoever owns this input should know about it */ + onClear: PropTypes.func, + + /** Set focus to this component the first time it renders. + * Override this in case you need to set focus on one field out of many, or when you want to disable autoFocus */ + autoFocus: PropTypes.bool, + + /** Prevent edits and interactions like focus for this input. */ + isDisabled: PropTypes.bool, + + /** Selection Object */ + selection: PropTypes.shape({ + start: PropTypes.number, + end: PropTypes.number, + }), + + /** Whether the full composer can be opened */ + isFullComposerAvailable: PropTypes.bool, + + /** Maximum number of lines in the text input */ + maxLines: PropTypes.number, + + /** Allow the full composer to be opened */ + setIsFullComposerAvailable: PropTypes.func, + + /** Whether the composer is full size */ + isComposerFullSize: PropTypes.bool, + + /** General styles to apply to the text input */ + // eslint-disable-next-line react/forbid-prop-types + style: PropTypes.any, +}; + +const defaultProps = { + shouldClear: false, + onClear: () => {}, + autoFocus: false, + isDisabled: false, + forwardedRef: null, + selection: { + start: 0, + end: 0, + }, + maxLines: undefined, + isFullComposerAvailable: false, + setIsFullComposerAvailable: () => {}, + isComposerFullSize: false, + style: null, +}; + +function Composer({shouldClear, onClear, isDisabled, maxLines, forwardedRef, isComposerFullSize, setIsFullComposerAvailable, ...props}) { + const textInput = useRef(null); + + /** + * Set the TextInput Ref + * @param {Element} el + */ + const setTextInputRef = useCallback((el) => { + textInput.current = el; + if (!_.isFunction(forwardedRef) || textInput.current === null) { + return; + } + + // This callback prop is used by the parent component using the constructor to + // get a ref to the inner textInput element e.g. if we do + // this.textInput = el} /> this will not + // return a ref to the component, but rather the HTML element by default + forwardedRef(textInput.current); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + useEffect(() => { + if (!shouldClear) { + return; + } + textInput.current.clear(); + onClear(); + }, [shouldClear, onClear]); + + /** + * Set maximum number of lines + * @return {Number} + */ + const maxNumberOfLines = useMemo(() => { + if (isComposerFullSize) { + return; + } + return maxLines; + }, [isComposerFullSize, maxLines]); + + // On native layers we like to have the Text Input not focused so the + // user can read new chats without the keyboard in the way of the view. + // On Android the selection prop is required on the TextInput but this prop has issues on IOS + const propsToPass = _.omit(props, 'selection'); + return ( + ComposerUtils.updateNumberOfLines({maxLines, isComposerFullSize, isDisabled, setIsFullComposerAvailable}, e)} + rejectResponderTermination={false} + smartInsertDelete={false} + maxNumberOfLines={maxNumberOfLines} + style={[...props.style, styles.verticalAlignMiddle]} + /* eslint-disable-next-line react/jsx-props-no-spreading */ + {...propsToPass} + readOnly={isDisabled} + /> + ); +} + +Composer.propTypes = propTypes; +Composer.defaultProps = defaultProps; + +const ComposerWithRef = React.forwardRef((props, ref) => ( + +)); + +ComposerWithRef.displayName = 'ComposerWithRef'; + +export default ComposerWithRef; diff --git a/src/libs/ComposerUtils/updateNumberOfLines/index.native.ts b/src/libs/ComposerUtils/updateNumberOfLines/index.native.ts index df9292ecd690..2da1c3e485d6 100644 --- a/src/libs/ComposerUtils/updateNumberOfLines/index.native.ts +++ b/src/libs/ComposerUtils/updateNumberOfLines/index.native.ts @@ -11,11 +11,14 @@ const updateNumberOfLines: UpdateNumberOfLines = (props, event) => { const lineHeight = styles.textInputCompose.lineHeight ?? 0; const paddingTopAndBottom = styles.textInputComposeSpacing.paddingVertical * 2; const inputHeight = event?.nativeEvent?.contentSize?.height ?? null; + if (!inputHeight) { return; } const numberOfLines = getNumberOfLines(lineHeight, paddingTopAndBottom, inputHeight); updateIsFullComposerAvailable(props, numberOfLines); + + return numberOfLines; }; export default updateNumberOfLines; diff --git a/src/libs/ComposerUtils/updateNumberOfLines/types.ts b/src/libs/ComposerUtils/updateNumberOfLines/types.ts index b0f9ba48ddc2..828c67624bd5 100644 --- a/src/libs/ComposerUtils/updateNumberOfLines/types.ts +++ b/src/libs/ComposerUtils/updateNumberOfLines/types.ts @@ -1,6 +1,6 @@ import {NativeSyntheticEvent, TextInputContentSizeChangeEventData} from 'react-native'; import ComposerProps from '@libs/ComposerUtils/types'; -type UpdateNumberOfLines = (props: ComposerProps, event: NativeSyntheticEvent) => void; +type UpdateNumberOfLines = (props: ComposerProps, event: NativeSyntheticEvent) => number | void; export default UpdateNumberOfLines; From 47318dce468793e435f530e8578c8367c3a9f3ef Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Tue, 28 Nov 2023 18:15:39 +0100 Subject: [PATCH 025/391] update the comment --- src/styles/StyleUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/styles/StyleUtils.ts b/src/styles/StyleUtils.ts index 279704cd278c..a0fd15dad2fd 100644 --- a/src/styles/StyleUtils.ts +++ b/src/styles/StyleUtils.ts @@ -1391,7 +1391,7 @@ function getDotIndicatorTextStyles(isErrorText = true): TextStyle { } /** - * Returns container styles for showing the icons in MultipleAvatars/SubscriptAvatar + * Get the style for setting the maximum height of the composer component */ function getComposerMaxHeightStyle(maxLines: number, isComposerFullSize: boolean): ViewStyle | undefined { const composerLineHeight = styles.textInputCompose.lineHeight ?? 0; From 786a859897ec5f1a0130be01901eba44f5592893 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Tue, 28 Nov 2023 22:28:35 +0100 Subject: [PATCH 026/391] simplify ios implementation --- src/components/Composer/index.ios.js | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/src/components/Composer/index.ios.js b/src/components/Composer/index.ios.js index 8204d38c3406..51ff66f5747d 100644 --- a/src/components/Composer/index.ios.js +++ b/src/components/Composer/index.ios.js @@ -1,5 +1,5 @@ import PropTypes from 'prop-types'; -import React, {useCallback, useEffect, useMemo, useRef} from 'react'; +import React, {useCallback, useEffect, useRef} from 'react'; import _ from 'underscore'; import RNTextInput from '@components/RNTextInput'; import * as ComposerUtils from '@libs/ComposerUtils'; @@ -92,17 +92,6 @@ function Composer({shouldClear, onClear, isDisabled, maxLines, forwardedRef, isC onClear(); }, [shouldClear, onClear]); - /** - * Set maximum number of lines - * @return {Number} - */ - const maxNumberOfLines = useMemo(() => { - if (isComposerFullSize) { - return; - } - return maxLines; - }, [isComposerFullSize, maxLines]); - // On native layers we like to have the Text Input not focused so the // user can read new chats without the keyboard in the way of the view. // On Android the selection prop is required on the TextInput but this prop has issues on IOS @@ -115,7 +104,7 @@ function Composer({shouldClear, onClear, isDisabled, maxLines, forwardedRef, isC onContentSizeChange={(e) => ComposerUtils.updateNumberOfLines({maxLines, isComposerFullSize, isDisabled, setIsFullComposerAvailable}, e)} rejectResponderTermination={false} smartInsertDelete={false} - maxNumberOfLines={maxNumberOfLines} + maxNumberOfLines={isComposerFullSize ? undefined : maxLines} style={[...props.style, styles.verticalAlignMiddle]} /* eslint-disable-next-line react/jsx-props-no-spreading */ {...propsToPass} From 3938bb6355fdb4d94d467cd493bb9483cbb8ceb9 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Tue, 28 Nov 2023 22:40:36 +0100 Subject: [PATCH 027/391] simplify composer --- ...eact-native+0.72.4+002+NumberOfLines.patch | 632 ------------------ ...ive+0.72.4+004+ModalKeyboardFlashing.patch | 18 - src/components/Composer/index.ios.js | 129 ---- .../{index.android.js => index.native.js} | 0 4 files changed, 779 deletions(-) delete mode 100644 patches/react-native+0.72.4+002+NumberOfLines.patch delete mode 100644 patches/react-native+0.72.4+004+ModalKeyboardFlashing.patch delete mode 100644 src/components/Composer/index.ios.js rename src/components/Composer/{index.android.js => index.native.js} (100%) diff --git a/patches/react-native+0.72.4+002+NumberOfLines.patch b/patches/react-native+0.72.4+002+NumberOfLines.patch deleted file mode 100644 index 16fec4bc8363..000000000000 --- a/patches/react-native+0.72.4+002+NumberOfLines.patch +++ /dev/null @@ -1,632 +0,0 @@ -diff --git a/node_modules/react-native/Libraries/Components/TextInput/RCTTextInputViewConfig.js b/node_modules/react-native/Libraries/Components/TextInput/RCTTextInputViewConfig.js -index 6f69329..d531bee 100644 ---- a/node_modules/react-native/Libraries/Components/TextInput/RCTTextInputViewConfig.js -+++ b/node_modules/react-native/Libraries/Components/TextInput/RCTTextInputViewConfig.js -@@ -144,6 +144,8 @@ const RCTTextInputViewConfig = { - placeholder: true, - autoCorrect: true, - multiline: true, -+ numberOfLines: true, -+ maximumNumberOfLines: true, - textContentType: true, - maxLength: true, - autoCapitalize: true, -diff --git a/node_modules/react-native/Libraries/Components/TextInput/TextInput.d.ts b/node_modules/react-native/Libraries/Components/TextInput/TextInput.d.ts -index 8badb2a..b19f197 100644 ---- a/node_modules/react-native/Libraries/Components/TextInput/TextInput.d.ts -+++ b/node_modules/react-native/Libraries/Components/TextInput/TextInput.d.ts -@@ -347,12 +347,6 @@ export interface TextInputAndroidProps { - */ - inlineImagePadding?: number | undefined; - -- /** -- * Sets the number of lines for a TextInput. -- * Use it with multiline set to true to be able to fill the lines. -- */ -- numberOfLines?: number | undefined; -- - /** - * Sets the return key to the label. Use it instead of `returnKeyType`. - * @platform android -@@ -663,11 +657,30 @@ export interface TextInputProps - */ - maxLength?: number | undefined; - -+ /** -+ * Sets the maximum number of lines for a TextInput. -+ * Use it with multiline set to true to be able to fill the lines. -+ */ -+ maxNumberOfLines?: number | undefined; -+ - /** - * If true, the text input can be multiple lines. The default value is false. - */ - multiline?: boolean | undefined; - -+ /** -+ * Sets the number of lines for a TextInput. -+ * Use it with multiline set to true to be able to fill the lines. -+ */ -+ numberOfLines?: number | undefined; -+ -+ /** -+ * Sets the number of rows for a TextInput. -+ * Use it with multiline set to true to be able to fill the lines. -+ */ -+ rows?: number | undefined; -+ -+ - /** - * Callback that is called when the text input is blurred - */ -diff --git a/node_modules/react-native/Libraries/Components/TextInput/TextInput.flow.js b/node_modules/react-native/Libraries/Components/TextInput/TextInput.flow.js -index 7ed4579..b1d994e 100644 ---- a/node_modules/react-native/Libraries/Components/TextInput/TextInput.flow.js -+++ b/node_modules/react-native/Libraries/Components/TextInput/TextInput.flow.js -@@ -343,26 +343,12 @@ type AndroidProps = $ReadOnly<{| - */ - inlineImagePadding?: ?number, - -- /** -- * Sets the number of lines for a `TextInput`. Use it with multiline set to -- * `true` to be able to fill the lines. -- * @platform android -- */ -- numberOfLines?: ?number, -- - /** - * Sets the return key to the label. Use it instead of `returnKeyType`. - * @platform android - */ - returnKeyLabel?: ?string, - -- /** -- * Sets the number of rows for a `TextInput`. Use it with multiline set to -- * `true` to be able to fill the lines. -- * @platform android -- */ -- rows?: ?number, -- - /** - * When `false`, it will prevent the soft keyboard from showing when the field is focused. - * Defaults to `true`. -@@ -632,6 +618,12 @@ export type Props = $ReadOnly<{| - */ - keyboardType?: ?KeyboardType, - -+ /** -+ * Sets the maximum number of lines for a `TextInput`. Use it with multiline set to -+ * `true` to be able to fill the lines. -+ */ -+ maxNumberOfLines?: ?number, -+ - /** - * Specifies largest possible scale a font can reach when `allowFontScaling` is enabled. - * Possible values: -@@ -653,6 +645,12 @@ export type Props = $ReadOnly<{| - */ - multiline?: ?boolean, - -+ /** -+ * Sets the number of lines for a `TextInput`. Use it with multiline set to -+ * `true` to be able to fill the lines. -+ */ -+ numberOfLines?: ?number, -+ - /** - * Callback that is called when the text input is blurred. - */ -@@ -814,6 +812,12 @@ export type Props = $ReadOnly<{| - */ - returnKeyType?: ?ReturnKeyType, - -+ /** -+ * Sets the number of rows for a `TextInput`. Use it with multiline set to -+ * `true` to be able to fill the lines. -+ */ -+ rows?: ?number, -+ - /** - * If `true`, the text input obscures the text entered so that sensitive text - * like passwords stay secure. The default value is `false`. Does not work with 'multiline={true}'. -diff --git a/node_modules/react-native/Libraries/Components/TextInput/TextInput.js b/node_modules/react-native/Libraries/Components/TextInput/TextInput.js -index 2127191..542fc06 100644 ---- a/node_modules/react-native/Libraries/Components/TextInput/TextInput.js -+++ b/node_modules/react-native/Libraries/Components/TextInput/TextInput.js -@@ -390,7 +390,6 @@ type AndroidProps = $ReadOnly<{| - /** - * Sets the number of lines for a `TextInput`. Use it with multiline set to - * `true` to be able to fill the lines. -- * @platform android - */ - numberOfLines?: ?number, - -@@ -403,10 +402,14 @@ type AndroidProps = $ReadOnly<{| - /** - * Sets the number of rows for a `TextInput`. Use it with multiline set to - * `true` to be able to fill the lines. -- * @platform android - */ - rows?: ?number, - -+ /** -+ * Sets the maximum number of lines the TextInput can have. -+ */ -+ maxNumberOfLines?: ?number, -+ - /** - * When `false`, it will prevent the soft keyboard from showing when the field is focused. - * Defaults to `true`. -@@ -1069,6 +1072,9 @@ function InternalTextInput(props: Props): React.Node { - accessibilityState, - id, - tabIndex, -+ rows, -+ numberOfLines, -+ maxNumberOfLines, - selection: propsSelection, - ...otherProps - } = props; -@@ -1427,6 +1433,8 @@ function InternalTextInput(props: Props): React.Node { - focusable={tabIndex !== undefined ? !tabIndex : focusable} - mostRecentEventCount={mostRecentEventCount} - nativeID={id ?? props.nativeID} -+ numberOfLines={props.rows ?? props.numberOfLines} -+ maximumNumberOfLines={maxNumberOfLines} - onBlur={_onBlur} - onKeyPressSync={props.unstable_onKeyPressSync} - onChange={_onChange} -@@ -1482,6 +1490,7 @@ function InternalTextInput(props: Props): React.Node { - mostRecentEventCount={mostRecentEventCount} - nativeID={id ?? props.nativeID} - numberOfLines={props.rows ?? props.numberOfLines} -+ maximumNumberOfLines={maxNumberOfLines} - onBlur={_onBlur} - onChange={_onChange} - onFocus={_onFocus} -diff --git a/node_modules/react-native/Libraries/Text/Text.js b/node_modules/react-native/Libraries/Text/Text.js -index df548af..e02f5da 100644 ---- a/node_modules/react-native/Libraries/Text/Text.js -+++ b/node_modules/react-native/Libraries/Text/Text.js -@@ -18,7 +18,11 @@ import processColor from '../StyleSheet/processColor'; - import {getAccessibilityRoleFromRole} from '../Utilities/AcessibilityMapping'; - import Platform from '../Utilities/Platform'; - import TextAncestor from './TextAncestor'; --import {NativeText, NativeVirtualText} from './TextNativeComponent'; -+import { -+ CONTAINS_MAX_NUMBER_OF_LINES_RENAME, -+ NativeText, -+ NativeVirtualText, -+} from './TextNativeComponent'; - import * as React from 'react'; - import {useContext, useMemo, useState} from 'react'; - -@@ -59,6 +63,7 @@ const Text: React.AbstractComponent< - pressRetentionOffset, - role, - suppressHighlighting, -+ numberOfLines, - ...restProps - } = props; - -@@ -192,14 +197,33 @@ const Text: React.AbstractComponent< - } - } - -- let numberOfLines = restProps.numberOfLines; -+ let numberOfLinesValue = numberOfLines; - if (numberOfLines != null && !(numberOfLines >= 0)) { - console.error( - `'numberOfLines' in must be a non-negative number, received: ${numberOfLines}. The value will be set to 0.`, - ); -- numberOfLines = 0; -+ numberOfLinesValue = 0; - } - -+ const numberOfLinesProps = useMemo((): { -+ maximumNumberOfLines?: ?number, -+ numberOfLines?: ?number, -+ } => { -+ // FIXME: Current logic is breaking all Text components. -+ // if (CONTAINS_MAX_NUMBER_OF_LINES_RENAME) { -+ // return { -+ // maximumNumberOfLines: numberOfLinesValue, -+ // }; -+ // } else { -+ // return { -+ // numberOfLines: numberOfLinesValue, -+ // }; -+ // } -+ return { -+ maximumNumberOfLines: numberOfLinesValue, -+ }; -+ }, [numberOfLinesValue]); -+ - const hasTextAncestor = useContext(TextAncestor); - - const _accessible = Platform.select({ -@@ -241,7 +265,6 @@ const Text: React.AbstractComponent< - isHighlighted={isHighlighted} - isPressable={isPressable} - nativeID={id ?? nativeID} -- numberOfLines={numberOfLines} - ref={forwardedRef} - selectable={_selectable} - selectionColor={selectionColor} -@@ -252,6 +275,7 @@ const Text: React.AbstractComponent< - - #import -+#import -+#import - - @implementation RCTMultilineTextInputViewManager - -@@ -17,8 +19,21 @@ - (UIView *)view - return [[RCTMultilineTextInputView alloc] initWithBridge:self.bridge]; - } - -+- (RCTShadowView *)shadowView -+{ -+ RCTBaseTextInputShadowView *shadowView = (RCTBaseTextInputShadowView *)[super shadowView]; -+ -+ shadowView.maximumNumberOfLines = 0; -+ shadowView.exactNumberOfLines = 0; -+ -+ return shadowView; -+} -+ - #pragma mark - Multiline (aka TextView) specific properties - - RCT_REMAP_VIEW_PROPERTY(dataDetectorTypes, backedTextInputView.dataDetectorTypes, UIDataDetectorTypes) - -+RCT_EXPORT_SHADOW_PROPERTY(maximumNumberOfLines, NSInteger) -+RCT_REMAP_SHADOW_PROPERTY(numberOfLines, exactNumberOfLines, NSInteger) -+ - @end -diff --git a/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputShadowView.h b/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputShadowView.h -index 8f4cf7e..6238ebc 100644 ---- a/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputShadowView.h -+++ b/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputShadowView.h -@@ -16,6 +16,7 @@ NS_ASSUME_NONNULL_BEGIN - @property (nonatomic, copy, nullable) NSString *text; - @property (nonatomic, copy, nullable) NSString *placeholder; - @property (nonatomic, assign) NSInteger maximumNumberOfLines; -+@property (nonatomic, assign) NSInteger exactNumberOfLines; - @property (nonatomic, copy, nullable) RCTDirectEventBlock onContentSizeChange; - - - (void)uiManagerWillPerformMounting; -diff --git a/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputShadowView.m b/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputShadowView.m -index 04d2446..9d77743 100644 ---- a/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputShadowView.m -+++ b/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputShadowView.m -@@ -218,7 +218,22 @@ - (NSAttributedString *)measurableAttributedText - - - (CGSize)sizeThatFitsMinimumSize:(CGSize)minimumSize maximumSize:(CGSize)maximumSize - { -- NSAttributedString *attributedText = [self measurableAttributedText]; -+ NSMutableAttributedString *attributedText = [[self measurableAttributedText] mutableCopy]; -+ -+ /* -+ * The block below is responsible for setting the exact height of the view in lines -+ * Unfortunatelly, iOS doesn't export any easy way to do it. So we set maximumNumberOfLines -+ * prop and then add random lines at the front. However, they are only used for layout -+ * so they are not visible on the screen. -+ */ -+ if (self.exactNumberOfLines) { -+ NSMutableString *newLines = [NSMutableString stringWithCapacity:self.exactNumberOfLines]; -+ for (NSUInteger i = 0UL; i < self.exactNumberOfLines; ++i) { -+ [newLines appendString:@"\n"]; -+ } -+ [attributedText insertAttributedString:[[NSAttributedString alloc] initWithString:newLines attributes:self.textAttributes.effectiveTextAttributes] atIndex:0]; -+ _maximumNumberOfLines = self.exactNumberOfLines; -+ } - - if (!_textStorage) { - _textContainer = [NSTextContainer new]; -diff --git a/node_modules/react-native/Libraries/Text/TextInput/Singleline/RCTSinglelineTextInputViewManager.m b/node_modules/react-native/Libraries/Text/TextInput/Singleline/RCTSinglelineTextInputViewManager.m -index 413ac42..56d039c 100644 ---- a/node_modules/react-native/Libraries/Text/TextInput/Singleline/RCTSinglelineTextInputViewManager.m -+++ b/node_modules/react-native/Libraries/Text/TextInput/Singleline/RCTSinglelineTextInputViewManager.m -@@ -19,6 +19,7 @@ - (RCTShadowView *)shadowView - RCTBaseTextInputShadowView *shadowView = (RCTBaseTextInputShadowView *)[super shadowView]; - - shadowView.maximumNumberOfLines = 1; -+ shadowView.exactNumberOfLines = 0; - - return shadowView; - } -diff --git a/node_modules/react-native/Libraries/Text/TextNativeComponent.js b/node_modules/react-native/Libraries/Text/TextNativeComponent.js -index 0d59904..3216e43 100644 ---- a/node_modules/react-native/Libraries/Text/TextNativeComponent.js -+++ b/node_modules/react-native/Libraries/Text/TextNativeComponent.js -@@ -9,6 +9,7 @@ - */ - - import {createViewConfig} from '../NativeComponent/ViewConfig'; -+import getNativeComponentAttributes from '../ReactNative/getNativeComponentAttributes'; - import UIManager from '../ReactNative/UIManager'; - import createReactNativeComponentClass from '../Renderer/shims/createReactNativeComponentClass'; - import {type HostComponent} from '../Renderer/shims/ReactNativeTypes'; -@@ -18,6 +19,7 @@ import {type TextProps} from './TextProps'; - - type NativeTextProps = $ReadOnly<{ - ...TextProps, -+ maximumNumberOfLines?: ?number, - isHighlighted?: ?boolean, - selectionColor?: ?ProcessedColorValue, - onClick?: ?(event: PressEvent) => mixed, -@@ -31,7 +33,7 @@ const textViewConfig = { - validAttributes: { - isHighlighted: true, - isPressable: true, -- numberOfLines: true, -+ maximumNumberOfLines: true, - ellipsizeMode: true, - allowFontScaling: true, - dynamicTypeRamp: true, -@@ -73,6 +75,12 @@ export const NativeText: HostComponent = - createViewConfig(textViewConfig), - ): any); - -+const jestIsDefined = typeof jest !== 'undefined'; -+export const CONTAINS_MAX_NUMBER_OF_LINES_RENAME: boolean = jestIsDefined -+ ? true -+ : getNativeComponentAttributes('RCTText')?.NativeProps -+ ?.maximumNumberOfLines === 'number'; -+ - export const NativeVirtualText: HostComponent = - !global.RN$Bridgeless && !UIManager.hasViewManagerConfig('RCTVirtualText') - ? NativeText -diff --git a/node_modules/react-native/ReactCommon/react/renderer/attributedstring/ParagraphAttributes.cpp b/node_modules/react-native/ReactCommon/react/renderer/attributedstring/ParagraphAttributes.cpp -index 2994aca..fff0d5e 100644 ---- a/node_modules/react-native/ReactCommon/react/renderer/attributedstring/ParagraphAttributes.cpp -+++ b/node_modules/react-native/ReactCommon/react/renderer/attributedstring/ParagraphAttributes.cpp -@@ -16,6 +16,7 @@ namespace facebook::react { - - bool ParagraphAttributes::operator==(const ParagraphAttributes &rhs) const { - return std::tie( -+ numberOfLines, - maximumNumberOfLines, - ellipsizeMode, - textBreakStrategy, -@@ -23,6 +24,7 @@ bool ParagraphAttributes::operator==(const ParagraphAttributes &rhs) const { - includeFontPadding, - android_hyphenationFrequency) == - std::tie( -+ rhs.numberOfLines, - rhs.maximumNumberOfLines, - rhs.ellipsizeMode, - rhs.textBreakStrategy, -@@ -42,6 +44,7 @@ bool ParagraphAttributes::operator!=(const ParagraphAttributes &rhs) const { - #if RN_DEBUG_STRING_CONVERTIBLE - SharedDebugStringConvertibleList ParagraphAttributes::getDebugProps() const { - return { -+ debugStringConvertibleItem("numberOfLines", numberOfLines), - debugStringConvertibleItem("maximumNumberOfLines", maximumNumberOfLines), - debugStringConvertibleItem("ellipsizeMode", ellipsizeMode), - debugStringConvertibleItem("textBreakStrategy", textBreakStrategy), -diff --git a/node_modules/react-native/ReactCommon/react/renderer/attributedstring/ParagraphAttributes.h b/node_modules/react-native/ReactCommon/react/renderer/attributedstring/ParagraphAttributes.h -index f5f87c6..b7d1e90 100644 ---- a/node_modules/react-native/ReactCommon/react/renderer/attributedstring/ParagraphAttributes.h -+++ b/node_modules/react-native/ReactCommon/react/renderer/attributedstring/ParagraphAttributes.h -@@ -30,6 +30,11 @@ class ParagraphAttributes : public DebugStringConvertible { - public: - #pragma mark - Fields - -+ /* -+ * Number of lines which paragraph takes. -+ */ -+ int numberOfLines{}; -+ - /* - * Maximum number of lines which paragraph can take. - * Zero value represents "no limit". -@@ -92,6 +97,7 @@ struct hash { - const facebook::react::ParagraphAttributes &attributes) const { - return folly::hash::hash_combine( - 0, -+ attributes.numberOfLines, - attributes.maximumNumberOfLines, - attributes.ellipsizeMode, - attributes.textBreakStrategy, -diff --git a/node_modules/react-native/ReactCommon/react/renderer/attributedstring/conversions.h b/node_modules/react-native/ReactCommon/react/renderer/attributedstring/conversions.h -index 8687b89..eab75f4 100644 ---- a/node_modules/react-native/ReactCommon/react/renderer/attributedstring/conversions.h -+++ b/node_modules/react-native/ReactCommon/react/renderer/attributedstring/conversions.h -@@ -835,10 +835,16 @@ inline ParagraphAttributes convertRawProp( - ParagraphAttributes const &defaultParagraphAttributes) { - auto paragraphAttributes = ParagraphAttributes{}; - -- paragraphAttributes.maximumNumberOfLines = convertRawProp( -+ paragraphAttributes.numberOfLines = convertRawProp( - context, - rawProps, - "numberOfLines", -+ sourceParagraphAttributes.numberOfLines, -+ defaultParagraphAttributes.numberOfLines); -+ paragraphAttributes.maximumNumberOfLines = convertRawProp( -+ context, -+ rawProps, -+ "maximumNumberOfLines", - sourceParagraphAttributes.maximumNumberOfLines, - defaultParagraphAttributes.maximumNumberOfLines); - paragraphAttributes.ellipsizeMode = convertRawProp( -@@ -913,6 +919,7 @@ inline std::string toString(AttributedString::Range const &range) { - inline folly::dynamic toDynamic( - const ParagraphAttributes ¶graphAttributes) { - auto values = folly::dynamic::object(); -+ values("numberOfLines", paragraphAttributes.numberOfLines); - values("maximumNumberOfLines", paragraphAttributes.maximumNumberOfLines); - values("ellipsizeMode", toString(paragraphAttributes.ellipsizeMode)); - values("textBreakStrategy", toString(paragraphAttributes.textBreakStrategy)); -@@ -1118,6 +1125,7 @@ constexpr static MapBuffer::Key PA_KEY_TEXT_BREAK_STRATEGY = 2; - constexpr static MapBuffer::Key PA_KEY_ADJUST_FONT_SIZE_TO_FIT = 3; - constexpr static MapBuffer::Key PA_KEY_INCLUDE_FONT_PADDING = 4; - constexpr static MapBuffer::Key PA_KEY_HYPHENATION_FREQUENCY = 5; -+constexpr static MapBuffer::Key PA_KEY_NUMBER_OF_LINES = 6; - - inline MapBuffer toMapBuffer(const ParagraphAttributes ¶graphAttributes) { - auto builder = MapBufferBuilder(); -@@ -1135,6 +1143,8 @@ inline MapBuffer toMapBuffer(const ParagraphAttributes ¶graphAttributes) { - builder.putString( - PA_KEY_HYPHENATION_FREQUENCY, - toString(paragraphAttributes.android_hyphenationFrequency)); -+ builder.putInt( -+ PA_KEY_NUMBER_OF_LINES, paragraphAttributes.numberOfLines); - - return builder.build(); - } -diff --git a/node_modules/react-native/ReactCommon/react/renderer/components/textinput/androidtextinput/react/renderer/components/androidtextinput/AndroidTextInputProps.cpp b/node_modules/react-native/ReactCommon/react/renderer/components/textinput/androidtextinput/react/renderer/components/androidtextinput/AndroidTextInputProps.cpp -index 9953e22..98eb3da 100644 ---- a/node_modules/react-native/ReactCommon/react/renderer/components/textinput/androidtextinput/react/renderer/components/androidtextinput/AndroidTextInputProps.cpp -+++ b/node_modules/react-native/ReactCommon/react/renderer/components/textinput/androidtextinput/react/renderer/components/androidtextinput/AndroidTextInputProps.cpp -@@ -56,6 +56,10 @@ AndroidTextInputProps::AndroidTextInputProps( - "numberOfLines", - sourceProps.numberOfLines, - {0})), -+ maximumNumberOfLines(CoreFeatures::enablePropIteratorSetter? sourceProps.maximumNumberOfLines : convertRawProp(context, rawProps, -+ "maximumNumberOfLines", -+ sourceProps.maximumNumberOfLines, -+ {0})), - disableFullscreenUI(CoreFeatures::enablePropIteratorSetter? sourceProps.disableFullscreenUI : convertRawProp(context, rawProps, - "disableFullscreenUI", - sourceProps.disableFullscreenUI, -@@ -281,6 +285,12 @@ void AndroidTextInputProps::setProp( - value, - paragraphAttributes, - maximumNumberOfLines, -+ "maximumNumberOfLines"); -+ REBUILD_FIELD_SWITCH_CASE( -+ paDefaults, -+ value, -+ paragraphAttributes, -+ numberOfLines, - "numberOfLines"); - REBUILD_FIELD_SWITCH_CASE( - paDefaults, value, paragraphAttributes, ellipsizeMode, "ellipsizeMode"); -@@ -323,6 +333,7 @@ void AndroidTextInputProps::setProp( - } - - switch (hash) { -+ RAW_SET_PROP_SWITCH_CASE_BASIC(maximumNumberOfLines); - RAW_SET_PROP_SWITCH_CASE_BASIC(autoComplete); - RAW_SET_PROP_SWITCH_CASE_BASIC(returnKeyLabel); - RAW_SET_PROP_SWITCH_CASE_BASIC(numberOfLines); -@@ -422,6 +433,7 @@ void AndroidTextInputProps::setProp( - // TODO T53300085: support this in codegen; this was hand-written - folly::dynamic AndroidTextInputProps::getDynamic() const { - folly::dynamic props = folly::dynamic::object(); -+ props["maximumNumberOfLines"] = maximumNumberOfLines; - props["autoComplete"] = autoComplete; - props["returnKeyLabel"] = returnKeyLabel; - props["numberOfLines"] = numberOfLines; -diff --git a/node_modules/react-native/ReactCommon/react/renderer/components/textinput/androidtextinput/react/renderer/components/androidtextinput/AndroidTextInputProps.h b/node_modules/react-native/ReactCommon/react/renderer/components/textinput/androidtextinput/react/renderer/components/androidtextinput/AndroidTextInputProps.h -index ba39ebb..ead28e3 100644 ---- a/node_modules/react-native/ReactCommon/react/renderer/components/textinput/androidtextinput/react/renderer/components/androidtextinput/AndroidTextInputProps.h -+++ b/node_modules/react-native/ReactCommon/react/renderer/components/textinput/androidtextinput/react/renderer/components/androidtextinput/AndroidTextInputProps.h -@@ -84,6 +84,7 @@ class AndroidTextInputProps final : public ViewProps, public BaseTextProps { - std::string autoComplete{}; - std::string returnKeyLabel{}; - int numberOfLines{0}; -+ int maximumNumberOfLines{0}; - bool disableFullscreenUI{false}; - std::string textBreakStrategy{}; - SharedColor underlineColorAndroid{}; -diff --git a/node_modules/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.mm b/node_modules/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.mm -index 368c334..a1bb33e 100644 ---- a/node_modules/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.mm -+++ b/node_modules/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.mm -@@ -244,26 +244,51 @@ - (void)getRectWithAttributedString:(AttributedString)attributedString - - #pragma mark - Private - --- (NSTextStorage *)_textStorageForNSAttributesString:(NSAttributedString *)attributedString -++- (NSTextStorage *)_textStorageForNSAttributesString:(NSAttributedString *)inputAttributedString - paragraphAttributes:(ParagraphAttributes)paragraphAttributes - size:(CGSize)size - { -- NSTextContainer *textContainer = [[NSTextContainer alloc] initWithSize:size]; -+ NSMutableAttributedString *attributedString = [ inputAttributedString mutableCopy]; -+ -+ /* -+ * The block below is responsible for setting the exact height of the view in lines -+ * Unfortunatelly, iOS doesn't export any easy way to do it. So we set maximumNumberOfLines -+ * prop and then add random lines at the front. However, they are only used for layout -+ * so they are not visible on the screen. This method is used for drawing only for Paragraph component -+ * but we set exact height in lines only on TextInput that doesn't use it. -+ */ -+ if (paragraphAttributes.numberOfLines) { -+ paragraphAttributes.maximumNumberOfLines = paragraphAttributes.numberOfLines; -+ NSMutableString *newLines = [NSMutableString stringWithCapacity: paragraphAttributes.numberOfLines]; -+ for (NSUInteger i = 0UL; i < paragraphAttributes.numberOfLines; ++i) { -+ // K is added on purpose. New line seems to be not enough for NTtextContainer -+ [newLines appendString:@"K\n"]; -+ } -+ NSDictionary * attributesOfFirstCharacter = [inputAttributedString attributesAtIndex:0 effectiveRange:NULL]; - -- textContainer.lineFragmentPadding = 0.0; // Note, the default value is 5. -- textContainer.lineBreakMode = paragraphAttributes.maximumNumberOfLines > 0 -- ? RCTNSLineBreakModeFromEllipsizeMode(paragraphAttributes.ellipsizeMode) -- : NSLineBreakByClipping; -- textContainer.maximumNumberOfLines = paragraphAttributes.maximumNumberOfLines; -+ [attributedString insertAttributedString:[[NSAttributedString alloc] initWithString:newLines attributes:attributesOfFirstCharacter] atIndex:0]; -+ } -+ -+ NSTextContainer *textContainer = [NSTextContainer new]; - - NSLayoutManager *layoutManager = [NSLayoutManager new]; - layoutManager.usesFontLeading = NO; - [layoutManager addTextContainer:textContainer]; - -- NSTextStorage *textStorage = [[NSTextStorage alloc] initWithAttributedString:attributedString]; -+ NSTextStorage *textStorage = [NSTextStorage new]; - - [textStorage addLayoutManager:layoutManager]; - -+ textContainer.lineFragmentPadding = 0.0; // Note, the default value is 5. -+ textContainer.lineBreakMode = paragraphAttributes.maximumNumberOfLines > 0 -+ ? RCTNSLineBreakModeFromEllipsizeMode(paragraphAttributes.ellipsizeMode) -+ : NSLineBreakByClipping; -+ textContainer.size = size; -+ textContainer.maximumNumberOfLines = paragraphAttributes.maximumNumberOfLines; -+ -+ [textStorage replaceCharactersInRange:(NSRange){0, textStorage.length} withAttributedString:attributedString]; -+ -+ - if (paragraphAttributes.adjustsFontSizeToFit) { - CGFloat minimumFontSize = !isnan(paragraphAttributes.minimumFontSize) ? paragraphAttributes.minimumFontSize : 4.0; - CGFloat maximumFontSize = !isnan(paragraphAttributes.maximumFontSize) ? paragraphAttributes.maximumFontSize : 96.0; diff --git a/patches/react-native+0.72.4+004+ModalKeyboardFlashing.patch b/patches/react-native+0.72.4+004+ModalKeyboardFlashing.patch deleted file mode 100644 index 84a233894f94..000000000000 --- a/patches/react-native+0.72.4+004+ModalKeyboardFlashing.patch +++ /dev/null @@ -1,18 +0,0 @@ -diff --git a/node_modules/react-native/React/Views/RCTModalHostViewManager.m b/node_modules/react-native/React/Views/RCTModalHostViewManager.m -index 4b9f9ad..b72984c 100644 ---- a/node_modules/react-native/React/Views/RCTModalHostViewManager.m -+++ b/node_modules/react-native/React/Views/RCTModalHostViewManager.m -@@ -79,6 +79,13 @@ RCT_EXPORT_MODULE() - if (self->_presentationBlock) { - self->_presentationBlock([modalHostView reactViewController], viewController, animated, completionBlock); - } else { -+ // In our App, If an input is blurred and a modal is opened, the rootView will become the firstResponder, which -+ // will cause system to retain a wrong keyboard state, and then the keyboard to flicker when the modal is closed. -+ // We first resign the rootView to avoid this problem. -+ UIWindow *window = RCTKeyWindow(); -+ if (window && window.rootViewController && [window.rootViewController.view isFirstResponder]) { -+ [window.rootViewController.view resignFirstResponder]; -+ } - [[modalHostView reactViewController] presentViewController:viewController - animated:animated - completion:completionBlock]; diff --git a/src/components/Composer/index.ios.js b/src/components/Composer/index.ios.js deleted file mode 100644 index 51ff66f5747d..000000000000 --- a/src/components/Composer/index.ios.js +++ /dev/null @@ -1,129 +0,0 @@ -import PropTypes from 'prop-types'; -import React, {useCallback, useEffect, useRef} from 'react'; -import _ from 'underscore'; -import RNTextInput from '@components/RNTextInput'; -import * as ComposerUtils from '@libs/ComposerUtils'; -import styles from '@styles/styles'; -import themeColors from '@styles/themes/default'; - -const propTypes = { - /** If the input should clear, it actually gets intercepted instead of .clear() */ - shouldClear: PropTypes.bool, - - /** A ref to forward to the text input */ - forwardedRef: PropTypes.func, - - /** When the input has cleared whoever owns this input should know about it */ - onClear: PropTypes.func, - - /** Set focus to this component the first time it renders. - * Override this in case you need to set focus on one field out of many, or when you want to disable autoFocus */ - autoFocus: PropTypes.bool, - - /** Prevent edits and interactions like focus for this input. */ - isDisabled: PropTypes.bool, - - /** Selection Object */ - selection: PropTypes.shape({ - start: PropTypes.number, - end: PropTypes.number, - }), - - /** Whether the full composer can be opened */ - isFullComposerAvailable: PropTypes.bool, - - /** Maximum number of lines in the text input */ - maxLines: PropTypes.number, - - /** Allow the full composer to be opened */ - setIsFullComposerAvailable: PropTypes.func, - - /** Whether the composer is full size */ - isComposerFullSize: PropTypes.bool, - - /** General styles to apply to the text input */ - // eslint-disable-next-line react/forbid-prop-types - style: PropTypes.any, -}; - -const defaultProps = { - shouldClear: false, - onClear: () => {}, - autoFocus: false, - isDisabled: false, - forwardedRef: null, - selection: { - start: 0, - end: 0, - }, - maxLines: undefined, - isFullComposerAvailable: false, - setIsFullComposerAvailable: () => {}, - isComposerFullSize: false, - style: null, -}; - -function Composer({shouldClear, onClear, isDisabled, maxLines, forwardedRef, isComposerFullSize, setIsFullComposerAvailable, ...props}) { - const textInput = useRef(null); - - /** - * Set the TextInput Ref - * @param {Element} el - */ - const setTextInputRef = useCallback((el) => { - textInput.current = el; - if (!_.isFunction(forwardedRef) || textInput.current === null) { - return; - } - - // This callback prop is used by the parent component using the constructor to - // get a ref to the inner textInput element e.g. if we do - // this.textInput = el} /> this will not - // return a ref to the component, but rather the HTML element by default - forwardedRef(textInput.current); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - useEffect(() => { - if (!shouldClear) { - return; - } - textInput.current.clear(); - onClear(); - }, [shouldClear, onClear]); - - // On native layers we like to have the Text Input not focused so the - // user can read new chats without the keyboard in the way of the view. - // On Android the selection prop is required on the TextInput but this prop has issues on IOS - const propsToPass = _.omit(props, 'selection'); - return ( - ComposerUtils.updateNumberOfLines({maxLines, isComposerFullSize, isDisabled, setIsFullComposerAvailable}, e)} - rejectResponderTermination={false} - smartInsertDelete={false} - maxNumberOfLines={isComposerFullSize ? undefined : maxLines} - style={[...props.style, styles.verticalAlignMiddle]} - /* eslint-disable-next-line react/jsx-props-no-spreading */ - {...propsToPass} - readOnly={isDisabled} - /> - ); -} - -Composer.propTypes = propTypes; -Composer.defaultProps = defaultProps; - -const ComposerWithRef = React.forwardRef((props, ref) => ( - -)); - -ComposerWithRef.displayName = 'ComposerWithRef'; - -export default ComposerWithRef; diff --git a/src/components/Composer/index.android.js b/src/components/Composer/index.native.js similarity index 100% rename from src/components/Composer/index.android.js rename to src/components/Composer/index.native.js From 88ad1d9a0ba6efe401476d0b3289a53c3dbe606c Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Tue, 28 Nov 2023 22:43:45 +0100 Subject: [PATCH 028/391] remove unused changes --- src/libs/ComposerUtils/updateNumberOfLines/index.native.ts | 2 -- src/libs/ComposerUtils/updateNumberOfLines/types.ts | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/libs/ComposerUtils/updateNumberOfLines/index.native.ts b/src/libs/ComposerUtils/updateNumberOfLines/index.native.ts index 2da1c3e485d6..1911413e3d05 100644 --- a/src/libs/ComposerUtils/updateNumberOfLines/index.native.ts +++ b/src/libs/ComposerUtils/updateNumberOfLines/index.native.ts @@ -17,8 +17,6 @@ const updateNumberOfLines: UpdateNumberOfLines = (props, event) => { } const numberOfLines = getNumberOfLines(lineHeight, paddingTopAndBottom, inputHeight); updateIsFullComposerAvailable(props, numberOfLines); - - return numberOfLines; }; export default updateNumberOfLines; diff --git a/src/libs/ComposerUtils/updateNumberOfLines/types.ts b/src/libs/ComposerUtils/updateNumberOfLines/types.ts index 828c67624bd5..b0f9ba48ddc2 100644 --- a/src/libs/ComposerUtils/updateNumberOfLines/types.ts +++ b/src/libs/ComposerUtils/updateNumberOfLines/types.ts @@ -1,6 +1,6 @@ import {NativeSyntheticEvent, TextInputContentSizeChangeEventData} from 'react-native'; import ComposerProps from '@libs/ComposerUtils/types'; -type UpdateNumberOfLines = (props: ComposerProps, event: NativeSyntheticEvent) => number | void; +type UpdateNumberOfLines = (props: ComposerProps, event: NativeSyntheticEvent) => void; export default UpdateNumberOfLines; From cbf1e667fb845c429906d8943d881f16f166976e Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Tue, 28 Nov 2023 22:44:18 +0100 Subject: [PATCH 029/391] remove empty line --- src/libs/ComposerUtils/updateNumberOfLines/index.native.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/libs/ComposerUtils/updateNumberOfLines/index.native.ts b/src/libs/ComposerUtils/updateNumberOfLines/index.native.ts index 1911413e3d05..df9292ecd690 100644 --- a/src/libs/ComposerUtils/updateNumberOfLines/index.native.ts +++ b/src/libs/ComposerUtils/updateNumberOfLines/index.native.ts @@ -11,7 +11,6 @@ const updateNumberOfLines: UpdateNumberOfLines = (props, event) => { const lineHeight = styles.textInputCompose.lineHeight ?? 0; const paddingTopAndBottom = styles.textInputComposeSpacing.paddingVertical * 2; const inputHeight = event?.nativeEvent?.contentSize?.height ?? null; - if (!inputHeight) { return; } From 8d44ecdc0d7d93d185af72ca635fe47a472ba3bc Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Sat, 2 Dec 2023 20:51:12 +0100 Subject: [PATCH 030/391] remove unused style --- src/styles/styles.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/styles/styles.ts b/src/styles/styles.ts index 983f1ba82caa..98a68a9dfc1c 100644 --- a/src/styles/styles.ts +++ b/src/styles/styles.ts @@ -331,10 +331,6 @@ const styles = (theme: ThemeColors) => textAlign: 'left', }, - verticalAlignMiddle: { - verticalAlign: 'middle', - }, - verticalAlignTop: { verticalAlign: 'top', }, From cb7785b60a00384a3d12887ca538cc1a27d9255d Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Sun, 3 Dec 2023 01:25:10 +0100 Subject: [PATCH 031/391] don't build android from source --- android/settings.gradle | 9 --------- 1 file changed, 9 deletions(-) diff --git a/android/settings.gradle b/android/settings.gradle index c2bb3db7845a..680dfbc32521 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -16,12 +16,3 @@ project(':react-native-dev-menu').projectDir = new File(rootProject.projectDir, apply from: file("../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesSettingsGradle(settings) include ':app' includeBuild('../node_modules/@react-native/gradle-plugin') - -includeBuild('../node_modules/react-native') { - dependencySubstitution { - substitute(module("com.facebook.react:react-android")).using(project(":packages:react-native:ReactAndroid")) - substitute(module("com.facebook.react:react-native")).using(project(":packages:react-native:ReactAndroid")) - substitute(module("com.facebook.react:hermes-android")).using(project(":packages:react-native:ReactAndroid:hermes-engine")) - substitute(module("com.facebook.react:hermes-engine")).using(project(":packages:react-native:ReactAndroid:hermes-engine")) - } -} From ff9ef800efb029170d078e1073577c76f3b79ea2 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Tue, 5 Dec 2023 10:28:48 +0100 Subject: [PATCH 032/391] fix: types --- src/libs/OptionsListUtils.ts | 186 +++++++++++++++-------------------- src/libs/ReportUtils.ts | 4 + 2 files changed, 82 insertions(+), 108 deletions(-) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 7c1156546454..3d7c6fb7ac98 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -6,9 +6,12 @@ import lodashSet from 'lodash/set'; import Onyx, {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import {ValueOf} from 'type-fest'; import CONST from '@src/CONST'; +import {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; import {Beta, Login, PersonalDetails, Policy, PolicyCategory, Report, ReportAction, ReportActions, Transaction} from '@src/types/onyx'; +import {Participant} from '@src/types/onyx/IOU'; import * as OnyxCommon from '@src/types/onyx/OnyxCommon'; +import {isEmptyObject, isNotEmptyObject} from '@src/types/utils/EmptyObject'; import * as CollectionUtils from './CollectionUtils'; import * as ErrorUtils from './ErrorUtils'; import * as LocalePhoneNumber from './LocalePhoneNumber'; @@ -82,7 +85,7 @@ Onyx.connect({ } const reportID = CollectionUtils.extractCollectionItemID(key); allReportActions[reportID] = actions; - const sortedReportActions = ReportActionUtils.getSortedReportActions(_.toArray(actions), true); + const sortedReportActions = ReportActionUtils.getSortedReportActions(Object.values(actions), true); allSortedReportActions[reportID] = sortedReportActions; lastReportActions[reportID] = sortedReportActions[0]; }, @@ -286,11 +289,11 @@ function getSearchText(report: Report, reportName: string, personalDetailList: A const chatRoomSubtitle = ReportUtils.getChatRoomSubtitle(report); Array.prototype.push.apply(searchTerms, title.split(/[,\s]/)); - Array.prototype.push.apply(searchTerms, chatRoomSubtitle?.split(/[,\s]/)); + Array.prototype.push.apply(searchTerms, chatRoomSubtitle?.split(/[,\s]/) ?? ['']); } else if (isChatRoomOrPolicyExpenseChat) { const chatRoomSubtitle = ReportUtils.getChatRoomSubtitle(report); - Array.prototype.push.apply(searchTerms, chatRoomSubtitle?.split(/[,\s]/)); + Array.prototype.push.apply(searchTerms, chatRoomSubtitle?.split(/[,\s]/) ?? ['']); } else { const participantAccountIDs = report.participantAccountIDs ?? []; if (allPersonalDetails) { @@ -310,24 +313,25 @@ function getSearchText(report: Report, reportName: string, personalDetailList: A /** * Get an object of error messages keyed by microtime by combining all error objects related to the report. */ -function getAllReportErrors(report: OnyxEntry, reportActions: OnyxEntry) { +function getAllReportErrors(report: OnyxEntry, reportActions: OnyxEntry) { const reportErrors = report?.errors ?? {}; const reportErrorFields = report?.errorFields ?? {}; - const reportActionErrors = Object.values(reportActions ?? {}).reduce( - (prevReportActionErrors: OnyxCommon.Errors, action: ReportAction) => (!action || _.isEmpty(action.errors) ? prevReportActionErrors : _.extend(prevReportActionErrors, action.errors)), + const reportActionErrors: OnyxCommon.Errors = Object.values(reportActions ?? {}).reduce( + (prevReportActionErrors, action) => (!action || isEmptyObject(action.errors) ? prevReportActionErrors : {...prevReportActionErrors, ...action.errors}), {}, ); - const parentReportAction: ReportAction = !report?.parentReportID || !report?.parentReportActionID ? {} : allReportActions[report.parentReportID][report.parentReportActionID] ?? {}; + const parentReportAction: OnyxEntry = + !report?.parentReportID || !report?.parentReportActionID ? null : allReportActions[report.parentReportID][report.parentReportActionID] ?? null; if (parentReportAction?.actorAccountID === currentUserAccountID && ReportActionUtils.isTransactionThread(parentReportAction)) { - const transactionID = lodashGet(parentReportAction, ['originalMessage', 'IOUTransactionID'], ''); - const transaction = allTransactions[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`] || {}; + const transactionID = parentReportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? parentReportAction?.originalMessage?.IOUTransactionID : undefined; + const transaction = allTransactions[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]; if (TransactionUtils.hasMissingSmartscanFields(transaction) && !ReportUtils.isSettled(transaction.reportID)) { _.extend(reportActionErrors, {smartscan: ErrorUtils.getMicroSecondOnyxError('report.genericSmartscanFailureMessage')}); } - } else if ((ReportUtils.isIOUReport(report) || ReportUtils.isExpenseReport(report)) && report.ownerAccountID === currentUserAccountID) { - if (ReportUtils.hasMissingSmartscanFields(report.reportID) && !ReportUtils.isSettled(report.reportID)) { + } else if ((ReportUtils.isIOUReport(report) || ReportUtils.isExpenseReport(report)) && report?.ownerAccountID === currentUserAccountID) { + if (ReportUtils.hasMissingSmartscanFields(report?.reportID ?? '') && !ReportUtils.isSettled(report?.reportID)) { _.extend(reportActionErrors, {smartscan: ErrorUtils.getMicroSecondOnyxError('report.genericSmartscanFailureMessage')}); } } @@ -350,43 +354,42 @@ function getAllReportErrors(report: OnyxEntry, reportActions: OnyxEntry< /** * Get the last message text from the report directly or from other sources for special cases. */ -function getLastMessageTextForReport(report) { - const lastReportAction = _.find(allSortedReportActions[report.reportID], (reportAction) => ReportActionUtils.shouldReportActionBeVisibleAsLastAction(reportAction)); +function getLastMessageTextForReport(report: OnyxEntry) { + const lastReportAction = allSortedReportActions[report?.reportID ?? '']?.find((reportAction) => ReportActionUtils.shouldReportActionBeVisibleAsLastAction(reportAction)); let lastMessageTextFromReport = ''; - const lastActionName = lodashGet(lastReportAction, 'actionName', ''); + const lastActionName = lastReportAction?.actionName ?? ''; - if (ReportActionUtils.isMoneyRequestAction(lastReportAction)) { + if (ReportActionUtils.isMoneyRequestAction(lastReportAction ?? null)) { const properSchemaForMoneyRequestMessage = ReportUtils.getReportPreviewMessage(report, lastReportAction, true); lastMessageTextFromReport = ReportUtils.formatReportLastMessageText(properSchemaForMoneyRequestMessage); - } else if (ReportActionUtils.isReportPreviewAction(lastReportAction)) { - const iouReport = ReportUtils.getReport(ReportActionUtils.getIOUReportIDFromReportActionPreview(lastReportAction)); - const lastIOUMoneyReport = _.find( - allSortedReportActions[iouReport.reportID], + } else if (ReportActionUtils.isReportPreviewAction(lastReportAction ?? null)) { + const iouReport = ReportUtils.getReport(ReportActionUtils.getIOUReportIDFromReportActionPreview(lastReportAction ?? null)); + const lastIOUMoneyReport = allSortedReportActions[iouReport?.reportID ?? '']?.find( (reportAction, key) => ReportActionUtils.shouldReportActionBeVisible(reportAction, key) && reportAction.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE && ReportActionUtils.isMoneyRequestAction(reportAction), ); - lastMessageTextFromReport = ReportUtils.getReportPreviewMessage(iouReport, lastIOUMoneyReport, true, ReportUtils.isChatReport(report)); - } else if (ReportActionUtils.isReimbursementQueuedAction(lastReportAction)) { - lastMessageTextFromReport = ReportUtils.getReimbursementQueuedActionMessage(lastReportAction, report); - } else if (ReportActionUtils.isReimbursementDeQueuedAction(lastReportAction)) { + lastMessageTextFromReport = ReportUtils.getReportPreviewMessage(isNotEmptyObject(iouReport) ? iouReport : null, lastIOUMoneyReport, true, ReportUtils.isChatReport(report)); + } else if (ReportActionUtils.isReimbursementQueuedAction(lastReportAction ?? null)) { + lastMessageTextFromReport = ReportUtils.getReimbursementQueuedActionMessage(lastReportAction ?? null, report); + } else if (ReportActionUtils.isReimbursementDeQueuedAction(lastReportAction ?? null)) { lastMessageTextFromReport = ReportUtils.getReimbursementDeQueuedActionMessage(report); - } else if (ReportActionUtils.isDeletedParentAction(lastReportAction) && ReportUtils.isChatReport(report)) { - lastMessageTextFromReport = ReportUtils.getDeletedParentActionMessageForChatReport(lastReportAction); - } else if (ReportUtils.isReportMessageAttachment({text: report.lastMessageText, html: report.lastMessageHtml, translationKey: report.lastMessageTranslationKey})) { - lastMessageTextFromReport = `[${Localize.translateLocal(report.lastMessageTranslationKey || 'common.attachment')}]`; - } else if (ReportActionUtils.isModifiedExpenseAction(lastReportAction)) { - const properSchemaForModifiedExpenseMessage = ReportUtils.getModifiedExpenseMessage(lastReportAction); - lastMessageTextFromReport = ReportUtils.formatReportLastMessageText(properSchemaForModifiedExpenseMessage, true); + } else if (ReportActionUtils.isDeletedParentAction(lastReportAction ?? null) && ReportUtils.isChatReport(report)) { + lastMessageTextFromReport = ReportUtils.getDeletedParentActionMessageForChatReport(lastReportAction ?? null); + } else if (ReportUtils.isReportMessageAttachment({text: report?.lastMessageText ?? '', html: report?.lastMessageHtml, translationKey: report?.lastMessageTranslationKey, type: ''})) { + lastMessageTextFromReport = `[${Localize.translateLocal((report?.lastMessageTranslationKey ?? 'common.attachment') as TranslationPaths)}]`; + } else if (ReportActionUtils.isModifiedExpenseAction(lastReportAction ?? null)) { + const properSchemaForModifiedExpenseMessage = ReportUtils.getModifiedExpenseMessage(lastReportAction ?? null); + lastMessageTextFromReport = ReportUtils.formatReportLastMessageText(properSchemaForModifiedExpenseMessage ?? '', true); } else if ( lastActionName === CONST.REPORT.ACTIONS.TYPE.TASKCOMPLETED || lastActionName === CONST.REPORT.ACTIONS.TYPE.TASKREOPENED || lastActionName === CONST.REPORT.ACTIONS.TYPE.TASKCANCELLED ) { - lastMessageTextFromReport = lodashGet(lastReportAction, 'message[0].text', ''); + lastMessageTextFromReport = lastReportAction?.message?.[0].text ?? ''; } else { - lastMessageTextFromReport = report ? report.lastMessageText || '' : ''; + lastMessageTextFromReport = report ? report.lastMessageText ?? '' : ''; } return lastMessageTextFromReport; } @@ -397,24 +400,24 @@ function getLastMessageTextForReport(report) { function createOption( accountIDs: number[], personalDetails: PersonalDetailsCollection, - report: Report, + report: OnyxEntry, reportActions: Record, {showChatPreviewLine = false, forcePolicyNamePreview = false}: {showChatPreviewLine?: boolean; forcePolicyNamePreview?: boolean}, ) { const result: ReportUtils.OptionData = { - text: null, + text: undefined, alternateText: null, pendingAction: null, allReportErrors: null, brickRoadIndicator: null, - icons: null, + icons: undefined, tooltipText: null, - ownerAccountID: null, + ownerAccountID: undefined, subtitle: null, - participantsList: null, + participantsList: undefined, accountID: 0, login: null, - reportID: null, + reportID: '', phoneNumber: null, hasDraftComment: false, keyForList: null, @@ -423,7 +426,7 @@ function createOption( isPinned: false, hasOutstandingIOU: false, isWaitingOnBankAccount: false, - iouReportID: null, + iouReportID: undefined, isIOUReportOwner: null, iouReportAmount: 0, isChatRoom: false, @@ -432,7 +435,7 @@ function createOption( isPolicyExpenseChat: false, isOwnPolicyExpenseChat: false, isExpenseReport: false, - policyID: null, + policyID: undefined, isOptimisticPersonalDetail: false, }; @@ -456,9 +459,9 @@ function createOption( result.isTaskReport = ReportUtils.isTaskReport(report); result.shouldShowSubscript = ReportUtils.shouldReportShowSubscript(report); result.isPolicyExpenseChat = ReportUtils.isPolicyExpenseChat(report); - result.isOwnPolicyExpenseChat = report.isOwnPolicyExpenseChat || false; + result.isOwnPolicyExpenseChat = report.isOwnPolicyExpenseChat ?? false; result.allReportErrors = getAllReportErrors(report, reportActions); - result.brickRoadIndicator = !_.isEmpty(result.allReportErrors) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : ''; + result.brickRoadIndicator = isNotEmptyObject(result.allReportErrors) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : ''; result.pendingAction = report.pendingFields ? report.pendingFields.addWorkspaceRoom ?? report.pendingFields.createChat : null; result.ownerAccountID = report.ownerAccountID; result.reportID = report.reportID; @@ -479,27 +482,27 @@ function createOption( 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 ?? ''].originalMessage?.reason ?? CONST.REPORT.ARCHIVE_REASON.DEFAULT; - lastMessageText = Localize.translate(preferredLocale, `reportArchiveReasons.${archiveReason}`, { + const lastReportAction = lastReportActions[report.reportID ?? '']; + if (result.isArchivedRoom && lastReportAction.actionName === CONST.REPORT.ACTIONS.TYPE.CLOSED) { + const archiveReason = lastReportAction.originalMessage?.reason ?? CONST.REPORT.ARCHIVE_REASON.DEFAULT; + lastMessageText = Localize.translate(preferredLocale ?? CONST.LOCALES.DEFAULT, `reportArchiveReasons.${archiveReason}`, { displayName: archiveReason.displayName ?? PersonalDetailsUtils.getDisplayNameOrDefault(lastActorDetails, 'displayName'), policyName: ReportUtils.getPolicyName(report), }); } if (result.isThread || result.isMoneyRequestReport) { - result.alternateText = lastMessageTextFromReport.length > 0 ? lastMessageText : Localize.translate(preferredLocale, 'report.noActivityYet'); + result.alternateText = lastMessageTextFromReport.length > 0 ? lastMessageText : Localize.translate(preferredLocale ?? CONST.LOCALES.DEFAULT, 'report.noActivityYet'); } else if (result.isChatRoom || result.isPolicyExpenseChat) { result.alternateText = showChatPreviewLine && !forcePolicyNamePreview && lastMessageText ? lastMessageText : subtitle; } else if (result.isTaskReport) { - result.alternateText = showChatPreviewLine && lastMessageText ? lastMessageTextFromReport : Localize.translate(preferredLocale, 'report.noActivityYet'); + result.alternateText = showChatPreviewLine && lastMessageText ? lastMessageTextFromReport : Localize.translate(preferredLocale ?? CONST.LOCALES.DEFAULT, 'report.noActivityYet'); } else { - result.alternateText = showChatPreviewLine && lastMessageText ? lastMessageText : LocalePhoneNumber.formatPhoneNumber(personalDetail.login); + result.alternateText = showChatPreviewLine && lastMessageText ? lastMessageText : LocalePhoneNumber.formatPhoneNumber(personalDetail.login ?? ''); } reportName = ReportUtils.getReportName(report); } else { - reportName = ReportUtils.getDisplayNameForParticipant(accountIDs[0]) || LocalePhoneNumber.formatPhoneNumber(personalDetail.login); + reportName = ReportUtils.getDisplayNameForParticipant(accountIDs[0]) ?? LocalePhoneNumber.formatPhoneNumber(personalDetail.login ?? ''); result.keyForList = String(accountIDs[0]); result.alternateText = LocalePhoneNumber.formatPhoneNumber(personalDetails[accountIDs[0]].login ?? ''); @@ -515,7 +518,8 @@ function createOption( } result.text = reportName; - result.searchText = getSearchText(report, reportName, personalDetailList, result.isChatRoom ?? result.isPolicyExpenseChat, result.isThread); + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + 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; @@ -531,7 +535,7 @@ function getPolicyExpenseReportOption(report: Report & {selected?: boolean; sear const option = createOption( expenseReport?.participantAccountIDs ?? [], allPersonalDetails ?? [], - expenseReport, + expenseReport ?? null, {}, { showChatPreviewLine: false, @@ -545,30 +549,6 @@ function getPolicyExpenseReportOption(report: Report & {selected?: boolean; sear option.selected = report.selected; return option; } -// /** -// * Get the option for a policy expense report. -// */ -// function getPolicyExpenseReportOption() { -// const expenseReport = policyExpenseReports?.[`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`]; -// const policyExpenseChatAvatarSource = ReportUtils.getWorkspaceAvatar(expenseReport); -// const reportName = ReportUtils.getReportName(expenseReport); -// return { -// ...expenseReport, -// keyForList: expenseReport?.policyID, -// text: reportName, -// alternateText: Localize.translateLocal('workspace.common.workspace'), -// icons: [ -// { -// source: policyExpenseChatAvatarSource, -// name: reportName, -// type: CONST.ICON_TYPE_WORKSPACE, -// }, -// ], -// selected: report.selected, -// isPolicyExpenseChat: true, -// searchText: report.searchText, -// }; -// } /** * Searches for a match when provided with a value @@ -627,16 +607,10 @@ function hasEnabledOptions(options: Record): boolean { * Sorts categories using a simple object. * It builds an hierarchy (based on an object), where each category has a name and other keys as subcategories. * Via the hierarchy we avoid duplicating and sort categories one by one. Subcategories are being sorted alphabetically. - * - * @param {Object} categories - * @returns {Array} */ -function sortCategories(categories) { +function sortCategories(categories: Record) { // Sorts categories alphabetically by name. - const sortedCategories = _.chain(categories) - .values() - .sortBy((category) => category.name) - .value(); + const sortedCategories = Object.values(categories).sort((a, b) => a.name.localeCompare(b.name)); // An object that respects nesting of categories. Also, can contain only uniq categories. const hierarchy = {}; @@ -656,16 +630,16 @@ function sortCategories(categories) { * } * } */ - _.each(sortedCategories, (category) => { + sortedCategories.forEach((category) => { const path = category.name.split(CONST.PARENT_CHILD_SEPARATOR); - const existedValue = lodashGet(hierarchy, path, {}); + const existedValue = hierarchy?.path ?? {}; lodashSet(hierarchy, path, { ...existedValue, name: category.name, }); }); - + console.log(sortedCategories, hierarchy); /** * A recursive function to convert hierarchy into an array of category objects. * The category object contains base 2 properties: "name" and "enabled". @@ -675,30 +649,26 @@ function sortCategories(categories) { * @returns {Array} */ const flatHierarchy = (initialHierarchy) => - _.reduce( - initialHierarchy, - (acc, category) => { - const {name, ...subcategories} = category; - - if (!_.isEmpty(name)) { - const categoryObject = { - name, - enabled: lodashGet(categories, [name, 'enabled'], false), - }; - - acc.push(categoryObject); - } + initialHierarchy.reduce((acc, category) => { + const {name, ...subcategories} = category; - if (!_.isEmpty(subcategories)) { - const nestedCategories = flatHierarchy(subcategories); + if (!_.isEmpty(name)) { + const categoryObject = { + name, + enabled: lodashGet(categories, [name, 'enabled'], false), + }; - acc.push(..._.sortBy(nestedCategories, 'name')); - } + acc.push(categoryObject); + } - return acc; - }, - [], - ); + if (!_.isEmpty(subcategories)) { + const nestedCategories = flatHierarchy(subcategories); + + acc.push(..._.sortBy(nestedCategories, 'name')); + } + + return acc; + }, []); return flatHierarchy(hierarchy); } diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 8f2382111f34..60d256d3c841 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -338,6 +338,10 @@ type OptionData = { isTaskReport?: boolean | null; parentReportAction?: ReportAction; displayNamesWithTooltips?: DisplayNameWithTooltips | null; + isDefaultRoom?: boolean; + isExpenseReport?: boolean; + isOptimisticPersonalDetail?: boolean; + selected?: boolean; } & Report; type OnyxDataTaskAssigneeChat = { From 558dbee176c47bb4335143320c71e755e5b5364e Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Tue, 5 Dec 2023 11:54:03 +0100 Subject: [PATCH 033/391] remove patch --- ...ve+0.72.4+002+ModalKeyboardFlashing.patch} | 0 ...eact-native+0.72.4+002+NumberOfLines.patch | 978 ------------------ 2 files changed, 978 deletions(-) rename patches/{react-native+0.72.4+004+ModalKeyboardFlashing.patch => react-native+0.72.4+002+ModalKeyboardFlashing.patch} (100%) delete mode 100644 patches/react-native+0.72.4+002+NumberOfLines.patch diff --git a/patches/react-native+0.72.4+004+ModalKeyboardFlashing.patch b/patches/react-native+0.72.4+002+ModalKeyboardFlashing.patch similarity index 100% rename from patches/react-native+0.72.4+004+ModalKeyboardFlashing.patch rename to patches/react-native+0.72.4+002+ModalKeyboardFlashing.patch diff --git a/patches/react-native+0.72.4+002+NumberOfLines.patch b/patches/react-native+0.72.4+002+NumberOfLines.patch deleted file mode 100644 index 75422f84708e..000000000000 --- a/patches/react-native+0.72.4+002+NumberOfLines.patch +++ /dev/null @@ -1,978 +0,0 @@ -diff --git a/node_modules/react-native/Libraries/Components/TextInput/AndroidTextInputNativeComponent.js b/node_modules/react-native/Libraries/Components/TextInput/AndroidTextInputNativeComponent.js -index 55b770d..4073836 100644 ---- a/node_modules/react-native/Libraries/Components/TextInput/AndroidTextInputNativeComponent.js -+++ b/node_modules/react-native/Libraries/Components/TextInput/AndroidTextInputNativeComponent.js -@@ -179,6 +179,13 @@ export type NativeProps = $ReadOnly<{| - */ - numberOfLines?: ?Int32, - -+ /** -+ * Sets the maximum number of lines for a `TextInput`. Use it with multiline set to -+ * `true` to be able to fill the lines. -+ * @platform android -+ */ -+ maximumNumberOfLines?: ?Int32, -+ - /** - * When `false`, if there is a small amount of space available around a text input - * (e.g. landscape orientation on a phone), the OS may choose to have the user edit -diff --git a/node_modules/react-native/Libraries/Components/TextInput/RCTTextInputViewConfig.js b/node_modules/react-native/Libraries/Components/TextInput/RCTTextInputViewConfig.js -index 6f69329..d531bee 100644 ---- a/node_modules/react-native/Libraries/Components/TextInput/RCTTextInputViewConfig.js -+++ b/node_modules/react-native/Libraries/Components/TextInput/RCTTextInputViewConfig.js -@@ -144,6 +144,8 @@ const RCTTextInputViewConfig = { - placeholder: true, - autoCorrect: true, - multiline: true, -+ numberOfLines: true, -+ maximumNumberOfLines: true, - textContentType: true, - maxLength: true, - autoCapitalize: true, -diff --git a/node_modules/react-native/Libraries/Components/TextInput/TextInput.d.ts b/node_modules/react-native/Libraries/Components/TextInput/TextInput.d.ts -index 8badb2a..b19f197 100644 ---- a/node_modules/react-native/Libraries/Components/TextInput/TextInput.d.ts -+++ b/node_modules/react-native/Libraries/Components/TextInput/TextInput.d.ts -@@ -347,12 +347,6 @@ export interface TextInputAndroidProps { - */ - inlineImagePadding?: number | undefined; - -- /** -- * Sets the number of lines for a TextInput. -- * Use it with multiline set to true to be able to fill the lines. -- */ -- numberOfLines?: number | undefined; -- - /** - * Sets the return key to the label. Use it instead of `returnKeyType`. - * @platform android -@@ -663,11 +657,30 @@ export interface TextInputProps - */ - maxLength?: number | undefined; - -+ /** -+ * Sets the maximum number of lines for a TextInput. -+ * Use it with multiline set to true to be able to fill the lines. -+ */ -+ maxNumberOfLines?: number | undefined; -+ - /** - * If true, the text input can be multiple lines. The default value is false. - */ - multiline?: boolean | undefined; - -+ /** -+ * Sets the number of lines for a TextInput. -+ * Use it with multiline set to true to be able to fill the lines. -+ */ -+ numberOfLines?: number | undefined; -+ -+ /** -+ * Sets the number of rows for a TextInput. -+ * Use it with multiline set to true to be able to fill the lines. -+ */ -+ rows?: number | undefined; -+ -+ - /** - * Callback that is called when the text input is blurred - */ -diff --git a/node_modules/react-native/Libraries/Components/TextInput/TextInput.flow.js b/node_modules/react-native/Libraries/Components/TextInput/TextInput.flow.js -index 7ed4579..b1d994e 100644 ---- a/node_modules/react-native/Libraries/Components/TextInput/TextInput.flow.js -+++ b/node_modules/react-native/Libraries/Components/TextInput/TextInput.flow.js -@@ -343,26 +343,12 @@ type AndroidProps = $ReadOnly<{| - */ - inlineImagePadding?: ?number, - -- /** -- * Sets the number of lines for a `TextInput`. Use it with multiline set to -- * `true` to be able to fill the lines. -- * @platform android -- */ -- numberOfLines?: ?number, -- - /** - * Sets the return key to the label. Use it instead of `returnKeyType`. - * @platform android - */ - returnKeyLabel?: ?string, - -- /** -- * Sets the number of rows for a `TextInput`. Use it with multiline set to -- * `true` to be able to fill the lines. -- * @platform android -- */ -- rows?: ?number, -- - /** - * When `false`, it will prevent the soft keyboard from showing when the field is focused. - * Defaults to `true`. -@@ -632,6 +618,12 @@ export type Props = $ReadOnly<{| - */ - keyboardType?: ?KeyboardType, - -+ /** -+ * Sets the maximum number of lines for a `TextInput`. Use it with multiline set to -+ * `true` to be able to fill the lines. -+ */ -+ maxNumberOfLines?: ?number, -+ - /** - * Specifies largest possible scale a font can reach when `allowFontScaling` is enabled. - * Possible values: -@@ -653,6 +645,12 @@ export type Props = $ReadOnly<{| - */ - multiline?: ?boolean, - -+ /** -+ * Sets the number of lines for a `TextInput`. Use it with multiline set to -+ * `true` to be able to fill the lines. -+ */ -+ numberOfLines?: ?number, -+ - /** - * Callback that is called when the text input is blurred. - */ -@@ -814,6 +812,12 @@ export type Props = $ReadOnly<{| - */ - returnKeyType?: ?ReturnKeyType, - -+ /** -+ * Sets the number of rows for a `TextInput`. Use it with multiline set to -+ * `true` to be able to fill the lines. -+ */ -+ rows?: ?number, -+ - /** - * If `true`, the text input obscures the text entered so that sensitive text - * like passwords stay secure. The default value is `false`. Does not work with 'multiline={true}'. -diff --git a/node_modules/react-native/Libraries/Components/TextInput/TextInput.js b/node_modules/react-native/Libraries/Components/TextInput/TextInput.js -index 2127191..542fc06 100644 ---- a/node_modules/react-native/Libraries/Components/TextInput/TextInput.js -+++ b/node_modules/react-native/Libraries/Components/TextInput/TextInput.js -@@ -390,7 +390,6 @@ type AndroidProps = $ReadOnly<{| - /** - * Sets the number of lines for a `TextInput`. Use it with multiline set to - * `true` to be able to fill the lines. -- * @platform android - */ - numberOfLines?: ?number, - -@@ -403,10 +402,14 @@ type AndroidProps = $ReadOnly<{| - /** - * Sets the number of rows for a `TextInput`. Use it with multiline set to - * `true` to be able to fill the lines. -- * @platform android - */ - rows?: ?number, - -+ /** -+ * Sets the maximum number of lines the TextInput can have. -+ */ -+ maxNumberOfLines?: ?number, -+ - /** - * When `false`, it will prevent the soft keyboard from showing when the field is focused. - * Defaults to `true`. -@@ -1069,6 +1072,9 @@ function InternalTextInput(props: Props): React.Node { - accessibilityState, - id, - tabIndex, -+ rows, -+ numberOfLines, -+ maxNumberOfLines, - selection: propsSelection, - ...otherProps - } = props; -@@ -1427,6 +1433,8 @@ function InternalTextInput(props: Props): React.Node { - focusable={tabIndex !== undefined ? !tabIndex : focusable} - mostRecentEventCount={mostRecentEventCount} - nativeID={id ?? props.nativeID} -+ numberOfLines={props.rows ?? props.numberOfLines} -+ maximumNumberOfLines={maxNumberOfLines} - onBlur={_onBlur} - onKeyPressSync={props.unstable_onKeyPressSync} - onChange={_onChange} -@@ -1482,6 +1490,7 @@ function InternalTextInput(props: Props): React.Node { - mostRecentEventCount={mostRecentEventCount} - nativeID={id ?? props.nativeID} - numberOfLines={props.rows ?? props.numberOfLines} -+ maximumNumberOfLines={maxNumberOfLines} - onBlur={_onBlur} - onChange={_onChange} - onFocus={_onFocus} -diff --git a/node_modules/react-native/Libraries/Text/Text.js b/node_modules/react-native/Libraries/Text/Text.js -index df548af..e02f5da 100644 ---- a/node_modules/react-native/Libraries/Text/Text.js -+++ b/node_modules/react-native/Libraries/Text/Text.js -@@ -18,7 +18,11 @@ import processColor from '../StyleSheet/processColor'; - import {getAccessibilityRoleFromRole} from '../Utilities/AcessibilityMapping'; - import Platform from '../Utilities/Platform'; - import TextAncestor from './TextAncestor'; --import {NativeText, NativeVirtualText} from './TextNativeComponent'; -+import { -+ CONTAINS_MAX_NUMBER_OF_LINES_RENAME, -+ NativeText, -+ NativeVirtualText, -+} from './TextNativeComponent'; - import * as React from 'react'; - import {useContext, useMemo, useState} from 'react'; - -@@ -59,6 +63,7 @@ const Text: React.AbstractComponent< - pressRetentionOffset, - role, - suppressHighlighting, -+ numberOfLines, - ...restProps - } = props; - -@@ -192,14 +197,33 @@ const Text: React.AbstractComponent< - } - } - -- let numberOfLines = restProps.numberOfLines; -+ let numberOfLinesValue = numberOfLines; - if (numberOfLines != null && !(numberOfLines >= 0)) { - console.error( - `'numberOfLines' in must be a non-negative number, received: ${numberOfLines}. The value will be set to 0.`, - ); -- numberOfLines = 0; -+ numberOfLinesValue = 0; - } - -+ const numberOfLinesProps = useMemo((): { -+ maximumNumberOfLines?: ?number, -+ numberOfLines?: ?number, -+ } => { -+ // FIXME: Current logic is breaking all Text components. -+ // if (CONTAINS_MAX_NUMBER_OF_LINES_RENAME) { -+ // return { -+ // maximumNumberOfLines: numberOfLinesValue, -+ // }; -+ // } else { -+ // return { -+ // numberOfLines: numberOfLinesValue, -+ // }; -+ // } -+ return { -+ maximumNumberOfLines: numberOfLinesValue, -+ }; -+ }, [numberOfLinesValue]); -+ - const hasTextAncestor = useContext(TextAncestor); - - const _accessible = Platform.select({ -@@ -241,7 +265,6 @@ const Text: React.AbstractComponent< - isHighlighted={isHighlighted} - isPressable={isPressable} - nativeID={id ?? nativeID} -- numberOfLines={numberOfLines} - ref={forwardedRef} - selectable={_selectable} - selectionColor={selectionColor} -@@ -252,6 +275,7 @@ const Text: React.AbstractComponent< - - #import -+#import -+#import - - @implementation RCTMultilineTextInputViewManager - -@@ -17,8 +19,21 @@ - (UIView *)view - return [[RCTMultilineTextInputView alloc] initWithBridge:self.bridge]; - } - -+- (RCTShadowView *)shadowView -+{ -+ RCTBaseTextInputShadowView *shadowView = (RCTBaseTextInputShadowView *)[super shadowView]; -+ -+ shadowView.maximumNumberOfLines = 0; -+ shadowView.exactNumberOfLines = 0; -+ -+ return shadowView; -+} -+ - #pragma mark - Multiline (aka TextView) specific properties - - RCT_REMAP_VIEW_PROPERTY(dataDetectorTypes, backedTextInputView.dataDetectorTypes, UIDataDetectorTypes) - -+RCT_EXPORT_SHADOW_PROPERTY(maximumNumberOfLines, NSInteger) -+RCT_REMAP_SHADOW_PROPERTY(numberOfLines, exactNumberOfLines, NSInteger) -+ - @end -diff --git a/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputShadowView.h b/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputShadowView.h -index 8f4cf7e..6238ebc 100644 ---- a/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputShadowView.h -+++ b/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputShadowView.h -@@ -16,6 +16,7 @@ NS_ASSUME_NONNULL_BEGIN - @property (nonatomic, copy, nullable) NSString *text; - @property (nonatomic, copy, nullable) NSString *placeholder; - @property (nonatomic, assign) NSInteger maximumNumberOfLines; -+@property (nonatomic, assign) NSInteger exactNumberOfLines; - @property (nonatomic, copy, nullable) RCTDirectEventBlock onContentSizeChange; - - - (void)uiManagerWillPerformMounting; -diff --git a/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputShadowView.m b/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputShadowView.m -index 04d2446..9d77743 100644 ---- a/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputShadowView.m -+++ b/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputShadowView.m -@@ -218,7 +218,22 @@ - (NSAttributedString *)measurableAttributedText - - - (CGSize)sizeThatFitsMinimumSize:(CGSize)minimumSize maximumSize:(CGSize)maximumSize - { -- NSAttributedString *attributedText = [self measurableAttributedText]; -+ NSMutableAttributedString *attributedText = [[self measurableAttributedText] mutableCopy]; -+ -+ /* -+ * The block below is responsible for setting the exact height of the view in lines -+ * Unfortunatelly, iOS doesn't export any easy way to do it. So we set maximumNumberOfLines -+ * prop and then add random lines at the front. However, they are only used for layout -+ * so they are not visible on the screen. -+ */ -+ if (self.exactNumberOfLines) { -+ NSMutableString *newLines = [NSMutableString stringWithCapacity:self.exactNumberOfLines]; -+ for (NSUInteger i = 0UL; i < self.exactNumberOfLines; ++i) { -+ [newLines appendString:@"\n"]; -+ } -+ [attributedText insertAttributedString:[[NSAttributedString alloc] initWithString:newLines attributes:self.textAttributes.effectiveTextAttributes] atIndex:0]; -+ _maximumNumberOfLines = self.exactNumberOfLines; -+ } - - if (!_textStorage) { - _textContainer = [NSTextContainer new]; -diff --git a/node_modules/react-native/Libraries/Text/TextInput/Singleline/RCTSinglelineTextInputViewManager.m b/node_modules/react-native/Libraries/Text/TextInput/Singleline/RCTSinglelineTextInputViewManager.m -index 413ac42..56d039c 100644 ---- a/node_modules/react-native/Libraries/Text/TextInput/Singleline/RCTSinglelineTextInputViewManager.m -+++ b/node_modules/react-native/Libraries/Text/TextInput/Singleline/RCTSinglelineTextInputViewManager.m -@@ -19,6 +19,7 @@ - (RCTShadowView *)shadowView - RCTBaseTextInputShadowView *shadowView = (RCTBaseTextInputShadowView *)[super shadowView]; - - shadowView.maximumNumberOfLines = 1; -+ shadowView.exactNumberOfLines = 0; - - return shadowView; - } -diff --git a/node_modules/react-native/Libraries/Text/TextNativeComponent.js b/node_modules/react-native/Libraries/Text/TextNativeComponent.js -index 0d59904..3216e43 100644 ---- a/node_modules/react-native/Libraries/Text/TextNativeComponent.js -+++ b/node_modules/react-native/Libraries/Text/TextNativeComponent.js -@@ -9,6 +9,7 @@ - */ - - import {createViewConfig} from '../NativeComponent/ViewConfig'; -+import getNativeComponentAttributes from '../ReactNative/getNativeComponentAttributes'; - import UIManager from '../ReactNative/UIManager'; - import createReactNativeComponentClass from '../Renderer/shims/createReactNativeComponentClass'; - import {type HostComponent} from '../Renderer/shims/ReactNativeTypes'; -@@ -18,6 +19,7 @@ import {type TextProps} from './TextProps'; - - type NativeTextProps = $ReadOnly<{ - ...TextProps, -+ maximumNumberOfLines?: ?number, - isHighlighted?: ?boolean, - selectionColor?: ?ProcessedColorValue, - onClick?: ?(event: PressEvent) => mixed, -@@ -31,7 +33,7 @@ const textViewConfig = { - validAttributes: { - isHighlighted: true, - isPressable: true, -- numberOfLines: true, -+ maximumNumberOfLines: true, - ellipsizeMode: true, - allowFontScaling: true, - dynamicTypeRamp: true, -@@ -73,6 +75,12 @@ export const NativeText: HostComponent = - createViewConfig(textViewConfig), - ): any); - -+const jestIsDefined = typeof jest !== 'undefined'; -+export const CONTAINS_MAX_NUMBER_OF_LINES_RENAME: boolean = jestIsDefined -+ ? true -+ : getNativeComponentAttributes('RCTText')?.NativeProps -+ ?.maximumNumberOfLines === 'number'; -+ - export const NativeVirtualText: HostComponent = - !global.RN$Bridgeless && !UIManager.hasViewManagerConfig('RCTVirtualText') - ? NativeText -diff --git a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewDefaults.java b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewDefaults.java -index 8cab407..ad5fa96 100644 ---- a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewDefaults.java -+++ b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewDefaults.java -@@ -12,5 +12,6 @@ public class ViewDefaults { - - public static final float FONT_SIZE_SP = 14.0f; - public static final int LINE_HEIGHT = 0; -- public static final int NUMBER_OF_LINES = Integer.MAX_VALUE; -+ public static final int NUMBER_OF_LINES = -1; -+ public static final int MAXIMUM_NUMBER_OF_LINES = Integer.MAX_VALUE; - } -diff --git a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewProps.java b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewProps.java -index 3f76fa7..7a5d096 100644 ---- a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewProps.java -+++ b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewProps.java -@@ -96,6 +96,7 @@ public class ViewProps { - public static final String LETTER_SPACING = "letterSpacing"; - public static final String NEEDS_OFFSCREEN_ALPHA_COMPOSITING = "needsOffscreenAlphaCompositing"; - public static final String NUMBER_OF_LINES = "numberOfLines"; -+ public static final String MAXIMUM_NUMBER_OF_LINES = "maximumNumberOfLines"; - public static final String ELLIPSIZE_MODE = "ellipsizeMode"; - public static final String ADJUSTS_FONT_SIZE_TO_FIT = "adjustsFontSizeToFit"; - public static final String MINIMUM_FONT_SCALE = "minimumFontScale"; -diff --git a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactBaseTextShadowNode.java b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactBaseTextShadowNode.java -index b5811c7..96eef96 100644 ---- a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactBaseTextShadowNode.java -+++ b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactBaseTextShadowNode.java -@@ -303,6 +303,7 @@ public abstract class ReactBaseTextShadowNode extends LayoutShadowNode { - protected boolean mIsAccessibilityLink = false; - - protected int mNumberOfLines = UNSET; -+ protected int mMaxNumberOfLines = UNSET; - protected int mTextAlign = Gravity.NO_GRAVITY; - protected int mTextBreakStrategy = - (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) ? 0 : Layout.BREAK_STRATEGY_HIGH_QUALITY; -@@ -387,6 +388,12 @@ public abstract class ReactBaseTextShadowNode extends LayoutShadowNode { - markUpdated(); - } - -+ @ReactProp(name = ViewProps.MAXIMUM_NUMBER_OF_LINES, defaultInt = UNSET) -+ public void setMaxNumberOfLines(int numberOfLines) { -+ mMaxNumberOfLines = numberOfLines == 0 ? UNSET : numberOfLines; -+ markUpdated(); -+ } -+ - @ReactProp(name = ViewProps.LINE_HEIGHT, defaultFloat = Float.NaN) - public void setLineHeight(float lineHeight) { - mTextAttributes.setLineHeight(lineHeight); -diff --git a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextAnchorViewManager.java b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextAnchorViewManager.java -index 7b5d0c1..c3032eb 100644 ---- a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextAnchorViewManager.java -+++ b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextAnchorViewManager.java -@@ -49,8 +49,8 @@ public abstract class ReactTextAnchorViewManager minimumFontSize -- && (mNumberOfLines != UNSET && layout.getLineCount() > mNumberOfLines -+ && (mMaxNumberOfLines != UNSET && layout.getLineCount() > mMaxNumberOfLines - || heightMode != YogaMeasureMode.UNDEFINED && layout.getHeight() > height)) { - // TODO: We could probably use a smarter algorithm here. This will require 0(n) - // measurements -@@ -124,9 +124,9 @@ public class ReactTextShadowNode extends ReactBaseTextShadowNode { - } - - final int lineCount = -- mNumberOfLines == UNSET -+ mMaxNumberOfLines == UNSET - ? layout.getLineCount() -- : Math.min(mNumberOfLines, layout.getLineCount()); -+ : Math.min(mMaxNumberOfLines, layout.getLineCount()); - - // Instead of using `layout.getWidth()` (which may yield a significantly larger width for - // text that is wrapping), compute width using the longest line. -diff --git a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextView.java b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextView.java -index 190bc27..c2bcdc1 100644 ---- a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextView.java -+++ b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextView.java -@@ -87,7 +87,7 @@ public class ReactTextView extends AppCompatTextView implements ReactCompoundVie - - mReactBackgroundManager = new ReactViewBackgroundManager(this); - -- mNumberOfLines = ViewDefaults.NUMBER_OF_LINES; -+ mNumberOfLines = ViewDefaults.MAXIMUM_NUMBER_OF_LINES; - mAdjustsFontSizeToFit = false; - mLinkifyMaskType = 0; - mNotifyOnInlineViewLayout = false; -@@ -576,7 +576,7 @@ public class ReactTextView extends AppCompatTextView implements ReactCompoundVie - } - - public void setNumberOfLines(int numberOfLines) { -- mNumberOfLines = numberOfLines == 0 ? ViewDefaults.NUMBER_OF_LINES : numberOfLines; -+ mNumberOfLines = numberOfLines == 0 ? ViewDefaults.MAXIMUM_NUMBER_OF_LINES : numberOfLines; - setSingleLine(mNumberOfLines == 1); - setMaxLines(mNumberOfLines); - } -@@ -596,7 +596,7 @@ public class ReactTextView extends AppCompatTextView implements ReactCompoundVie - public void updateView() { - @Nullable - TextUtils.TruncateAt ellipsizeLocation = -- mNumberOfLines == ViewDefaults.NUMBER_OF_LINES || mAdjustsFontSizeToFit -+ mNumberOfLines == ViewDefaults.MAXIMUM_NUMBER_OF_LINES || mAdjustsFontSizeToFit - ? null - : mEllipsizeLocation; - setEllipsize(ellipsizeLocation); -diff --git a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManager.java b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManager.java -index 561a2d0..9409cfc 100644 ---- a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManager.java -+++ b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManager.java -@@ -18,6 +18,7 @@ import android.text.SpannableStringBuilder; - import android.text.Spanned; - import android.text.StaticLayout; - import android.text.TextPaint; -+import android.text.TextUtils; - import android.util.LayoutDirection; - import android.util.LruCache; - import android.view.View; -@@ -65,6 +66,7 @@ public class TextLayoutManager { - private static final String TEXT_BREAK_STRATEGY_KEY = "textBreakStrategy"; - private static final String HYPHENATION_FREQUENCY_KEY = "android_hyphenationFrequency"; - private static final String MAXIMUM_NUMBER_OF_LINES_KEY = "maximumNumberOfLines"; -+ private static final String NUMBER_OF_LINES_KEY = "numberOfLines"; - private static final LruCache sSpannableCache = - new LruCache<>(spannableCacheSize); - private static final ConcurrentHashMap sTagToSpannableCache = -@@ -385,6 +387,48 @@ public class TextLayoutManager { - ? paragraphAttributes.getInt(MAXIMUM_NUMBER_OF_LINES_KEY) - : UNSET; - -+ int numberOfLines = -+ paragraphAttributes.hasKey(NUMBER_OF_LINES_KEY) -+ ? paragraphAttributes.getInt(NUMBER_OF_LINES_KEY) -+ : UNSET; -+ -+ int lines = layout.getLineCount(); -+ if (numberOfLines != UNSET && numberOfLines != 0 && numberOfLines >= lines && text.length() > 0) { -+ int numberOfEmptyLines = numberOfLines - lines; -+ SpannableStringBuilder ssb = new SpannableStringBuilder(); -+ -+ // for some reason a newline on end causes issues with computing height so we add a character -+ if (text.toString().endsWith("\n")) { -+ ssb.append("A"); -+ } -+ -+ for (int i = 0; i < numberOfEmptyLines; ++i) { -+ ssb.append("\nA"); -+ } -+ -+ Object[] spans = text.getSpans(0, 0, Object.class); -+ for (Object span : spans) { // It's possible we need to set exl-exl -+ ssb.setSpan(span, 0, ssb.length(), text.getSpanFlags(span)); -+ }; -+ -+ text = new SpannableStringBuilder(TextUtils.concat(text, ssb)); -+ boring = null; -+ layout = createLayout( -+ text, -+ boring, -+ width, -+ widthYogaMeasureMode, -+ includeFontPadding, -+ textBreakStrategy, -+ hyphenationFrequency); -+ } -+ -+ -+ if (numberOfLines != UNSET && numberOfLines != 0) { -+ maximumNumberOfLines = numberOfLines; -+ } -+ -+ - int calculatedLineCount = - maximumNumberOfLines == UNSET || maximumNumberOfLines == 0 - ? layout.getLineCount() -diff --git a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManagerMapBuffer.java b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManagerMapBuffer.java -index 0d118f0..0ae44b7 100644 ---- a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManagerMapBuffer.java -+++ b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManagerMapBuffer.java -@@ -18,6 +18,7 @@ import android.text.SpannableStringBuilder; - import android.text.Spanned; - import android.text.StaticLayout; - import android.text.TextPaint; -+import android.text.TextUtils; - import android.util.LayoutDirection; - import android.util.LruCache; - import android.view.View; -@@ -61,6 +62,7 @@ public class TextLayoutManagerMapBuffer { - public static final short PA_KEY_ADJUST_FONT_SIZE_TO_FIT = 3; - public static final short PA_KEY_INCLUDE_FONT_PADDING = 4; - public static final short PA_KEY_HYPHENATION_FREQUENCY = 5; -+ public static final short PA_KEY_NUMBER_OF_LINES = 6; - - private static final boolean ENABLE_MEASURE_LOGGING = ReactBuildConfig.DEBUG && false; - -@@ -399,6 +401,47 @@ public class TextLayoutManagerMapBuffer { - ? paragraphAttributes.getInt(PA_KEY_MAX_NUMBER_OF_LINES) - : UNSET; - -+ int numberOfLines = -+ paragraphAttributes.contains(PA_KEY_NUMBER_OF_LINES) -+ ? paragraphAttributes.getInt(PA_KEY_NUMBER_OF_LINES) -+ : UNSET; -+ -+ int lines = layout.getLineCount(); -+ if (numberOfLines != UNSET && numberOfLines != 0 && numberOfLines > lines && text.length() > 0) { -+ int numberOfEmptyLines = numberOfLines - lines; -+ SpannableStringBuilder ssb = new SpannableStringBuilder(); -+ -+ // for some reason a newline on end causes issues with computing height so we add a character -+ if (text.toString().endsWith("\n")) { -+ ssb.append("A"); -+ } -+ -+ for (int i = 0; i < numberOfEmptyLines; ++i) { -+ ssb.append("\nA"); -+ } -+ -+ Object[] spans = text.getSpans(0, 0, Object.class); -+ for (Object span : spans) { // It's possible we need to set exl-exl -+ ssb.setSpan(span, 0, ssb.length(), text.getSpanFlags(span)); -+ }; -+ -+ text = new SpannableStringBuilder(TextUtils.concat(text, ssb)); -+ boring = null; -+ layout = createLayout( -+ text, -+ boring, -+ width, -+ widthYogaMeasureMode, -+ includeFontPadding, -+ textBreakStrategy, -+ hyphenationFrequency); -+ } -+ -+ if (numberOfLines != UNSET && numberOfLines != 0) { -+ maximumNumberOfLines = numberOfLines; -+ } -+ -+ - int calculatedLineCount = - maximumNumberOfLines == UNSET || maximumNumberOfLines == 0 - ? layout.getLineCount() -diff --git a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.java b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.java -index ced37be..ef2f321 100644 ---- a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.java -+++ b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.java -@@ -548,7 +548,13 @@ public class ReactEditText extends AppCompatEditText - * href='https://android.googlesource.com/platform/frameworks/base/+/jb-release/core/java/android/widget/TextView.java'>TextView.java} - */ - if (isMultiline()) { -+ // we save max lines as setSingleLines overwrites it -+ // https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/widget/TextView.java#10671 -+ int maxLines = getMaxLines(); - setSingleLine(false); -+ if (maxLines != -1) { -+ setMaxLines(maxLines); -+ } - } - - // We override the KeyListener so that all keys on the soft input keyboard as well as hardware -diff --git a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputLocalData.java b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputLocalData.java -index a850510..c59be1d 100644 ---- a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputLocalData.java -+++ b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputLocalData.java -@@ -41,9 +41,9 @@ public final class ReactTextInputLocalData { - public void apply(EditText editText) { - editText.setText(mText); - editText.setTextSize(TypedValue.COMPLEX_UNIT_PX, mTextSize); -+ editText.setInputType(mInputType); - editText.setMinLines(mMinLines); - editText.setMaxLines(mMaxLines); -- editText.setInputType(mInputType); - editText.setHint(mPlaceholder); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - editText.setBreakStrategy(mBreakStrategy); -diff --git a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java -index b27ace4..c6a2d63 100644 ---- a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java -+++ b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java -@@ -737,9 +737,18 @@ public class ReactTextInputManager extends BaseViewManager= Build.VERSION_CODES.M -diff --git a/node_modules/react-native/ReactCommon/react/renderer/attributedstring/ParagraphAttributes.cpp b/node_modules/react-native/ReactCommon/react/renderer/attributedstring/ParagraphAttributes.cpp -index 2994aca..fff0d5e 100644 ---- a/node_modules/react-native/ReactCommon/react/renderer/attributedstring/ParagraphAttributes.cpp -+++ b/node_modules/react-native/ReactCommon/react/renderer/attributedstring/ParagraphAttributes.cpp -@@ -16,6 +16,7 @@ namespace facebook::react { - - bool ParagraphAttributes::operator==(const ParagraphAttributes &rhs) const { - return std::tie( -+ numberOfLines, - maximumNumberOfLines, - ellipsizeMode, - textBreakStrategy, -@@ -23,6 +24,7 @@ bool ParagraphAttributes::operator==(const ParagraphAttributes &rhs) const { - includeFontPadding, - android_hyphenationFrequency) == - std::tie( -+ rhs.numberOfLines, - rhs.maximumNumberOfLines, - rhs.ellipsizeMode, - rhs.textBreakStrategy, -@@ -42,6 +44,7 @@ bool ParagraphAttributes::operator!=(const ParagraphAttributes &rhs) const { - #if RN_DEBUG_STRING_CONVERTIBLE - SharedDebugStringConvertibleList ParagraphAttributes::getDebugProps() const { - return { -+ debugStringConvertibleItem("numberOfLines", numberOfLines), - debugStringConvertibleItem("maximumNumberOfLines", maximumNumberOfLines), - debugStringConvertibleItem("ellipsizeMode", ellipsizeMode), - debugStringConvertibleItem("textBreakStrategy", textBreakStrategy), -diff --git a/node_modules/react-native/ReactCommon/react/renderer/attributedstring/ParagraphAttributes.h b/node_modules/react-native/ReactCommon/react/renderer/attributedstring/ParagraphAttributes.h -index f5f87c6..b7d1e90 100644 ---- a/node_modules/react-native/ReactCommon/react/renderer/attributedstring/ParagraphAttributes.h -+++ b/node_modules/react-native/ReactCommon/react/renderer/attributedstring/ParagraphAttributes.h -@@ -30,6 +30,11 @@ class ParagraphAttributes : public DebugStringConvertible { - public: - #pragma mark - Fields - -+ /* -+ * Number of lines which paragraph takes. -+ */ -+ int numberOfLines{}; -+ - /* - * Maximum number of lines which paragraph can take. - * Zero value represents "no limit". -@@ -92,6 +97,7 @@ struct hash { - const facebook::react::ParagraphAttributes &attributes) const { - return folly::hash::hash_combine( - 0, -+ attributes.numberOfLines, - attributes.maximumNumberOfLines, - attributes.ellipsizeMode, - attributes.textBreakStrategy, -diff --git a/node_modules/react-native/ReactCommon/react/renderer/attributedstring/conversions.h b/node_modules/react-native/ReactCommon/react/renderer/attributedstring/conversions.h -index 8687b89..eab75f4 100644 ---- a/node_modules/react-native/ReactCommon/react/renderer/attributedstring/conversions.h -+++ b/node_modules/react-native/ReactCommon/react/renderer/attributedstring/conversions.h -@@ -835,10 +835,16 @@ inline ParagraphAttributes convertRawProp( - ParagraphAttributes const &defaultParagraphAttributes) { - auto paragraphAttributes = ParagraphAttributes{}; - -- paragraphAttributes.maximumNumberOfLines = convertRawProp( -+ paragraphAttributes.numberOfLines = convertRawProp( - context, - rawProps, - "numberOfLines", -+ sourceParagraphAttributes.numberOfLines, -+ defaultParagraphAttributes.numberOfLines); -+ paragraphAttributes.maximumNumberOfLines = convertRawProp( -+ context, -+ rawProps, -+ "maximumNumberOfLines", - sourceParagraphAttributes.maximumNumberOfLines, - defaultParagraphAttributes.maximumNumberOfLines); - paragraphAttributes.ellipsizeMode = convertRawProp( -@@ -913,6 +919,7 @@ inline std::string toString(AttributedString::Range const &range) { - inline folly::dynamic toDynamic( - const ParagraphAttributes ¶graphAttributes) { - auto values = folly::dynamic::object(); -+ values("numberOfLines", paragraphAttributes.numberOfLines); - values("maximumNumberOfLines", paragraphAttributes.maximumNumberOfLines); - values("ellipsizeMode", toString(paragraphAttributes.ellipsizeMode)); - values("textBreakStrategy", toString(paragraphAttributes.textBreakStrategy)); -@@ -1118,6 +1125,7 @@ constexpr static MapBuffer::Key PA_KEY_TEXT_BREAK_STRATEGY = 2; - constexpr static MapBuffer::Key PA_KEY_ADJUST_FONT_SIZE_TO_FIT = 3; - constexpr static MapBuffer::Key PA_KEY_INCLUDE_FONT_PADDING = 4; - constexpr static MapBuffer::Key PA_KEY_HYPHENATION_FREQUENCY = 5; -+constexpr static MapBuffer::Key PA_KEY_NUMBER_OF_LINES = 6; - - inline MapBuffer toMapBuffer(const ParagraphAttributes ¶graphAttributes) { - auto builder = MapBufferBuilder(); -@@ -1135,6 +1143,8 @@ inline MapBuffer toMapBuffer(const ParagraphAttributes ¶graphAttributes) { - builder.putString( - PA_KEY_HYPHENATION_FREQUENCY, - toString(paragraphAttributes.android_hyphenationFrequency)); -+ builder.putInt( -+ PA_KEY_NUMBER_OF_LINES, paragraphAttributes.numberOfLines); - - return builder.build(); - } -diff --git a/node_modules/react-native/ReactCommon/react/renderer/components/textinput/androidtextinput/react/renderer/components/androidtextinput/AndroidTextInputProps.cpp b/node_modules/react-native/ReactCommon/react/renderer/components/textinput/androidtextinput/react/renderer/components/androidtextinput/AndroidTextInputProps.cpp -index 9953e22..98eb3da 100644 ---- a/node_modules/react-native/ReactCommon/react/renderer/components/textinput/androidtextinput/react/renderer/components/androidtextinput/AndroidTextInputProps.cpp -+++ b/node_modules/react-native/ReactCommon/react/renderer/components/textinput/androidtextinput/react/renderer/components/androidtextinput/AndroidTextInputProps.cpp -@@ -56,6 +56,10 @@ AndroidTextInputProps::AndroidTextInputProps( - "numberOfLines", - sourceProps.numberOfLines, - {0})), -+ maximumNumberOfLines(CoreFeatures::enablePropIteratorSetter? sourceProps.maximumNumberOfLines : convertRawProp(context, rawProps, -+ "maximumNumberOfLines", -+ sourceProps.maximumNumberOfLines, -+ {0})), - disableFullscreenUI(CoreFeatures::enablePropIteratorSetter? sourceProps.disableFullscreenUI : convertRawProp(context, rawProps, - "disableFullscreenUI", - sourceProps.disableFullscreenUI, -@@ -281,6 +285,12 @@ void AndroidTextInputProps::setProp( - value, - paragraphAttributes, - maximumNumberOfLines, -+ "maximumNumberOfLines"); -+ REBUILD_FIELD_SWITCH_CASE( -+ paDefaults, -+ value, -+ paragraphAttributes, -+ numberOfLines, - "numberOfLines"); - REBUILD_FIELD_SWITCH_CASE( - paDefaults, value, paragraphAttributes, ellipsizeMode, "ellipsizeMode"); -@@ -323,6 +333,7 @@ void AndroidTextInputProps::setProp( - } - - switch (hash) { -+ RAW_SET_PROP_SWITCH_CASE_BASIC(maximumNumberOfLines); - RAW_SET_PROP_SWITCH_CASE_BASIC(autoComplete); - RAW_SET_PROP_SWITCH_CASE_BASIC(returnKeyLabel); - RAW_SET_PROP_SWITCH_CASE_BASIC(numberOfLines); -@@ -422,6 +433,7 @@ void AndroidTextInputProps::setProp( - // TODO T53300085: support this in codegen; this was hand-written - folly::dynamic AndroidTextInputProps::getDynamic() const { - folly::dynamic props = folly::dynamic::object(); -+ props["maximumNumberOfLines"] = maximumNumberOfLines; - props["autoComplete"] = autoComplete; - props["returnKeyLabel"] = returnKeyLabel; - props["numberOfLines"] = numberOfLines; -diff --git a/node_modules/react-native/ReactCommon/react/renderer/components/textinput/androidtextinput/react/renderer/components/androidtextinput/AndroidTextInputProps.h b/node_modules/react-native/ReactCommon/react/renderer/components/textinput/androidtextinput/react/renderer/components/androidtextinput/AndroidTextInputProps.h -index ba39ebb..ead28e3 100644 ---- a/node_modules/react-native/ReactCommon/react/renderer/components/textinput/androidtextinput/react/renderer/components/androidtextinput/AndroidTextInputProps.h -+++ b/node_modules/react-native/ReactCommon/react/renderer/components/textinput/androidtextinput/react/renderer/components/androidtextinput/AndroidTextInputProps.h -@@ -84,6 +84,7 @@ class AndroidTextInputProps final : public ViewProps, public BaseTextProps { - std::string autoComplete{}; - std::string returnKeyLabel{}; - int numberOfLines{0}; -+ int maximumNumberOfLines{0}; - bool disableFullscreenUI{false}; - std::string textBreakStrategy{}; - SharedColor underlineColorAndroid{}; -diff --git a/node_modules/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.mm b/node_modules/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.mm -index 368c334..a1bb33e 100644 ---- a/node_modules/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.mm -+++ b/node_modules/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.mm -@@ -244,26 +244,51 @@ - (void)getRectWithAttributedString:(AttributedString)attributedString - - #pragma mark - Private - --- (NSTextStorage *)_textStorageForNSAttributesString:(NSAttributedString *)attributedString -++- (NSTextStorage *)_textStorageForNSAttributesString:(NSAttributedString *)inputAttributedString - paragraphAttributes:(ParagraphAttributes)paragraphAttributes - size:(CGSize)size - { -- NSTextContainer *textContainer = [[NSTextContainer alloc] initWithSize:size]; -+ NSMutableAttributedString *attributedString = [ inputAttributedString mutableCopy]; -+ -+ /* -+ * The block below is responsible for setting the exact height of the view in lines -+ * Unfortunatelly, iOS doesn't export any easy way to do it. So we set maximumNumberOfLines -+ * prop and then add random lines at the front. However, they are only used for layout -+ * so they are not visible on the screen. This method is used for drawing only for Paragraph component -+ * but we set exact height in lines only on TextInput that doesn't use it. -+ */ -+ if (paragraphAttributes.numberOfLines) { -+ paragraphAttributes.maximumNumberOfLines = paragraphAttributes.numberOfLines; -+ NSMutableString *newLines = [NSMutableString stringWithCapacity: paragraphAttributes.numberOfLines]; -+ for (NSUInteger i = 0UL; i < paragraphAttributes.numberOfLines; ++i) { -+ // K is added on purpose. New line seems to be not enough for NTtextContainer -+ [newLines appendString:@"K\n"]; -+ } -+ NSDictionary * attributesOfFirstCharacter = [inputAttributedString attributesAtIndex:0 effectiveRange:NULL]; - -- textContainer.lineFragmentPadding = 0.0; // Note, the default value is 5. -- textContainer.lineBreakMode = paragraphAttributes.maximumNumberOfLines > 0 -- ? RCTNSLineBreakModeFromEllipsizeMode(paragraphAttributes.ellipsizeMode) -- : NSLineBreakByClipping; -- textContainer.maximumNumberOfLines = paragraphAttributes.maximumNumberOfLines; -+ [attributedString insertAttributedString:[[NSAttributedString alloc] initWithString:newLines attributes:attributesOfFirstCharacter] atIndex:0]; -+ } -+ -+ NSTextContainer *textContainer = [NSTextContainer new]; - - NSLayoutManager *layoutManager = [NSLayoutManager new]; - layoutManager.usesFontLeading = NO; - [layoutManager addTextContainer:textContainer]; - -- NSTextStorage *textStorage = [[NSTextStorage alloc] initWithAttributedString:attributedString]; -+ NSTextStorage *textStorage = [NSTextStorage new]; - - [textStorage addLayoutManager:layoutManager]; - -+ textContainer.lineFragmentPadding = 0.0; // Note, the default value is 5. -+ textContainer.lineBreakMode = paragraphAttributes.maximumNumberOfLines > 0 -+ ? RCTNSLineBreakModeFromEllipsizeMode(paragraphAttributes.ellipsizeMode) -+ : NSLineBreakByClipping; -+ textContainer.size = size; -+ textContainer.maximumNumberOfLines = paragraphAttributes.maximumNumberOfLines; -+ -+ [textStorage replaceCharactersInRange:(NSRange){0, textStorage.length} withAttributedString:attributedString]; -+ -+ - if (paragraphAttributes.adjustsFontSizeToFit) { - CGFloat minimumFontSize = !isnan(paragraphAttributes.minimumFontSize) ? paragraphAttributes.minimumFontSize : 4.0; - CGFloat maximumFontSize = !isnan(paragraphAttributes.maximumFontSize) ? paragraphAttributes.maximumFontSize : 96.0; From 1cc23caf58b02347f67b3bb8e16294f5137eda25 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Tue, 5 Dec 2023 14:07:23 +0100 Subject: [PATCH 034/391] fix: types --- src/libs/OptionsListUtils.ts | 213 +++++++++++++++++++---------------- src/libs/ReportUtils.ts | 3 +- src/types/onyx/Report.ts | 1 + 3 files changed, 121 insertions(+), 96 deletions(-) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index fe88a44ba911..3461212fee50 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -3,6 +3,7 @@ import {parsePhoneNumber} from 'awesome-phonenumber'; import Str from 'expensify-common/lib/str'; import lodashOrderBy from 'lodash/orderBy'; import lodashSet from 'lodash/set'; +import lodashTimes from 'lodash/times'; import Onyx, {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import {ValueOf} from 'type-fest'; import CONST from '@src/CONST'; @@ -29,6 +30,8 @@ type PersonalDetailsCollection = Record = {}) { +function getAvatarsForAccountIDs(accountIDs: number[], personalDetails: PersonalDetailsCollection, defaultValues: Record = {}): OnyxCommon.Icon[] { const reversedDefaultValues: Record = {}; Object.entries(defaultValues).forEach((item) => { @@ -142,7 +145,7 @@ function getAvatarsForAccountIDs(accountIDs: number[], personalDetails: Personal id: accountID, source: UserUtils.getAvatar(userPersonalDetail.avatar, userPersonalDetail.accountID), type: CONST.ICON_TYPE_AVATAR, - name: userPersonalDetail.login, + name: userPersonalDetail.login ?? '', }; }); } @@ -151,7 +154,7 @@ function getAvatarsForAccountIDs(accountIDs: number[], personalDetails: Personal * Returns the personal details for an array of accountIDs * @returns keys of the object are emails, values are PersonalDetails objects. */ -function getPersonalDetailsForAccountIDs(accountIDs: number[], personalDetails: PersonalDetailsCollection) { +function getPersonalDetailsForAccountIDs(accountIDs: number[], personalDetails: PersonalDetailsCollection): Record> { const personalDetailsForAccountIDs: Record> = {}; if (!personalDetails) { return personalDetailsForAccountIDs; @@ -189,14 +192,14 @@ function isPersonalDetailsReady(personalDetails: PersonalDetailsCollection): boo /** * Get the participant option for a report. */ -function getParticipantsOption(participant: Participant & {searchText?: string}, personalDetails: PersonalDetailsCollection) { +function getParticipantsOption(participant: ReportUtils.Participant & {searchText?: string}, personalDetails: PersonalDetailsCollection): ReportUtils.Participant { const detail = getPersonalDetailsForAccountIDs([participant.accountID], personalDetails)[participant.accountID]; const login = detail.login ?? participant.login ?? ''; const displayName = detail.displayName ?? LocalePhoneNumber.formatPhoneNumber(login); return { keyForList: String(detail.accountID), login, - accountID: detail.accountID, + accountID: detail.accountID ?? 0, text: displayName, firstName: detail.firstName ?? '', lastName: detail.lastName ?? '', @@ -313,7 +316,7 @@ function getSearchText(report: Report, reportName: string, personalDetailList: A /** * Get an object of error messages keyed by microtime by combining all error objects related to the report. */ -function getAllReportErrors(report: OnyxEntry, reportActions: OnyxEntry) { +function getAllReportErrors(report: OnyxEntry, reportActions: OnyxEntry): OnyxCommon.Errors { const reportErrors = report?.errors ?? {}; const reportErrorFields = report?.errorFields ?? {}; const reportActionErrors: OnyxCommon.Errors = Object.values(reportActions ?? {}).reduce( @@ -354,7 +357,7 @@ function getAllReportErrors(report: OnyxEntry, reportActions: OnyxEntry< /** * Get the last message text from the report directly or from other sources for special cases. */ -function getLastMessageTextForReport(report: OnyxEntry) { +function getLastMessageTextForReport(report: OnyxEntry): string { const lastReportAction = allSortedReportActions[report?.reportID ?? '']?.find((reportAction) => ReportActionUtils.shouldReportActionBeVisibleAsLastAction(reportAction)); let lastMessageTextFromReport = ''; const lastActionName = lastReportAction?.actionName ?? ''; @@ -403,7 +406,7 @@ function createOption( report: OnyxEntry, reportActions: Record, {showChatPreviewLine = false, forcePolicyNamePreview = false}: {showChatPreviewLine?: boolean; forcePolicyNamePreview?: boolean}, -) { +): ReportUtils.OptionData { const result: ReportUtils.OptionData = { text: undefined, alternateText: null, @@ -529,7 +532,7 @@ function createOption( /** * Get the option for a policy expense report. */ -function getPolicyExpenseReportOption(report: Report & {selected?: boolean; searchText?: string}) { +function getPolicyExpenseReportOption(report: Report): ReportUtils.OptionData { const expenseReport = policyExpenseReports?.[`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`]; const option = createOption( @@ -603,17 +606,28 @@ function hasEnabledOptions(options: Record): boolean { return Object.values(options).some((option) => option.enabled); } +type Category = { + name: string; + enabled: boolean; +}; + +type DynamicKey = { + [K in Key]?: Hierarchy | undefined; +}; + +type Hierarchy = Record>; + /** * Sorts categories using a simple object. * It builds an hierarchy (based on an object), where each category has a name and other keys as subcategories. * Via the hierarchy we avoid duplicating and sort categories one by one. Subcategories are being sorted alphabetically. */ -function sortCategories(categories: Record) { +function sortCategories(categories: Record): Category[] { // Sorts categories alphabetically by name. const sortedCategories = Object.values(categories).sort((a, b) => a.name.localeCompare(b.name)); // An object that respects nesting of categories. Also, can contain only uniq categories. - const hierarchy = {}; + const hierarchy: Hierarchy = {}; /** * Iterates over all categories to set each category in a proper place in hierarchy @@ -639,32 +653,28 @@ function sortCategories(categories: Record) { name: category.name, }); }); - console.log(sortedCategories, hierarchy); + /** * A recursive function to convert hierarchy into an array of category objects. * The category object contains base 2 properties: "name" and "enabled". * It iterates each key one by one. When a category has subcategories, goes deeper into them. Also, sorts subcategories alphabetically. - * - * @param {Object} initialHierarchy - * @returns {Array} */ - const flatHierarchy = (initialHierarchy) => - initialHierarchy.reduce((acc, category) => { + const flatHierarchy = (initialHierarchy: Hierarchy) => + Object.values(initialHierarchy).reduce((acc: Category[], category) => { const {name, ...subcategories} = category; - - if (!_.isEmpty(name)) { + if (name) { const categoryObject = { name, - enabled: lodashGet(categories, [name, 'enabled'], false), + enabled: categories.name.enabled ?? false, }; acc.push(categoryObject); } - if (!_.isEmpty(subcategories)) { + if (isNotEmptyObject(subcategories)) { const nestedCategories = flatHierarchy(subcategories); - acc.push(..._.sortBy(nestedCategories, 'name')); + acc.push(...nestedCategories.sort((a, b) => a.name.localeCompare(b.name))); } return acc; @@ -675,15 +685,15 @@ function sortCategories(categories: Record) { /** * Sorts tags alphabetically by name. - * - * @param {Object} tags - * @returns {Array} */ -function sortTags(tags) { - const sortedTags = _.chain(tags) - .values() - .sortBy((tag) => tag.name) - .value(); +function sortTags(tags: Record | Tag[]) { + let sortedTags; + + if (Array.isArray(tags)) { + sortedTags = tags.sort((a, b) => a.name.localeCompare(b.name)); + } else { + sortedTags = Object.values(tags).sort((a, b) => a.name.localeCompare(b.name)); + } return sortedTags; } @@ -691,16 +701,13 @@ function sortTags(tags) { /** * Builds the options for the 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} + * @param options - an initial object array + * @param} [isOneLine] - a flag to determine if text should be one line */ -function getIndentedOptionTree(options, isOneLine = false) { - const optionCollection = new Map(); +function getIndentedOptionTree(options: Category[], isOneLine = false): Option[] { + const optionCollection = new Map(); - _.each(options, (option) => { + options.forEach((option) => { if (isOneLine) { if (optionCollection.has(option.name)) { return; @@ -718,7 +725,7 @@ function getIndentedOptionTree(options, isOneLine = false) { } option.name.split(CONST.PARENT_CHILD_SEPARATOR).forEach((optionName, index, array) => { - const indents = _.times(index, () => CONST.INDENTS).join(''); + const indents = lodashTimes(index, () => CONST.INDENTS).join(''); const isChild = array.length - 1 === index; const searchText = array.slice(0, index + 1).join(CONST.PARENT_CHILD_SEPARATOR); @@ -738,24 +745,23 @@ function getIndentedOptionTree(options, isOneLine = false) { return Array.from(optionCollection.values()); } +type CategorySection = {title: string; shouldShow: boolean; indexOffset: number; data: Option[]}; /** * Builds 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: Category[], + searchInputValue: string, + maxRecentReportsToShow: number, +): CategorySection[] { const sortedCategories = sortCategories(categories); - const enabledCategories = _.filter(sortedCategories, (category) => category.enabled); + const enabledCategories = Object.values(sortedCategories).filter((category) => category.enabled); - const categorySections = []; - const numberOfCategories = _.size(enabledCategories); + const categorySections: CategorySection[] = []; + const numberOfCategories = enabledCategories.length; let indexOffset = 0; @@ -771,8 +777,8 @@ function getCategoryListSections(categories, recentlyUsedCategories, selectedOpt return categorySections; } - if (!_.isEmpty(searchInputValue)) { - const searchCategories = _.filter(enabledCategories, (category) => category.name.toLowerCase().includes(searchInputValue.toLowerCase())); + if (searchInputValue) { + const searchCategories = enabledCategories.filter((category) => category.name.toLowerCase().includes(searchInputValue.toLowerCase())); categorySections.push({ // "Search" section @@ -797,7 +803,7 @@ function getCategoryListSections(categories, recentlyUsedCategories, selectedOpt return categorySections; } - if (!_.isEmpty(selectedOptions)) { + if (selectedOptions.length > 0) { categorySections.push({ // "Selected" section title: '', @@ -809,16 +815,15 @@ function getCategoryListSections(categories, recentlyUsedCategories, selectedOpt indexOffset += selectedOptions.length; } - const selectedOptionNames = _.map(selectedOptions, (selectedOption) => selectedOption.name); - const filteredRecentlyUsedCategories = _.chain(recentlyUsedCategories) - .filter((categoryName) => !_.includes(selectedOptionNames, categoryName) && lodashGet(categories, [categoryName, 'enabled'], false)) + const selectedOptionNames = selectedOptions.map((selectedOption) => selectedOption.name); + const filteredRecentlyUsedCategories = recentlyUsedCategories + .filter((categoryName) => !selectedOptionNames.includes(categoryName) && (categories[categoryName].enabled ?? false)) .map((categoryName) => ({ name: categoryName, - enabled: lodashGet(categories, [categoryName, 'enabled'], false), - })) - .value(); + enabled: categories[categoryName].enabled ?? false, + })); - if (!_.isEmpty(filteredRecentlyUsedCategories)) { + if (filteredRecentlyUsedCategories.length > 0) { const cutRecentlyUsedCategories = filteredRecentlyUsedCategories.slice(0, maxRecentReportsToShow); categorySections.push({ @@ -832,7 +837,7 @@ function getCategoryListSections(categories, recentlyUsedCategories, selectedOpt indexOffset += filteredRecentlyUsedCategories.length; } - const filteredCategories = _.filter(enabledCategories, (category) => !_.includes(selectedOptionNames, category.name)); + const filteredCategories = enabledCategories.filter((category) => !selectedOptionNames.includes(category.name)); categorySections.push({ // "All" section when items amount more than the threshold @@ -854,28 +859,18 @@ function getTagsOptions(tags: Tag[]) { /** * Build the section list for tags - * - * @param {Object[]} rawTags - * @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(rawTags, recentlyUsedTags, selectedOptions, searchInputValue, maxRecentReportsToShow) { +function getTagListSections(rawTags: Tag[], recentlyUsedTags: string[], selectedOptions: Category[], searchInputValue: string, maxRecentReportsToShow: number) { const tagSections = []; - const tags = _.map(rawTags, (tag) => { + const tags = rawTags.map((tag) => { // This is to remove unnecessary escaping backslash in tag name sent from backend. - const tagName = tag.name && tag.name.replace(/\\{1,2}:/g, ':'); + const tagName = tag.name?.replace(/\\{1,2}:/g, ':'); return {...tag, name: tagName}; }); const sortedTags = sortTags(tags); - const enabledTags = _.filter(sortedTags, (tag) => tag.enabled); - const numberOfTags = _.size(enabledTags); + const enabledTags = sortedTags.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 @@ -951,7 +946,7 @@ function getTagListSections(rawTags, recentlyUsedTags, selectedOptions, searchIn indexOffset += selectedOptions.length; } - if (!_.isEmpty(filteredRecentlyUsedTags)) { + if (filteredRecentlyUsedTags.length > 0) { const cutRecentlyUsedTags = filteredRecentlyUsedTags.slice(0, maxRecentReportsToShow); tagSections.push({ @@ -976,6 +971,35 @@ function getTagListSections(rawTags, recentlyUsedTags, selectedOptions, searchIn return tagSections; } +type GetOptionsConfig = { + reportActions?: Record; + betas?: Beta[]; + selectedOptions?: Category[]; + maxRecentReportsToShow?: number; + excludeLogins?: string[]; + includeMultipleParticipantReports?: boolean; + includePersonalDetails?: boolean; + includeRecentReports?: boolean; + 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?: string[]; + includeTags?: boolean; + tags?: Record; + recentlyUsedTags?: string[]; + canInviteUser?: boolean; + includeSelectedOptions?: boolean; +}; /** * Build the options */ @@ -1011,7 +1035,7 @@ function getOptions( recentlyUsedTags = [], canInviteUser = true, includeSelectedOptions = false, - }, + }: GetOptionsConfig, ) { if (includeCategories) { const categoryOptions = getCategoryListSections(categories, recentlyUsedCategories, selectedOptions, searchInputValue, maxRecentReportsToShow); @@ -1051,13 +1075,13 @@ function getOptions( } let recentReportOptions = []; - let personalDetailsOptions: Option[] = []; + let personalDetailsOptions: ReportUtils.OptionData[] = []; const reportMapForAccountIDs: Record = {}; const parsedPhoneNumber = parsePhoneNumber(LoginUtils.appendCountryCode(Str.removeSMSDomain(searchInputValue))); const searchValue = parsedPhoneNumber.possible ? parsedPhoneNumber.number?.e164 : searchInputValue.toLowerCase(); // Filter out all the reports that shouldn't be displayed - const filteredReports = Object.values(reports).filter((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) @@ -1071,7 +1095,7 @@ function getOptions( }); orderedReports.reverse(); - const allReportOptions: Option[] = []; + const allReportOptions: ReportUtils.OptionData[] = []; orderedReports.forEach((report) => { if (!report) { return; @@ -1147,7 +1171,7 @@ function getOptions( } // Exclude the current user from the personal details list - const optionsToExclude = [{login: currentUserLogin}, {login: CONST.EMAIL.NOTIFICATIONS}]; + const optionsToExclude: Array> = [{login: currentUserLogin}, {login: CONST.EMAIL.NOTIFICATIONS}]; // If we're including selected options from the search results, we only want to exclude them if the search input is empty // This is because on certain pages, we show the selected options at the top when the search input is empty @@ -1232,20 +1256,19 @@ function getOptions( let currentUserOption = allPersonalDetailsOptions.find((personalDetailsOption) => personalDetailsOption.login === currentUserLogin); if (searchValue && currentUserOption && !isSearchStringMatch(searchValue, currentUserOption.searchText)) { - currentUserOption = null; + currentUserOption = undefined; } - let userToInvite = null; + let userToInvite: ReportUtils.OptionData | null = null; const noOptions = recentReportOptions.length + personalDetailsOptions.length === 0 && !currentUserOption; - const noOptionsMatchExactly = !_.find( - personalDetailsOptions.concat(recentReportOptions), - (option) => option.login === addSMSDomainIfPhoneNumber(searchValue).toLowerCase() || option.login === searchValue.toLowerCase(), - ); + const noOptionsMatchExactly = !personalDetailsOptions + .concat(recentReportOptions) + .find((option) => option.login === addSMSDomainIfPhoneNumber(searchValue ?? '').toLowerCase() || option.login === searchValue?.toLowerCase()); if ( searchValue && (noOptions || noOptionsMatchExactly) && - !isCurrentUser({login: searchValue}) && + !isCurrentUser({login: searchValue} as PersonalDetails) && 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 ?? '')))) && @@ -1290,7 +1313,7 @@ function getOptions( recentReportOptions, [ (option) => { - if (option.isChatRoom || option.isArchivedRoom) { + if (!!option.isChatRoom || option.isArchivedRoom) { return 3; } if (!option.login) { @@ -1309,7 +1332,7 @@ function getOptions( } return { - personalDetails: _.filter(personalDetailsOptions, (personalDetailsOption) => !personalDetailsOption.isOptimisticPersonalDetail), + personalDetails: personalDetailsOptions.filter((personalDetailsOption) => !personalDetailsOption.isOptimisticPersonalDetail), recentReports: recentReportOptions, userToInvite: canInviteUser ? userToInvite : null, currentUserOption, @@ -1342,10 +1365,10 @@ function getSearchOptions(reports: Record, personalDetails: Pers /** * Build the IOUConfirmation options for showing the payee personalDetail */ -function getIOUConfirmationOptionsFromPayeePersonalDetail(personalDetail: PersonalDetails, amountText: string) { +function getIOUConfirmationOptionsFromPayeePersonalDetail(personalDetail: PersonalDetails, amountText: string): PersonalDetails { const formattedLogin = LocalePhoneNumber.formatPhoneNumber(personalDetail.login ?? ''); return { - text: personalDetail.displayName || formattedLogin, + text: personalDetail.displayName ? personalDetail.displayName : formattedLogin, alternateText: formattedLogin || personalDetail.displayName, icons: [ { @@ -1364,7 +1387,7 @@ function getIOUConfirmationOptionsFromPayeePersonalDetail(personalDetail: Person /** * Build the IOUConfirmationOptions for showing participants */ -function getIOUConfirmationOptionsFromParticipants(participants: Option[], amountText: string) { +function getIOUConfirmationOptionsFromParticipants(participants: Participant[], amountText: string) { return participants.map((participant) => ({ ...participant, descriptiveText: amountText, diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 14098f36db3c..b0e27cbb989d 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -345,6 +345,7 @@ type OptionData = { isExpenseReport?: boolean; isOptimisticPersonalDetail?: boolean; selected?: boolean; + isOptimisticAccount?: boolean; } & Report; type OnyxDataTaskAssigneeChat = { @@ -4439,4 +4440,4 @@ export { canEditWriteCapability, }; -export type {OptionData}; +export type {OptionData, Participant}; diff --git a/src/types/onyx/Report.ts b/src/types/onyx/Report.ts index fbeb36d1a6e8..16d0556730ec 100644 --- a/src/types/onyx/Report.ts +++ b/src/types/onyx/Report.ts @@ -147,6 +147,7 @@ type Report = { participantsList?: Array>; text?: string; privateNotes?: Record; + selected?: boolean; }; export default Report; From b25572798fd71ce4b0f961c0ea51dadfc4751044 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Thu, 7 Dec 2023 07:57:40 +0100 Subject: [PATCH 035/391] fix: types --- src/libs/OptionsListUtils.ts | 177 ++++++++++++++++++----------------- src/libs/ReportUtils.ts | 18 +--- src/libs/SidebarUtils.ts | 5 +- src/types/onyx/IOU.ts | 10 ++ 4 files changed, 107 insertions(+), 103 deletions(-) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 3461212fee50..f446767e9502 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -3,16 +3,18 @@ import {parsePhoneNumber} from 'awesome-phonenumber'; import Str from 'expensify-common/lib/str'; import lodashOrderBy from 'lodash/orderBy'; import lodashSet from 'lodash/set'; +import lodashSortBy from 'lodash/sortBy'; import lodashTimes from 'lodash/times'; import Onyx, {OnyxCollection, OnyxEntry} from 'react-native-onyx'; -import {ValueOf} from 'type-fest'; +// import _ from 'underscore'; import CONST from '@src/CONST'; import {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; import {Beta, Login, PersonalDetails, Policy, PolicyCategory, Report, ReportAction, ReportActions, Transaction} from '@src/types/onyx'; import {Participant} from '@src/types/onyx/IOU'; import * as OnyxCommon from '@src/types/onyx/OnyxCommon'; -import {isEmptyObject, isNotEmptyObject} from '@src/types/utils/EmptyObject'; +import DeepValueOf from '@src/types/utils/DeepValueOf'; +import {EmptyObject, isEmptyObject, isNotEmptyObject} from '@src/types/utils/EmptyObject'; import * as CollectionUtils from './CollectionUtils'; import * as ErrorUtils from './ErrorUtils'; import * as LocalePhoneNumber from './LocalePhoneNumber'; @@ -26,12 +28,14 @@ import * as ReportUtils from './ReportUtils'; import * as TransactionUtils from './TransactionUtils'; import * as UserUtils from './UserUtils'; -type PersonalDetailsCollection = Record; +type PersonalDetailsCollection = Record; type Tag = {enabled: boolean; name: string}; type Option = {text: string; keyForList: string; searchText: string; tooltipText: string; isDisabled: boolean}; +type PayeePersonalDetails = {text: string; alternateText: string; icons: OnyxCommon.Icon[]; descriptiveText: string; login: string; accountID: number}; + /** * 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 @@ -59,10 +63,15 @@ Onyx.connect({ callback: (value) => (allPersonalDetails = Object.keys(value ?? {}).length === 0 ? {} : value), }); -let preferredLocale: OnyxEntry>; +let preferredLocale: DeepValueOf = CONST.LOCALES.DEFAULT; Onyx.connect({ key: ONYXKEYS.NVP_PREFERRED_LOCALE, - callback: (value) => (preferredLocale = value ?? CONST.LOCALES.DEFAULT), + callback: (value) => { + if (!value) { + return; + } + preferredLocale = value; + }, }); const policies: OnyxCollection = {}; @@ -105,7 +114,7 @@ Onyx.connect({ }, }); -let allTransactions: Record = {}; +let allTransactions: OnyxCollection = {}; Onyx.connect({ key: ONYXKEYS.COLLECTION.TRANSACTION, waitForCollectionCallback: true, @@ -113,7 +122,16 @@ Onyx.connect({ if (!value) { return; } - allTransactions = _.pick(value, (transaction) => !!transaction); + + allTransactions = Object.keys(value) + .filter((key) => !!value[key]) + .reduce((result: OnyxCollection, key) => { + if (result) { + // eslint-disable-next-line no-param-reassign + result[key] = value[key]; + } + return result; + }, {}); }, }); @@ -154,8 +172,8 @@ function getAvatarsForAccountIDs(accountIDs: number[], personalDetails: Personal * Returns the personal details for an array of accountIDs * @returns keys of the object are emails, values are PersonalDetails objects. */ -function getPersonalDetailsForAccountIDs(accountIDs: number[], personalDetails: PersonalDetailsCollection): Record> { - const personalDetailsForAccountIDs: Record> = {}; +function getPersonalDetailsForAccountIDs(accountIDs: number[], personalDetails: PersonalDetailsCollection): Record { + const personalDetailsForAccountIDs: Record = {}; if (!personalDetails) { return personalDetailsForAccountIDs; } @@ -164,11 +182,11 @@ function getPersonalDetailsForAccountIDs(accountIDs: number[], personalDetails: if (!cleanAccountID) { return; } - let personalDetail: Partial = personalDetails[accountID]; + let personalDetail: PersonalDetails = personalDetails[accountID]; if (!personalDetail) { personalDetail = { avatar: UserUtils.getDefaultAvatar(cleanAccountID), - }; + } as PersonalDetails; } if (cleanAccountID === CONST.ACCOUNT_ID.CONCIERGE) { @@ -192,8 +210,8 @@ function isPersonalDetailsReady(personalDetails: PersonalDetailsCollection): boo /** * Get the participant option for a report. */ -function getParticipantsOption(participant: ReportUtils.Participant & {searchText?: string}, personalDetails: PersonalDetailsCollection): ReportUtils.Participant { - const detail = getPersonalDetailsForAccountIDs([participant.accountID], personalDetails)[participant.accountID]; +function getParticipantsOption(participant: ReportUtils.OptionData, personalDetails: PersonalDetailsCollection): Participant { + const detail = getPersonalDetailsForAccountIDs([participant.accountID ?? -1], personalDetails)[participant.accountID ?? -1]; const login = detail.login ?? participant.login ?? ''; const displayName = detail.displayName ?? LocalePhoneNumber.formatPhoneNumber(login); return { @@ -213,8 +231,8 @@ function getParticipantsOption(participant: ReportUtils.Participant & {searchTex }, ], phoneNumber: detail.phoneNumber ?? '', - selected: participant.selected, - searchText: participant.searchText, + selected: !!participant.selected, + searchText: participant.searchText ?? '', }; } @@ -271,7 +289,13 @@ function uniqFast(items: string[]) { * Array.prototype.push.apply is faster than using the spread operator, and concat() is faster than push(). */ -function getSearchText(report: Report, reportName: string, personalDetailList: Array>, isChatRoomOrPolicyExpenseChat: boolean, isThread: boolean): string { +function getSearchText( + report: OnyxEntry, + reportName: string, + personalDetailList: Array>, + isChatRoomOrPolicyExpenseChat: boolean, + isThread: boolean, +): string { let searchTerms: string[] = []; if (!isChatRoomOrPolicyExpenseChat) { @@ -319,23 +343,22 @@ function getSearchText(report: Report, reportName: string, personalDetailList: A function getAllReportErrors(report: OnyxEntry, reportActions: OnyxEntry): OnyxCommon.Errors { const reportErrors = report?.errors ?? {}; const reportErrorFields = report?.errorFields ?? {}; - const reportActionErrors: OnyxCommon.Errors = Object.values(reportActions ?? {}).reduce( + let reportActionErrors: OnyxCommon.Errors = Object.values(reportActions ?? {}).reduce( (prevReportActionErrors, action) => (!action || isEmptyObject(action.errors) ? prevReportActionErrors : {...prevReportActionErrors, ...action.errors}), {}, ); - const parentReportAction: OnyxEntry = !report?.parentReportID || !report?.parentReportActionID ? null : allReportActions[report.parentReportID][report.parentReportActionID] ?? null; if (parentReportAction?.actorAccountID === currentUserAccountID && ReportActionUtils.isTransactionThread(parentReportAction)) { const transactionID = parentReportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? parentReportAction?.originalMessage?.IOUTransactionID : undefined; - const transaction = allTransactions[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]; - if (TransactionUtils.hasMissingSmartscanFields(transaction) && !ReportUtils.isSettled(transaction.reportID)) { - _.extend(reportActionErrors, {smartscan: ErrorUtils.getMicroSecondOnyxError('report.genericSmartscanFailureMessage')}); + const transaction = allTransactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]; + if (TransactionUtils.hasMissingSmartscanFields(transaction ?? null) && !ReportUtils.isSettled(transaction?.reportID)) { + reportActionErrors = {...reportActionErrors, smartscan: ErrorUtils.getMicroSecondOnyxError('report.genericSmartscanFailureMessage')}; } } else if ((ReportUtils.isIOUReport(report) || ReportUtils.isExpenseReport(report)) && report?.ownerAccountID === currentUserAccountID) { if (ReportUtils.hasMissingSmartscanFields(report?.reportID ?? '') && !ReportUtils.isSettled(report?.reportID)) { - _.extend(reportActionErrors, {smartscan: ErrorUtils.getMicroSecondOnyxError('report.genericSmartscanFailureMessage')}); + reportActionErrors = {...reportActionErrors, smartscan: ErrorUtils.getMicroSecondOnyxError('report.genericSmartscanFailureMessage')}; } } @@ -346,10 +369,10 @@ function getAllReportErrors(report: OnyxEntry, reportActions: OnyxEntry< reportActionErrors, }; // Combine all error messages keyed by microtime into one object - const allReportErrors = Object.values(errorSources)?.reduce( - (prevReportErrors, errors) => (Object.keys(errors ?? {}).length > 0 ? prevReportErrors : Object.assign(prevReportErrors, errors)), - {}, - ); + const allReportErrors = Object.values(errorSources)?.reduce((prevReportErrors, errors) => { + const yes = Object.keys(errors ?? {}).length > 0 ? prevReportErrors : Object.assign(prevReportErrors, errors); + return yes; + }, {}); return allReportErrors; } @@ -488,18 +511,18 @@ function createOption( const lastReportAction = lastReportActions[report.reportID ?? '']; if (result.isArchivedRoom && lastReportAction.actionName === CONST.REPORT.ACTIONS.TYPE.CLOSED) { const archiveReason = lastReportAction.originalMessage?.reason ?? CONST.REPORT.ARCHIVE_REASON.DEFAULT; - lastMessageText = Localize.translate(preferredLocale ?? CONST.LOCALES.DEFAULT, `reportArchiveReasons.${archiveReason}`, { + lastMessageText = Localize.translate(preferredLocale, `reportArchiveReasons.${archiveReason}`, { displayName: archiveReason.displayName ?? PersonalDetailsUtils.getDisplayNameOrDefault(lastActorDetails, 'displayName'), policyName: ReportUtils.getPolicyName(report), }); } if (result.isThread || result.isMoneyRequestReport) { - result.alternateText = lastMessageTextFromReport.length > 0 ? lastMessageText : Localize.translate(preferredLocale ?? CONST.LOCALES.DEFAULT, 'report.noActivityYet'); + result.alternateText = lastMessageTextFromReport.length > 0 ? lastMessageText : Localize.translate(preferredLocale, 'report.noActivityYet'); } else if (result.isChatRoom || result.isPolicyExpenseChat) { result.alternateText = showChatPreviewLine && !forcePolicyNamePreview && lastMessageText ? lastMessageText : subtitle; } else if (result.isTaskReport) { - result.alternateText = showChatPreviewLine && lastMessageText ? lastMessageTextFromReport : Localize.translate(preferredLocale ?? CONST.LOCALES.DEFAULT, 'report.noActivityYet'); + result.alternateText = showChatPreviewLine && lastMessageText ? lastMessageTextFromReport : Localize.translate(preferredLocale, 'report.noActivityYet'); } else { result.alternateText = showChatPreviewLine && lastMessageText ? lastMessageText : LocalePhoneNumber.formatPhoneNumber(personalDetail.login ?? ''); } @@ -1086,7 +1109,7 @@ function getOptions( // Sorting the reports works like this: // - Order everything by the last message timestamp (descending) // - All archived reports should remain at the bottom - const orderedReports = _.sortBy(filteredReports, (report) => { + const orderedReports = lodashSortBy(filteredReports, (report) => { if (ReportUtils.isArchivedRoom(report)) { return CONST.DATE.UNIX_EPOCH; } @@ -1157,7 +1180,14 @@ function getOptions( // 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)); + const filteredDetails: PersonalDetailsCollection = Object.keys(personalDetails) + .filter((key) => 'login' in personalDetails[+key]) + .reduce((obj: PersonalDetailsCollection, key) => { + // eslint-disable-next-line no-param-reassign + obj[+key] = personalDetails[+key]; + return obj; + }, {}); + const havingLoginPersonalDetails = !includeP2P ? {} : filteredDetails; let allPersonalDetailsOptions = Object.values(havingLoginPersonalDetails).map((personalDetail) => createOption([personalDetail?.accountID ?? 0], personalDetails, reportMapForAccountIDs[personalDetail?.accountID], reportActions, { showChatPreviewLine, @@ -1344,7 +1374,7 @@ function getOptions( /** * Build the options for the Search view */ -function getSearchOptions(reports: Record, personalDetails: PersonalDetailsCollection, betas: Beta[] = [], searchValue = '') { +function getSearchOptions(reports: Record, personalDetails: PersonalDetailsCollection, searchValue = '', betas: Beta[] = []) { return getOptions(reports, personalDetails, { betas, searchInputValue: searchValue.trim(), @@ -1365,21 +1395,21 @@ function getSearchOptions(reports: Record, personalDetails: Pers /** * Build the IOUConfirmation options for showing the payee personalDetail */ -function getIOUConfirmationOptionsFromPayeePersonalDetail(personalDetail: PersonalDetails, amountText: string): PersonalDetails { +function getIOUConfirmationOptionsFromPayeePersonalDetail(personalDetail: PersonalDetails, amountText: string): PayeePersonalDetails { const formattedLogin = LocalePhoneNumber.formatPhoneNumber(personalDetail.login ?? ''); return { text: personalDetail.displayName ? personalDetail.displayName : formattedLogin, - alternateText: formattedLogin || personalDetail.displayName, + alternateText: formattedLogin ?? personalDetail.displayName, icons: [ { source: UserUtils.getAvatar(personalDetail.avatar, personalDetail.accountID), - name: personalDetail.login, + name: personalDetail.login ?? '', type: CONST.ICON_TYPE_AVATAR, id: personalDetail.accountID, }, ], descriptiveText: amountText, - login: personalDetail.login, + login: personalDetail.login ?? '', accountID: personalDetail.accountID, }; } @@ -1396,24 +1426,6 @@ function getIOUConfirmationOptionsFromParticipants(participants: Participant[], /** * 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] - * @param {boolean} [includeSelectedOptions] - * @returns {Object} */ function getFilteredOptions( reports: Record, @@ -1490,11 +1502,10 @@ function getShareDestinationOptions( /** * Format personalDetails or userToInvite to be shown in the list * - * @param {Object} member - personalDetails or userToInvite - * @param {Object} config - keys to overwrite the default values - * @returns {Object} + * @param member - personalDetails or userToInvite + * @param config - keys to overwrite the default values */ -function formatMemberForList(member, config = {}) { +function formatMemberForList(member: ReportUtils.OptionData, config: ReportUtils.OptionData | EmptyObject = {}) { if (!member) { return undefined; } @@ -1519,7 +1530,7 @@ function formatMemberForList(member, config = {}) { /** * Build the options for the Workspace Member Invite view */ -function getMemberInviteOptions(personalDetails: PersonalDetailsCollection, betas = [], searchValue = '', excludeLogins = []) { +function getMemberInviteOptions(personalDetails: PersonalDetailsCollection, betas: Beta[] = [], searchValue = '', excludeLogins: string[] = []) { return getOptions({}, personalDetails, { betas, searchInputValue: searchValue.trim(), @@ -1565,12 +1576,8 @@ function getHeaderMessage(hasSelectableOptions: boolean, hasUserToInvite: boolea /** * Helper method for non-user lists (eg. categories and tags) that returns the text to be used for the header's message and title (if any) - * - * @param {Boolean} hasSelectableOptions - * @param {String} searchValue - * @return {String} */ -function getHeaderMessageForNonUserList(hasSelectableOptions, searchValue) { +function getHeaderMessageForNonUserList(hasSelectableOptions: boolean, searchValue: string): string { if (searchValue && !hasSelectableOptions) { return Localize.translate(preferredLocale, 'common.noResultsFound'); } @@ -1580,22 +1587,22 @@ function getHeaderMessageForNonUserList(hasSelectableOptions, searchValue) { /** * Helper method to check whether an option can show tooltip or not */ -function shouldOptionShowTooltip(option: Option): boolean { +function shouldOptionShowTooltip(option: ReportUtils.OptionData): boolean { return Boolean((!option.isChatRoom || option.isThread) && !option.isArchivedRoom); } /** * Handles the logic for displaying selected participants from the search term - * @param {String} searchTerm - * @param {Array} selectedOptions - * @param {Array} filteredRecentReports - * @param {Array} filteredPersonalDetails - * @param {Object} personalDetails - * @param {Boolean} shouldGetOptionDetails - * @param {Number} indexOffset - * @returns {Object} */ -function formatSectionsFromSearchTerm(searchTerm, selectedOptions, filteredRecentReports, filteredPersonalDetails, personalDetails = {}, shouldGetOptionDetails = false, indexOffset) { +function formatSectionsFromSearchTerm( + searchTerm: string, + selectedOptions: ReportUtils.OptionData[], + filteredRecentReports: ReportUtils.OptionData[], + filteredPersonalDetails: PersonalDetails[], + personalDetails: PersonalDetails | EmptyObject = {}, + shouldGetOptionDetails = false, + indexOffset = 0, +) { // We show the selected participants at the top of the list when there is no search term // However, if there is a search term we remove the selected participants from the top of the list unless they are part of the search results // This clears up space on mobile views, where if you create a group with 4+ people you can't see the selected participants and the search results at the same time @@ -1604,12 +1611,12 @@ function formatSectionsFromSearchTerm(searchTerm, selectedOptions, filteredRecen section: { title: undefined, data: shouldGetOptionDetails - ? _.map(selectedOptions, (participant) => { - const isPolicyExpenseChat = lodashGet(participant, 'isPolicyExpenseChat', false); + ? selectedOptions.map((participant) => { + const isPolicyExpenseChat = participant.isPolicyExpenseChat ?? false; return isPolicyExpenseChat ? getPolicyExpenseReportOption(participant) : getParticipantsOption(participant, personalDetails); }) : selectedOptions, - shouldShow: !_.isEmpty(selectedOptions), + shouldShow: selectedOptions.length > 0, indexOffset, }, newIndexOffset: indexOffset + selectedOptions.length, @@ -1618,11 +1625,11 @@ function formatSectionsFromSearchTerm(searchTerm, selectedOptions, filteredRecen // If you select a new user you don't have a contact for, they won't get returned as part of a recent report or personal details // This will add them to the list of options, deduping them if they already exist in the other lists - const selectedParticipantsWithoutDetails = _.filter(selectedOptions, (participant) => { - const accountID = lodashGet(participant, 'accountID', null); - const isPartOfSearchTerm = participant.searchText.toLowerCase().includes(searchTerm.trim().toLowerCase()); - const isReportInRecentReports = _.some(filteredRecentReports, (report) => report.accountID === accountID); - const isReportInPersonalDetails = _.some(filteredPersonalDetails, (personalDetail) => personalDetail.accountID === accountID); + const selectedParticipantsWithoutDetails = selectedOptions.filter((participant) => { + const accountID = participant.accountID ?? null; + const isPartOfSearchTerm = participant.searchText?.toLowerCase().includes(searchTerm.trim().toLowerCase()); + const isReportInRecentReports = filteredRecentReports.some((report) => report.accountID === accountID); + const isReportInPersonalDetails = filteredPersonalDetails.some((personalDetail) => personalDetail.accountID === accountID); return isPartOfSearchTerm && !isReportInRecentReports && !isReportInPersonalDetails; }); @@ -1630,12 +1637,12 @@ function formatSectionsFromSearchTerm(searchTerm, selectedOptions, filteredRecen section: { title: undefined, data: shouldGetOptionDetails - ? _.map(selectedParticipantsWithoutDetails, (participant) => { - const isPolicyExpenseChat = lodashGet(participant, 'isPolicyExpenseChat', false); + ? selectedParticipantsWithoutDetails.map((participant) => { + const isPolicyExpenseChat = participant.isPolicyExpenseChat ?? false; return isPolicyExpenseChat ? getPolicyExpenseReportOption(participant) : getParticipantsOption(participant, personalDetails); }) : selectedParticipantsWithoutDetails, - shouldShow: !_.isEmpty(selectedParticipantsWithoutDetails), + shouldShow: selectedParticipantsWithoutDetails.length > 0, indexOffset, }, newIndexOffset: indexOffset + selectedParticipantsWithoutDetails.length, diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 91a0a19e303c..2dbd9b6d163c 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -17,6 +17,7 @@ import {ParentNavigationSummaryParams, TranslationPaths} from '@src/languages/ty import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import {Beta, Login, PersonalDetails, Policy, PolicyTags, Report, ReportAction, Session, Transaction} from '@src/types/onyx'; +import {Participant} from '@src/types/onyx/IOU'; import {Errors, Icon, PendingAction} from '@src/types/onyx/OnyxCommon'; import {ChangeLog, IOUMessage, OriginalMessageActionName} from '@src/types/onyx/OriginalMessage'; import {Message, ReportActions} from '@src/types/onyx/ReportAction'; @@ -61,20 +62,6 @@ type ExpenseOriginalMessage = { oldBillable?: string; }; -type Participant = { - accountID: number; - alternateText: string; - firstName: string; - icons: Icon[]; - keyForList: string; - lastName: string; - login: string; - phoneNumber: string; - searchText: string; - selected: boolean; - text: string; -}; - type SpendBreakdown = { nonReimbursableSpend: number; reimbursableSpend: number; @@ -347,6 +334,7 @@ type OptionData = { isOptimisticPersonalDetail?: boolean; selected?: boolean; isOptimisticAccount?: boolean; + isDisabled?: boolean; } & Report; type OnyxDataTaskAssigneeChat = { @@ -4459,4 +4447,4 @@ export { shouldAutoFocusOnKeyPress, }; -export type {OptionData, Participant}; +export type {OptionData}; diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index bace29e06d28..c8452d3d3870 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -6,7 +6,6 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import {PersonalDetails} from '@src/types/onyx'; import Beta from '@src/types/onyx/Beta'; -import * as OnyxCommon from '@src/types/onyx/OnyxCommon'; import Policy from '@src/types/onyx/Policy'; import Report from '@src/types/onyx/Report'; import ReportAction, {ReportActions} from '@src/types/onyx/ReportAction'; @@ -275,7 +274,7 @@ function getOptionData( isWaitingOnBankAccount: false, isAllowedToComment: true, }; - const participantPersonalDetailList: PersonalDetails[] = Object.values(OptionsListUtils.getPersonalDetailsForAccountIDs(report.participantAccountIDs ?? [], personalDetails)); + const participantPersonalDetailList = Object.values(OptionsListUtils.getPersonalDetailsForAccountIDs(report.participantAccountIDs ?? [], personalDetails)); const personalDetail = participantPersonalDetailList[0] ?? {}; result.isThread = ReportUtils.isChatThread(report); @@ -288,7 +287,7 @@ function getOptionData( result.isMoneyRequestReport = ReportUtils.isMoneyRequestReport(report); result.shouldShowSubscript = ReportUtils.shouldReportShowSubscript(report); result.pendingAction = report.pendingFields ? report.pendingFields.addWorkspaceRoom || report.pendingFields.createChat : null; - result.allReportErrors = OptionsListUtils.getAllReportErrors(report, reportActions) as OnyxCommon.Errors; + result.allReportErrors = OptionsListUtils.getAllReportErrors(report, reportActions); result.brickRoadIndicator = Object.keys(result.allReportErrors ?? {}).length !== 0 ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : ''; result.ownerAccountID = report.ownerAccountID; result.managerID = report.managerID; diff --git a/src/types/onyx/IOU.ts b/src/types/onyx/IOU.ts index a74d2ab74be0..d8b200b06c00 100644 --- a/src/types/onyx/IOU.ts +++ b/src/types/onyx/IOU.ts @@ -1,3 +1,5 @@ +import {Icon} from './OnyxCommon'; + type Participant = { accountID: number; login?: string; @@ -5,6 +7,14 @@ type Participant = { isOwnPolicyExpenseChat?: boolean; selected?: boolean; reportID?: string; + searchText?: string; + alternateText: string; + firstName: string; + icons: Icon[]; + keyForList: string; + lastName: string; + phoneNumber: string; + text: string; }; type IOU = { From 905b5e3a700072f8b2da964518f52874bec10f9d Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Thu, 7 Dec 2023 13:07:51 +0100 Subject: [PATCH 036/391] fix: types --- src/libs/OptionsListUtils.ts | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index f446767e9502..8b93a78df5ed 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -11,7 +11,6 @@ import CONST from '@src/CONST'; import {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; import {Beta, Login, PersonalDetails, Policy, PolicyCategory, Report, ReportAction, ReportActions, Transaction} from '@src/types/onyx'; -import {Participant} from '@src/types/onyx/IOU'; import * as OnyxCommon from '@src/types/onyx/OnyxCommon'; import DeepValueOf from '@src/types/utils/DeepValueOf'; import {EmptyObject, isEmptyObject, isNotEmptyObject} from '@src/types/utils/EmptyObject'; @@ -634,18 +633,14 @@ type Category = { enabled: boolean; }; -type DynamicKey = { - [K in Key]?: Hierarchy | undefined; -}; - -type Hierarchy = Record>; +type Hierarchy = Record; /** * Sorts categories using a simple object. * It builds an hierarchy (based on an object), where each category has a name and other keys as subcategories. * Via the hierarchy we avoid duplicating and sort categories one by one. Subcategories are being sorted alphabetically. */ -function sortCategories(categories: Record): Category[] { +function sortCategories(categories: Record): Category[] { // Sorts categories alphabetically by name. const sortedCategories = Object.values(categories).sort((a, b) => a.name.localeCompare(b.name)); @@ -688,7 +683,7 @@ function sortCategories(categories: Record): Category[] if (name) { const categoryObject = { name, - enabled: categories.name.enabled ?? false, + enabled: categories[name].enabled ?? false, }; acc.push(categoryObject); From 9eafd1fce280df3a53617063348a525f3f2ca6b5 Mon Sep 17 00:00:00 2001 From: brunovjk Date: Thu, 7 Dec 2023 09:39:04 -0300 Subject: [PATCH 037/391] Add 'didScreenTransitionEnd' prop to MoneyRequestParticipantsSelector --- .../MoneyRequestParticipantsSelector.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js index c08c8c0a21b8..8d6ed55724bf 100755 --- a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js +++ b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js @@ -66,6 +66,9 @@ const propTypes = { /** Whether we are searching for reports in the server */ isSearchingForReports: PropTypes.bool, + /** Whether the screen transition has ended */ + didScreenTransitionEnd: PropTypes.bool, + ...withLocalizePropTypes, }; @@ -78,6 +81,7 @@ const defaultProps = { betas: [], isDistanceRequest: false, isSearchingForReports: false, + didScreenTransitionEnd: false, }; function MoneyRequestParticipantsSelector({ @@ -94,6 +98,7 @@ function MoneyRequestParticipantsSelector({ iouType, isDistanceRequest, isSearchingForReports, + didScreenTransitionEnd, }) { const styles = useThemeStyles(); const [searchTerm, setSearchTerm] = useState(''); From a5ed7d8ddc0cdcb9d1caa0da9e391bcb3b751c04 Mon Sep 17 00:00:00 2001 From: brunovjk Date: Thu, 7 Dec 2023 09:41:25 -0300 Subject: [PATCH 038/391] Integrate 'didScreenTransitionEnd' prop in MoneyRequestParticipantsPage --- .../MoneyRequestParticipantsPage.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsPage.js b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsPage.js index d0982e6296db..edf452c78848 100644 --- a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsPage.js +++ b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsPage.js @@ -132,7 +132,7 @@ function MoneyRequestParticipantsPage({iou, selectedTab, route, transaction}) { onEntryTransitionEnd={() => optionsSelectorRef.current && optionsSelectorRef.current.focus()} testID={MoneyRequestParticipantsPage.displayName} > - {({safeAreaPaddingBottomStyle}) => ( + {({safeAreaPaddingBottomStyle, didScreenTransitionEnd}) => ( )} From 19d6d6856cd2b713a8da56b47a8b4c35d444a7e8 Mon Sep 17 00:00:00 2001 From: brunovjk Date: Thu, 7 Dec 2023 09:44:33 -0300 Subject: [PATCH 039/391] Conditionally update chatOptions on screen transition end --- .../MoneyRequestParticipantsSelector.js | 64 ++++++++++--------- 1 file changed, 33 insertions(+), 31 deletions(-) diff --git a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js index 8d6ed55724bf..292ace6c9c50 100755 --- a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js +++ b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js @@ -230,37 +230,39 @@ function MoneyRequestParticipantsSelector({ const isOptionsDataReady = ReportUtils.isReportDataReady() && OptionsListUtils.isPersonalDetailsReady(personalDetails); useEffect(() => { - const chatOptions = OptionsListUtils.getFilteredOptions( - reports, - personalDetails, - betas, - searchTerm, - participants, - CONST.EXPENSIFY_EMAILS, - - // If we are using this component in the "Request money" flow then we pass the includeOwnedWorkspaceChats argument so that the current user - // sees the option to request money from their admin on their own Workspace Chat. - iouType === CONST.IOU.TYPE.REQUEST, - - // We don't want to include any P2P options like personal details or reports that are not workspace chats for certain features. - !isDistanceRequest, - false, - {}, - [], - false, - {}, - [], - // We don't want the user to be able to invite individuals when they are in the "Distance request" flow for now. - // This functionality is being built here: https://github.com/Expensify/App/issues/23291 - !isDistanceRequest, - true, - ); - setNewChatOptions({ - recentReports: chatOptions.recentReports, - personalDetails: chatOptions.personalDetails, - userToInvite: chatOptions.userToInvite, - }); - }, [betas, reports, participants, personalDetails, translate, searchTerm, setNewChatOptions, iouType, isDistanceRequest]); + if (didScreenTransitionEnd) { + const chatOptions = OptionsListUtils.getFilteredOptions( + reports, + personalDetails, + betas, + searchTerm, + participants, + CONST.EXPENSIFY_EMAILS, + + // If we are using this component in the "Request money" flow then we pass the includeOwnedWorkspaceChats argument so that the current user + // sees the option to request money from their admin on their own Workspace Chat. + iouType === CONST.IOU.TYPE.REQUEST, + + // We don't want to include any P2P options like personal details or reports that are not workspace chats for certain features. + !isDistanceRequest, + false, + {}, + [], + false, + {}, + [], + // We don't want the user to be able to invite individuals when they are in the "Distance request" flow for now. + // This functionality is being built here: https://github.com/Expensify/App/issues/23291 + !isDistanceRequest, + true, + ); + setNewChatOptions({ + recentReports: chatOptions.recentReports, + personalDetails: chatOptions.personalDetails, + userToInvite: chatOptions.userToInvite, + }); + } + }, [betas, reports, participants, personalDetails, translate, searchTerm, setNewChatOptions, iouType, isDistanceRequest, didScreenTransitionEnd]); // When search term updates we will fetch any reports const setSearchTermAndSearchInServer = useCallback((text = '') => { From 5e92f737ce5dde050666890d44b7c97ed0d6489b Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Fri, 8 Dec 2023 10:54:40 +0100 Subject: [PATCH 040/391] fix: tests --- src/libs/OptionsListUtils.ts | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 8b93a78df5ed..4d2f218822b1 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -1,16 +1,17 @@ /* eslint-disable no-continue */ import {parsePhoneNumber} from 'awesome-phonenumber'; import Str from 'expensify-common/lib/str'; +import lodashGet from 'lodash/get'; import lodashOrderBy from 'lodash/orderBy'; import lodashSet from 'lodash/set'; import lodashSortBy from 'lodash/sortBy'; import lodashTimes from 'lodash/times'; import Onyx, {OnyxCollection, OnyxEntry} from 'react-native-onyx'; -// import _ from 'underscore'; import CONST from '@src/CONST'; import {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; import {Beta, Login, PersonalDetails, Policy, PolicyCategory, Report, ReportAction, ReportActions, Transaction} from '@src/types/onyx'; +import {Participant} from '@src/types/onyx/IOU'; import * as OnyxCommon from '@src/types/onyx/OnyxCommon'; import DeepValueOf from '@src/types/utils/DeepValueOf'; import {EmptyObject, isEmptyObject, isNotEmptyObject} from '@src/types/utils/EmptyObject'; @@ -342,17 +343,17 @@ function getSearchText( function getAllReportErrors(report: OnyxEntry, reportActions: OnyxEntry): OnyxCommon.Errors { const reportErrors = report?.errors ?? {}; const reportErrorFields = report?.errorFields ?? {}; - let reportActionErrors: OnyxCommon.Errors = Object.values(reportActions ?? {}).reduce( + let reportActionErrors = Object.values(reportActions ?? {}).reduce( (prevReportActionErrors, action) => (!action || isEmptyObject(action.errors) ? prevReportActionErrors : {...prevReportActionErrors, ...action.errors}), {}, ); const parentReportAction: OnyxEntry = - !report?.parentReportID || !report?.parentReportActionID ? null : allReportActions[report.parentReportID][report.parentReportActionID] ?? null; + !report?.parentReportID || !report?.parentReportActionID ? null : allReportActions?.[report.parentReportID ?? '']?.[report.parentReportActionID ?? ''] ?? null; if (parentReportAction?.actorAccountID === currentUserAccountID && ReportActionUtils.isTransactionThread(parentReportAction)) { - const transactionID = parentReportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? parentReportAction?.originalMessage?.IOUTransactionID : undefined; + const transactionID = parentReportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? parentReportAction?.originalMessage?.IOUTransactionID : null; const transaction = allTransactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]; - if (TransactionUtils.hasMissingSmartscanFields(transaction ?? null) && !ReportUtils.isSettled(transaction?.reportID)) { + if (TransactionUtils.hasMissingSmartscanFields(transaction) && !ReportUtils.isSettled(transaction?.reportID)) { reportActionErrors = {...reportActionErrors, smartscan: ErrorUtils.getMicroSecondOnyxError('report.genericSmartscanFailureMessage')}; } } else if ((ReportUtils.isIOUReport(report) || ReportUtils.isExpenseReport(report)) && report?.ownerAccountID === currentUserAccountID) { @@ -664,8 +665,7 @@ function sortCategories(categories: Record): Category[] { */ sortedCategories.forEach((category) => { const path = category.name.split(CONST.PARENT_CHILD_SEPARATOR); - const existedValue = hierarchy?.path ?? {}; - + const existedValue = lodashGet(hierarchy, path, {}); lodashSet(hierarchy, path, { ...existedValue, name: category.name, @@ -771,7 +771,7 @@ type CategorySection = {title: string; shouldShow: boolean; indexOffset: number; function getCategoryListSections( categories: Record, recentlyUsedCategories: string[], - selectedOptions: Category[], + selectedOptions: Array, searchInputValue: string, maxRecentReportsToShow: number, ): CategorySection[] { @@ -992,7 +992,7 @@ function getTagListSections(rawTags: Tag[], recentlyUsedTags: string[], selected type GetOptionsConfig = { reportActions?: Record; betas?: Beta[]; - selectedOptions?: Category[]; + selectedOptions?: Array; maxRecentReportsToShow?: number; excludeLogins?: string[]; includeMultipleParticipantReports?: boolean; @@ -1182,6 +1182,7 @@ function getOptions( obj[+key] = personalDetails[+key]; return obj; }, {}); + const havingLoginPersonalDetails = !includeP2P ? {} : filteredDetails; let allPersonalDetailsOptions = Object.values(havingLoginPersonalDetails).map((personalDetail) => createOption([personalDetail?.accountID ?? 0], personalDetails, reportMapForAccountIDs[personalDetail?.accountID], reportActions, { @@ -1196,7 +1197,7 @@ function getOptions( } // Exclude the current user from the personal details list - const optionsToExclude: Array> = [{login: currentUserLogin}, {login: CONST.EMAIL.NOTIFICATIONS}]; + const optionsToExclude: Array> = [{login: currentUserLogin}, {login: CONST.EMAIL.NOTIFICATIONS}]; // If we're including selected options from the search results, we only want to exclude them if the search input is empty // This is because on certain pages, we show the selected options at the top when the search input is empty From feadbf2280bd8a9ac40e307b0360ea3b0934d868 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Fri, 8 Dec 2023 15:59:52 +0100 Subject: [PATCH 041/391] fix: tests --- src/libs/GroupChatUtils.ts | 1 - src/libs/OptionsListUtils.ts | 261 +++++++++++++++++------------------ src/types/onyx/IOU.ts | 2 +- 3 files changed, 129 insertions(+), 135 deletions(-) diff --git a/src/libs/GroupChatUtils.ts b/src/libs/GroupChatUtils.ts index db64f6574824..2037782d0b18 100644 --- a/src/libs/GroupChatUtils.ts +++ b/src/libs/GroupChatUtils.ts @@ -17,7 +17,6 @@ function getGroupChatName(report: Report): string | undefined { const participants = report.participantAccountIDs ?? []; const isMultipleParticipantReport = participants.length > 1; const participantPersonalDetails = OptionsListUtils.getPersonalDetailsForAccountIDs(participants, allPersonalDetails ?? {}); - // @ts-expect-error Error will gone when OptionsListUtils will be migrated to Typescript const displayNamesWithTooltips = ReportUtils.getDisplayNamesWithTooltips(participantPersonalDetails, isMultipleParticipantReport); return ReportUtils.getDisplayNamesStringFromTooltips(displayNamesWithTooltips); } diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 71b8c54e1aaa..4e2995a4c9dc 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -1,6 +1,7 @@ /* eslint-disable no-continue */ import {parsePhoneNumber} from 'awesome-phonenumber'; import Str from 'expensify-common/lib/str'; +// eslint-disable-next-line you-dont-need-lodash-underscore/get import lodashGet from 'lodash/get'; import lodashOrderBy from 'lodash/orderBy'; import lodashSet from 'lodash/set'; @@ -28,14 +29,51 @@ import * as ReportUtils from './ReportUtils'; import * as TransactionUtils from './TransactionUtils'; import * as UserUtils from './UserUtils'; -type PersonalDetailsCollection = Record; - -type Tag = {enabled: boolean; name: string}; +type Tag = {enabled: boolean; name: string; accountID: number | null}; type Option = {text: string; keyForList: string; searchText: string; tooltipText: string; isDisabled: boolean}; type PayeePersonalDetails = {text: string; alternateText: string; icons: OnyxCommon.Icon[]; descriptiveText: string; login: string; accountID: number}; +type CategorySection = {title: string; shouldShow: boolean; indexOffset: number; data: Option[]}; + +type Category = { + name: string; + enabled: boolean; +}; + +type Hierarchy = Record; + +type GetOptionsConfig = { + reportActions?: Record; + betas?: Beta[]; + selectedOptions?: Array; + maxRecentReportsToShow?: number; + excludeLogins?: string[]; + includeMultipleParticipantReports?: boolean; + includePersonalDetails?: boolean; + includeRecentReports?: boolean; + 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?: string[]; + includeTags?: boolean; + tags?: Record; + recentlyUsedTags?: string[]; + canInviteUser?: boolean; + includeSelectedOptions?: boolean; +}; + /** * 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 @@ -149,7 +187,7 @@ function addSMSDomainIfPhoneNumber(login: string): string { /** * Returns avatar data for a list of user accountIDs */ -function getAvatarsForAccountIDs(accountIDs: number[], personalDetails: PersonalDetailsCollection, defaultValues: Record = {}): OnyxCommon.Icon[] { +function getAvatarsForAccountIDs(accountIDs: number[], personalDetails: OnyxCollection, defaultValues: Record = {}): OnyxCommon.Icon[] { const reversedDefaultValues: Record = {}; Object.entries(defaultValues).forEach((item) => { @@ -157,7 +195,7 @@ function getAvatarsForAccountIDs(accountIDs: number[], personalDetails: Personal }); return accountIDs.map((accountID) => { const login = reversedDefaultValues[accountID] ?? ''; - const userPersonalDetail = personalDetails[accountID] ?? {login, accountID, avatar: ''}; + const userPersonalDetail = personalDetails?.[accountID] ?? {login, accountID, avatar: ''}; return { id: accountID, @@ -172,7 +210,7 @@ function getAvatarsForAccountIDs(accountIDs: number[], personalDetails: Personal * Returns the personal details for an array of accountIDs * @returns keys of the object are emails, values are PersonalDetails objects. */ -function getPersonalDetailsForAccountIDs(accountIDs: number[], personalDetails: PersonalDetailsCollection): Record { +function getPersonalDetailsForAccountIDs(accountIDs: number[], personalDetails: OnyxCollection): Record { const personalDetailsForAccountIDs: Record = {}; if (!personalDetails) { return personalDetailsForAccountIDs; @@ -182,7 +220,7 @@ function getPersonalDetailsForAccountIDs(accountIDs: number[], personalDetails: if (!cleanAccountID) { return; } - let personalDetail: PersonalDetails = personalDetails[accountID]; + let personalDetail: OnyxEntry = personalDetails[accountID]; if (!personalDetail) { personalDetail = { avatar: UserUtils.getDefaultAvatar(cleanAccountID), @@ -202,15 +240,15 @@ function getPersonalDetailsForAccountIDs(accountIDs: number[], personalDetails: /** * Return true if personal details data is ready, i.e. report list options can be created. */ -function isPersonalDetailsReady(personalDetails: PersonalDetailsCollection): boolean { +function isPersonalDetailsReady(personalDetails: OnyxCollection): boolean { const personalDetailsKeys = Object.keys(personalDetails ?? {}); - return personalDetailsKeys.length > 0 && personalDetailsKeys.some((key) => personalDetails[Number(key)].accountID); + return personalDetailsKeys.length > 0 && personalDetailsKeys.some((key) => personalDetails?.[Number(key)]?.accountID); } /** * Get the participant option for a report. */ -function getParticipantsOption(participant: ReportUtils.OptionData, personalDetails: PersonalDetailsCollection): Participant { +function getParticipantsOption(participant: ReportUtils.OptionData, personalDetails: OnyxCollection): Participant { const detail = getPersonalDetailsForAccountIDs([participant.accountID ?? -1], personalDetails)[participant.accountID ?? -1]; const login = detail.login ?? participant.login ?? ''; const displayName = detail.displayName ?? LocalePhoneNumber.formatPhoneNumber(login); @@ -353,15 +391,15 @@ function getAllReportErrors(report: OnyxEntry, reportActions: OnyxEntry< if (parentReportAction?.actorAccountID === currentUserAccountID && ReportActionUtils.isTransactionThread(parentReportAction)) { const transactionID = parentReportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? parentReportAction?.originalMessage?.IOUTransactionID : null; const transaction = allTransactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]; - if (TransactionUtils.hasMissingSmartscanFields(transaction) && !ReportUtils.isSettled(transaction?.reportID)) { + if (TransactionUtils.hasMissingSmartscanFields(transaction ?? null) && !ReportUtils.isSettled(transaction?.reportID)) { reportActionErrors = {...reportActionErrors, smartscan: ErrorUtils.getMicroSecondOnyxError('report.genericSmartscanFailureMessage')}; } } else if ((ReportUtils.isIOUReport(report) || ReportUtils.isExpenseReport(report)) && report?.ownerAccountID === currentUserAccountID) { if (ReportUtils.hasMissingSmartscanFields(report?.reportID ?? '') && !ReportUtils.isSettled(report?.reportID)) { reportActionErrors = {...reportActionErrors, smartscan: ErrorUtils.getMicroSecondOnyxError('report.genericSmartscanFailureMessage')}; } - } else if (ReportUtils.hasSmartscanError(_.values(reportActions))) { - _.extend(reportActionErrors, {smartscan: ErrorUtils.getMicroSecondOnyxError('report.genericSmartscanFailureMessage')}); + } else if (ReportUtils.hasSmartscanError(Object.values(reportActions ?? {}))) { + reportActionErrors = {...reportActionErrors, smartscan: ErrorUtils.getMicroSecondOnyxError('report.genericSmartscanFailureMessage')}; } // All error objects related to the report. Each object in the sources contains error messages keyed by microtime @@ -427,7 +465,7 @@ function getLastMessageTextForReport(report: OnyxEntry): string { */ function createOption( accountIDs: number[], - personalDetails: PersonalDetailsCollection, + personalDetails: OnyxCollection, report: OnyxEntry, reportActions: Record, {showChatPreviewLine = false, forcePolicyNamePreview = false}: {showChatPreviewLine?: boolean; forcePolicyNamePreview?: boolean}, @@ -502,7 +540,6 @@ function createOption( result.hasOutstandingIOU = report.hasOutstandingIOU; result.isWaitingOnBankAccount = report.isWaitingOnBankAccount; result.policyID = report.policyID; - hasMultipleParticipants = personalDetailList.length > 1 || result.isChatRoom || result.isPolicyExpenseChat; subtitle = ReportUtils.getChatRoomSubtitle(report); @@ -511,10 +548,10 @@ function createOption( let lastMessageText = hasMultipleParticipants && lastActorDetails && lastActorDetails.accountID !== currentUserAccountID ? `${lastActorDetails.displayName}: ` : ''; lastMessageText += report ? lastMessageTextFromReport : ''; const lastReportAction = lastReportActions[report.reportID ?? '']; - if (result.isArchivedRoom && lastReportAction.actionName === CONST.REPORT.ACTIONS.TYPE.CLOSED) { + if (result.isArchivedRoom && lastReportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.CLOSED) { const archiveReason = lastReportAction.originalMessage?.reason ?? CONST.REPORT.ARCHIVE_REASON.DEFAULT; - lastMessageText = Localize.translate(preferredLocale, `reportArchiveReasons.${archiveReason}`, { - displayName: archiveReason.displayName ?? PersonalDetailsUtils.getDisplayNameOrDefault(lastActorDetails, 'displayName'), + lastMessageText = Localize.translate(preferredLocale, `reportArchiveReasons.${archiveReason}` as 'reportArchiveReasons.removedFromPolicy', { + displayName: PersonalDetailsUtils.getDisplayNameOrDefault(lastActorDetails, 'displayName'), policyName: ReportUtils.getPolicyName(report), }); } @@ -533,7 +570,7 @@ function createOption( reportName = ReportUtils.getDisplayNameForParticipant(accountIDs[0]) ?? LocalePhoneNumber.formatPhoneNumber(personalDetail.login ?? ''); result.keyForList = String(accountIDs[0]); - result.alternateText = LocalePhoneNumber.formatPhoneNumber(personalDetails[accountIDs[0]].login ?? ''); + result.alternateText = LocalePhoneNumber.formatPhoneNumber(personalDetails?.[accountIDs[0]]?.login ?? ''); } result.isIOUReportOwner = ReportUtils.isIOUOwnedByCurrentUser(result); @@ -562,7 +599,7 @@ function getPolicyExpenseReportOption(report: Report): ReportUtils.OptionData { const option = createOption( expenseReport?.participantAccountIDs ?? [], - allPersonalDetails ?? [], + allPersonalDetails ?? {}, expenseReport ?? null, {}, { @@ -631,13 +668,6 @@ function hasEnabledOptions(options: Record): boolean { return Object.values(options).some((option) => option.enabled); } -type Category = { - name: string; - enabled: boolean; -}; - -type Hierarchy = Record; - /** * Sorts categories using a simple object. * It builds an hierarchy (based on an object), where each category has a name and other keys as subcategories. @@ -718,54 +748,56 @@ function sortTags(tags: Record | Tag[]) { return sortedTags; } -/** - * Builds the options for the tree hierarchy via indents - * - * @param options - an initial object array - * @param} [isOneLine] - a flag to determine if text should be one line - */ -function getIndentedOptionTree(options: Category[], isOneLine = false): Option[] { - const optionCollection = new Map(); +function indentOption(option: Category, optionCollection: Map, isOneLine: boolean) { + if (isOneLine) { + if (optionCollection.has(option.name)) { + return; + } - options.forEach((option) => { - if (isOneLine) { - if (optionCollection.has(option.name)) { - return; - } + optionCollection.set(option.name, { + text: option.name, + keyForList: option.name, + searchText: option.name, + tooltipText: option.name, + isDisabled: !option.enabled, + }); - optionCollection.set(option.name, { - text: option.name, - keyForList: option.name, - searchText: option.name, - tooltipText: option.name, - isDisabled: !option.enabled, - }); + return; + } + option.name.split(CONST.PARENT_CHILD_SEPARATOR).forEach((optionName, index, array) => { + const indents = lodashTimes(index, () => CONST.INDENTS).join(''); + const isChild = array.length - 1 === index; + const searchText = array.slice(0, index + 1).join(CONST.PARENT_CHILD_SEPARATOR); + + if (optionCollection.has(searchText)) { return; } - option.name.split(CONST.PARENT_CHILD_SEPARATOR).forEach((optionName, index, array) => { - const indents = lodashTimes(index, () => CONST.INDENTS).join(''); - const isChild = array.length - 1 === index; - const searchText = array.slice(0, index + 1).join(CONST.PARENT_CHILD_SEPARATOR); - - if (optionCollection.has(searchText)) { - return; - } - - optionCollection.set(searchText, { - text: `${indents}${optionName}`, - keyForList: searchText, - searchText, - tooltipText: optionName, - isDisabled: isChild ? !option.enabled : true, - }); + optionCollection.set(searchText, { + text: `${indents}${optionName}`, + keyForList: searchText, + searchText, + tooltipText: optionName, + isDisabled: isChild ? !option.enabled : true, }); }); - +} +/** + * Builds the options for the tree hierarchy via indents + * + * @param options - an initial object array + * @param} [isOneLine] - a flag to determine if text should be one line + */ +function getIndentedOptionTree(options: Category[] | Record, isOneLine = false): Option[] { + const optionCollection = new Map(); + if (Array.isArray(options)) { + options.forEach((option) => indentOption(option, optionCollection, isOneLine)); + } else { + Object.values(options).forEach((option) => indentOption(option, optionCollection, isOneLine)); + } return Array.from(optionCollection.values()); } -type CategorySection = {title: string; shouldShow: boolean; indexOffset: number; data: Option[]}; /** * Builds the section list for categories @@ -773,7 +805,7 @@ type CategorySection = {title: string; shouldShow: boolean; indexOffset: number; function getCategoryListSections( categories: Record, recentlyUsedCategories: string[], - selectedOptions: Array, + selectedOptions: Category[], searchInputValue: string, maxRecentReportsToShow: number, ): CategorySection[] { @@ -870,13 +902,6 @@ function getCategoryListSections( return categorySections; } -/** - * Transforms the provided tags into objects with a specific structure. - */ -function getTagsOptions(tags: Tag[]) { - return getIndentedOptionTree(tags); -} - /** * Build the section list for tags */ @@ -905,7 +930,7 @@ function getTagListSections(rawTags: Tag[], recentlyUsedTags: string[], selected title: '', shouldShow: false, indexOffset, - data: getTagsOptions(selectedTagOptions), + data: getIndentedOptionTree(selectedTagOptions), }); return tagSections; @@ -919,7 +944,7 @@ function getTagListSections(rawTags: Tag[], recentlyUsedTags: string[], selected title: '', shouldShow: true, indexOffset, - data: getTagsOptions(searchTags), + data: getIndentedOptionTree(searchTags), }); return tagSections; @@ -931,7 +956,7 @@ function getTagListSections(rawTags: Tag[], recentlyUsedTags: string[], selected title: '', shouldShow: false, indexOffset, - data: getTagsOptions(enabledTags), + data: getIndentedOptionTree(enabledTags), }); return tagSections; @@ -960,7 +985,7 @@ function getTagListSections(rawTags: Tag[], recentlyUsedTags: string[], selected title: '', shouldShow: true, indexOffset, - data: getTagsOptions(selectedTagOptions), + data: getIndentedOptionTree(selectedTagOptions), }); indexOffset += selectedOptions.length; @@ -974,7 +999,7 @@ function getTagListSections(rawTags: Tag[], recentlyUsedTags: string[], selected title: Localize.translateLocal('common.recent'), shouldShow: true, indexOffset, - data: getTagsOptions(cutRecentlyUsedTags), + data: getIndentedOptionTree(cutRecentlyUsedTags), }); indexOffset += filteredRecentlyUsedTags.length; @@ -985,47 +1010,18 @@ function getTagListSections(rawTags: Tag[], recentlyUsedTags: string[], selected title: Localize.translateLocal('common.all'), shouldShow: true, indexOffset, - data: getTagsOptions(filteredTags), + data: getIndentedOptionTree(filteredTags), }); return tagSections; } -type GetOptionsConfig = { - reportActions?: Record; - betas?: Beta[]; - selectedOptions?: Array; - maxRecentReportsToShow?: number; - excludeLogins?: string[]; - includeMultipleParticipantReports?: boolean; - includePersonalDetails?: boolean; - includeRecentReports?: boolean; - 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?: string[]; - includeTags?: boolean; - tags?: Record; - recentlyUsedTags?: string[]; - canInviteUser?: boolean; - includeSelectedOptions?: boolean; -}; /** * Build the options */ function getOptions( reports: Record, - personalDetails: PersonalDetailsCollection, + personalDetails: OnyxCollection, { reportActions = {}, betas = [], @@ -1058,7 +1054,7 @@ function getOptions( }: GetOptionsConfig, ) { if (includeCategories) { - const categoryOptions = getCategoryListSections(categories, recentlyUsedCategories, selectedOptions, searchInputValue, maxRecentReportsToShow); + const categoryOptions = getCategoryListSections(categories, recentlyUsedCategories, selectedOptions as Category[], searchInputValue, maxRecentReportsToShow); return { recentReports: [], @@ -1071,7 +1067,7 @@ function getOptions( } if (includeTags) { - const tagOptions = getTagListSections(Object.values(tags), recentlyUsedTags, selectedOptions, searchInputValue, maxRecentReportsToShow); + const tagOptions = getTagListSections(Object.values(tags), recentlyUsedTags, selectedOptions as Category[], searchInputValue, maxRecentReportsToShow); return { recentReports: [], @@ -1177,17 +1173,20 @@ function getOptions( // 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 filteredDetails: PersonalDetailsCollection = Object.keys(personalDetails) - .filter((key) => 'login' in personalDetails[+key]) - .reduce((obj: PersonalDetailsCollection, key) => { - // eslint-disable-next-line no-param-reassign - obj[+key] = personalDetails[+key]; + const filteredDetails: OnyxCollection = Object.keys(personalDetails ?? {}) + .filter((key) => 'login' in (personalDetails?.[+key] ?? {})) + .reduce((obj: OnyxCollection, key) => { + if (obj) { + // eslint-disable-next-line no-param-reassign + obj[+key] = personalDetails?.[+key] ?? null; + } + return obj; }, {}); const havingLoginPersonalDetails = !includeP2P ? {} : filteredDetails; - let allPersonalDetailsOptions = Object.values(havingLoginPersonalDetails).map((personalDetail) => - createOption([personalDetail?.accountID ?? 0], personalDetails, reportMapForAccountIDs[personalDetail?.accountID], reportActions, { + let allPersonalDetailsOptions = Object.values(havingLoginPersonalDetails ?? {}).map((personalDetail) => + createOption([personalDetail?.accountID ?? 0], personalDetails, reportMapForAccountIDs[personalDetail?.accountID ?? 0], reportActions, { showChatPreviewLine, forcePolicyNamePreview, }), @@ -1199,13 +1198,13 @@ function getOptions( } // Exclude the current user from the personal details list - const optionsToExclude: Array> = [{login: currentUserLogin}, {login: CONST.EMAIL.NOTIFICATIONS}]; + const optionsToExclude = [{login: currentUserLogin}, {login: CONST.EMAIL.NOTIFICATIONS}]; // If we're including selected options from the search results, we only want to exclude them if the search input is empty // This is because on certain pages, we show the selected options at the top when the search input is empty // This prevents the issue of seeing the selected option twice if you have them as a recent chat and select them if (!includeSelectedOptions || searchInputValue === '') { - optionsToExclude.push(...selectedOptions); + optionsToExclude.push(...(selectedOptions as Participant[])); } excludeLogins.forEach((login) => { @@ -1233,11 +1232,7 @@ 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) && - optionsToExclude.some((option) => (option.login && option.login === reportOption.login) ?? option.reportID === reportOption.reportID) - ) { + if (!includeThreads && optionsToExclude.some((option) => 'login' in option && option.login === reportOption.login)) { continue; } @@ -1269,7 +1264,7 @@ function getOptions( if (includePersonalDetails) { // Next loop over all personal details removing any that are selectedUsers or recentChats allPersonalDetailsOptions.forEach((personalDetailOption) => { - if (optionsToExclude.some((optionToExclude) => optionToExclude.login === personalDetailOption.login)) { + if (optionsToExclude.some((optionToExclude) => 'login' in optionToExclude && optionToExclude.login === personalDetailOption.login)) { return; } const {searchText, participantsList, isChatRoom} = personalDetailOption; @@ -1297,10 +1292,10 @@ function getOptions( searchValue && (noOptions || noOptionsMatchExactly) && !isCurrentUser({login: searchValue} as PersonalDetails) && - selectedOptions.every((option) => option.login !== searchValue) && + selectedOptions.every((option) => 'login' in 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 ?? '')))) && - !optionsToExclude.find((optionToExclude) => optionToExclude.login === addSMSDomainIfPhoneNumber(searchValue).toLowerCase()) && + !optionsToExclude.find((optionToExclude) => 'login' in optionToExclude && optionToExclude.login === addSMSDomainIfPhoneNumber(searchValue).toLowerCase()) && (searchValue !== CONST.EMAIL.CHRONOS || Permissions.canUseChronos(betas)) && !excludeUnknownUsers ) { @@ -1372,7 +1367,7 @@ function getOptions( /** * Build the options for the Search view */ -function getSearchOptions(reports: Record, personalDetails: PersonalDetailsCollection, searchValue = '', betas: Beta[] = []) { +function getSearchOptions(reports: Record, personalDetails: OnyxCollection, searchValue = '', betas: Beta[] = []) { return getOptions(reports, personalDetails, { betas, searchInputValue: searchValue.trim(), @@ -1427,7 +1422,7 @@ function getIOUConfirmationOptionsFromParticipants(participants: Participant[], */ function getFilteredOptions( reports: Record, - personalDetails: PersonalDetailsCollection, + personalDetails: OnyxCollection, betas: Beta[] = [], searchValue = '', selectedOptions = [], @@ -1470,7 +1465,7 @@ function getFilteredOptions( function getShareDestinationOptions( reports: Record, - personalDetails: PersonalDetailsCollection, + personalDetails: OnyxCollection, betas: Beta[] = [], searchValue = '', selectedOptions = [], @@ -1528,7 +1523,7 @@ function formatMemberForList(member: ReportUtils.OptionData, config: ReportUtils /** * Build the options for the Workspace Member Invite view */ -function getMemberInviteOptions(personalDetails: PersonalDetailsCollection, betas: Beta[] = [], searchValue = '', excludeLogins: string[] = []) { +function getMemberInviteOptions(personalDetails: OnyxCollection, betas: Beta[] = [], searchValue = '', excludeLogins: string[] = []) { return getOptions({}, personalDetails, { betas, searchInputValue: searchValue.trim(), @@ -1597,7 +1592,7 @@ function formatSectionsFromSearchTerm( selectedOptions: ReportUtils.OptionData[], filteredRecentReports: ReportUtils.OptionData[], filteredPersonalDetails: PersonalDetails[], - personalDetails: PersonalDetails | EmptyObject = {}, + personalDetails: OnyxCollection = {}, shouldGetOptionDetails = false, indexOffset = 0, ) { diff --git a/src/types/onyx/IOU.ts b/src/types/onyx/IOU.ts index d8b200b06c00..08ca5731d48a 100644 --- a/src/types/onyx/IOU.ts +++ b/src/types/onyx/IOU.ts @@ -2,7 +2,7 @@ import {Icon} from './OnyxCommon'; type Participant = { accountID: number; - login?: string; + login: string | undefined; isPolicyExpenseChat?: boolean; isOwnPolicyExpenseChat?: boolean; selected?: boolean; From 60a94cc56bd65bbad005dba29b73ee6e23c3ad81 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Mon, 11 Dec 2023 16:32:59 +0100 Subject: [PATCH 042/391] fix: added return type --- src/libs/OptionsListUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index d5176db5ca7a..fee60fd397f5 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -304,7 +304,7 @@ function getParticipantNames(personalDetailList?: Array * A very optimized method to remove duplicates from an array. * Taken from https://stackoverflow.com/a/9229821/9114791 */ -function uniqFast(items: string[]) { +function uniqFast(items: string[]): string[] { const seenItems: Record = {}; const result: string[] = []; let j = 0; From f919766c1d17b6980fcedb6bd230914cf3c92f1d Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Tue, 12 Dec 2023 15:22:06 +0100 Subject: [PATCH 043/391] fix: adressing comments --- src/libs/OptionsListUtils.ts | 50 ++++++++++++++++++++---------------- src/libs/PolicyUtils.ts | 2 ++ 2 files changed, 30 insertions(+), 22 deletions(-) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index fee60fd397f5..af8cd78d662f 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -24,6 +24,7 @@ import * as LoginUtils from './LoginUtils'; import Navigation from './Navigation/Navigation'; import Permissions from './Permissions'; import * as PersonalDetailsUtils from './PersonalDetailsUtils'; +import {PersonalDetailsList} from './PolicyUtils'; import * as ReportActionUtils from './ReportActionsUtils'; import * as ReportUtils from './ReportUtils'; import * as TaskUtils from './TaskUtils'; @@ -36,7 +37,7 @@ type Option = {text: string; keyForList: string; searchText: string; tooltipText type PayeePersonalDetails = {text: string; alternateText: string; icons: OnyxCommon.Icon[]; descriptiveText: string; login: string; accountID: number}; -type CategorySection = {title: string; shouldShow: boolean; indexOffset: number; data: Option[]}; +type CategorySection = {title: string | undefined; shouldShow: boolean; indexOffset: number; data: Option[] | Participant[] | ReportUtils.OptionData[]}; type Category = { name: string; @@ -96,7 +97,7 @@ Onyx.connect({ callback: (value) => (loginList = Object.keys(value ?? {}).length === 0 ? {} : value), }); -let allPersonalDetails: OnyxEntry>; +let allPersonalDetails: OnyxEntry; Onyx.connect({ key: ONYXKEYS.PERSONAL_DETAILS_LIST, callback: (value) => (allPersonalDetails = Object.keys(value ?? {}).length === 0 ? {} : value), @@ -186,9 +187,10 @@ function addSMSDomainIfPhoneNumber(login: string): string { } /** - * Returns avatar data for a list of user accountIDs + * @param defaultValues {login: accountID} In workspace invite page, when new user is added we pass available data to opt in + * @returns Returns avatar data for a list of user accountIDs */ -function getAvatarsForAccountIDs(accountIDs: number[], personalDetails: OnyxCollection, defaultValues: Record = {}): OnyxCommon.Icon[] { +function getAvatarsForAccountIDs(accountIDs: number[], personalDetails: OnyxEntry, defaultValues: Record = {}): OnyxCommon.Icon[] { const reversedDefaultValues: Record = {}; Object.entries(defaultValues).forEach((item) => { @@ -211,8 +213,8 @@ function getAvatarsForAccountIDs(accountIDs: number[], personalDetails: OnyxColl * Returns the personal details for an array of accountIDs * @returns keys of the object are emails, values are PersonalDetails objects. */ -function getPersonalDetailsForAccountIDs(accountIDs: number[], personalDetails: OnyxCollection): Record { - const personalDetailsForAccountIDs: Record = {}; +function getPersonalDetailsForAccountIDs(accountIDs: number[], personalDetails: OnyxEntry): PersonalDetailsList { + const personalDetailsForAccountIDs: PersonalDetailsList = {}; if (!personalDetails) { return personalDetailsForAccountIDs; } @@ -241,7 +243,7 @@ function getPersonalDetailsForAccountIDs(accountIDs: number[], personalDetails: /** * Return true if personal details data is ready, i.e. report list options can be created. */ -function isPersonalDetailsReady(personalDetails: OnyxCollection): boolean { +function isPersonalDetailsReady(personalDetails: OnyxEntry): boolean { const personalDetailsKeys = Object.keys(personalDetails ?? {}); return personalDetailsKeys.length > 0 && personalDetailsKeys.some((key) => personalDetails?.[Number(key)]?.accountID); } @@ -249,14 +251,14 @@ function isPersonalDetailsReady(personalDetails: OnyxCollection /** * Get the participant option for a report. */ -function getParticipantsOption(participant: ReportUtils.OptionData, personalDetails: OnyxCollection): Participant { +function getParticipantsOption(participant: ReportUtils.OptionData, personalDetails: OnyxEntry): Participant { const detail = getPersonalDetailsForAccountIDs([participant.accountID ?? -1], personalDetails)[participant.accountID ?? -1]; const login = detail.login ?? participant.login ?? ''; const displayName = detail.displayName ?? LocalePhoneNumber.formatPhoneNumber(login); return { keyForList: String(detail.accountID), login, - accountID: detail.accountID ?? 0, + accountID: detail.accountID ?? -1, text: displayName, firstName: detail.firstName ?? '', lastName: detail.lastName ?? '', @@ -468,7 +470,7 @@ function getLastMessageTextForReport(report: OnyxEntry): string { */ function createOption( accountIDs: number[], - personalDetails: OnyxCollection, + personalDetails: OnyxEntry, report: OnyxEntry, reportActions: Record, {showChatPreviewLine = false, forcePolicyNamePreview = false}: {showChatPreviewLine?: boolean; forcePolicyNamePreview?: boolean}, @@ -790,7 +792,7 @@ function indentOption(option: Category, optionCollection: Map, i * Builds the options for the tree hierarchy via indents * * @param options - an initial object array - * @param} [isOneLine] - a flag to determine if text should be one line + * @param [isOneLine] - a flag to determine if text should be one line */ function getIndentedOptionTree(options: Category[] | Record, isOneLine = false): Option[] { const optionCollection = new Map(); @@ -1024,7 +1026,7 @@ function getTagListSections(rawTags: Tag[], recentlyUsedTags: string[], selected */ function getOptions( reports: Record, - personalDetails: OnyxCollection, + personalDetails: OnyxEntry, { reportActions = {}, betas = [], @@ -1176,12 +1178,12 @@ function getOptions( // 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 filteredDetails: OnyxCollection = Object.keys(personalDetails ?? {}) + const filteredDetails: OnyxEntry = Object.keys(personalDetails ?? {}) .filter((key) => 'login' in (personalDetails?.[+key] ?? {})) - .reduce((obj: OnyxCollection, key) => { + .reduce((obj: OnyxEntry, key) => { if (obj) { // eslint-disable-next-line no-param-reassign - obj[+key] = personalDetails?.[+key] ?? null; + obj[+key] = personalDetails?.[+key]; } return obj; @@ -1370,7 +1372,7 @@ function getOptions( /** * Build the options for the Search view */ -function getSearchOptions(reports: Record, personalDetails: OnyxCollection, searchValue = '', betas: Beta[] = []) { +function getSearchOptions(reports: Record, personalDetails: OnyxEntry, searchValue = '', betas: Beta[] = []) { return getOptions(reports, personalDetails, { betas, searchInputValue: searchValue.trim(), @@ -1425,7 +1427,7 @@ function getIOUConfirmationOptionsFromParticipants(participants: Participant[], */ function getFilteredOptions( reports: Record, - personalDetails: OnyxCollection, + personalDetails: OnyxEntry, betas: Beta[] = [], searchValue = '', selectedOptions = [], @@ -1468,7 +1470,7 @@ function getFilteredOptions( function getShareDestinationOptions( reports: Record, - personalDetails: OnyxCollection, + personalDetails: OnyxEntry, betas: Beta[] = [], searchValue = '', selectedOptions = [], @@ -1501,7 +1503,7 @@ function getShareDestinationOptions( * @param member - personalDetails or userToInvite * @param config - keys to overwrite the default values */ -function formatMemberForList(member: ReportUtils.OptionData, config: ReportUtils.OptionData | EmptyObject = {}) { +function formatMemberForList(member: ReportUtils.OptionData, config: ReportUtils.OptionData | EmptyObject = {}): ReportUtils.OptionData | undefined { if (!member) { return undefined; } @@ -1526,7 +1528,7 @@ function formatMemberForList(member: ReportUtils.OptionData, config: ReportUtils /** * Build the options for the Workspace Member Invite view */ -function getMemberInviteOptions(personalDetails: OnyxCollection, betas: Beta[] = [], searchValue = '', excludeLogins: string[] = []) { +function getMemberInviteOptions(personalDetails: OnyxEntry, betas: Beta[] = [], searchValue = '', excludeLogins: string[] = []) { return getOptions({}, personalDetails, { betas, searchInputValue: searchValue.trim(), @@ -1587,6 +1589,10 @@ function shouldOptionShowTooltip(option: ReportUtils.OptionData): boolean { return Boolean((!option.isChatRoom || option.isThread) && !option.isArchivedRoom); } +type SectionForSearchTerm = { + section: CategorySection; + newIndexOffset: number; +}; /** * Handles the logic for displaying selected participants from the search term */ @@ -1595,10 +1601,10 @@ function formatSectionsFromSearchTerm( selectedOptions: ReportUtils.OptionData[], filteredRecentReports: ReportUtils.OptionData[], filteredPersonalDetails: PersonalDetails[], - personalDetails: OnyxCollection = {}, + personalDetails: OnyxEntry = {}, shouldGetOptionDetails = false, indexOffset = 0, -) { +): SectionForSearchTerm { // We show the selected participants at the top of the list when there is no search term // However, if there is a search term we remove the selected participants from the top of the list unless they are part of the search results // This clears up space on mobile views, where if you create a group with 4+ people you can't see the selected participants and the search results at the same time diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index 19129959d016..a62080999f02 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -219,3 +219,5 @@ export { isPendingDeletePolicy, isPolicyMember, }; + +export type {PersonalDetailsList}; From f34bc7a0f17d1d983653600199b2fa1df77cc0ce Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Wed, 13 Dec 2023 10:32:26 +0100 Subject: [PATCH 044/391] fix: added native implementations of lodash methods and add tests for them , addressed review comments --- src/libs/OptionsListUtils.ts | 55 +++++++++++++++++++++++++----------- src/libs/ReportUtils.ts | 1 + src/libs/SidebarUtils.ts | 1 - src/utils/get.ts | 15 ++++++++++ src/utils/sortBy.ts | 35 +++++++++++++++++++++++ src/utils/times.ts | 6 ++++ tests/unit/get.ts | 27 ++++++++++++++++++ tests/unit/sortBy.ts | 21 ++++++++++++++ tests/unit/times.ts | 33 ++++++++++++++++++++++ 9 files changed, 176 insertions(+), 18 deletions(-) create mode 100644 src/utils/get.ts create mode 100644 src/utils/sortBy.ts create mode 100644 src/utils/times.ts create mode 100644 tests/unit/get.ts create mode 100644 tests/unit/sortBy.ts create mode 100644 tests/unit/times.ts diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index e425a2efceb1..fca8345a7323 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -1,12 +1,8 @@ /* eslint-disable no-continue */ import {parsePhoneNumber} from 'awesome-phonenumber'; import Str from 'expensify-common/lib/str'; -// eslint-disable-next-line you-dont-need-lodash-underscore/get -import lodashGet from 'lodash/get'; import lodashOrderBy from 'lodash/orderBy'; import lodashSet from 'lodash/set'; -import lodashSortBy from 'lodash/sortBy'; -import lodashTimes from 'lodash/times'; import Onyx, {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import CONST from '@src/CONST'; import {TranslationPaths} from '@src/languages/types'; @@ -16,6 +12,9 @@ import {Participant} from '@src/types/onyx/IOU'; import * as OnyxCommon from '@src/types/onyx/OnyxCommon'; import DeepValueOf from '@src/types/utils/DeepValueOf'; import {EmptyObject, isEmptyObject, isNotEmptyObject} from '@src/types/utils/EmptyObject'; +import get from '@src/utils/get'; +import sortBy from '@src/utils/sortBy'; +import times from '@src/utils/times'; import * as CollectionUtils from './CollectionUtils'; import * as ErrorUtils from './ErrorUtils'; import * as LocalePhoneNumber from './LocalePhoneNumber'; @@ -31,13 +30,35 @@ import * as TaskUtils from './TaskUtils'; import * as TransactionUtils from './TransactionUtils'; import * as UserUtils from './UserUtils'; -type Tag = {enabled: boolean; name: string; accountID: number | null}; +type Tag = { + enabled: boolean; + name: string; + accountID: number | null; +}; -type Option = {text: string; keyForList: string; searchText: string; tooltipText: string; isDisabled: boolean}; +type Option = { + text: string | null; + keyForList: string; + searchText: string; + tooltipText: string; + isDisabled: boolean; +}; -type PayeePersonalDetails = {text: string; alternateText: string; icons: OnyxCommon.Icon[]; descriptiveText: string; login: string; accountID: number}; +type PayeePersonalDetails = { + text: string; + alternateText: string; + icons: OnyxCommon.Icon[]; + descriptiveText: string; + login: string; + accountID: number; +}; -type CategorySection = {title: string | undefined; shouldShow: boolean; indexOffset: number; data: Option[] | Participant[] | ReportUtils.OptionData[]}; +type CategorySection = { + title: string | undefined; + shouldShow: boolean; + indexOffset: number; + data: Option[] | Participant[] | ReportUtils.OptionData[]; +}; type Category = { name: string; @@ -478,7 +499,7 @@ function createOption( const result: ReportUtils.OptionData = { text: undefined, alternateText: null, - pendingAction: null, + pendingAction: undefined, allReportErrors: null, brickRoadIndicator: null, icons: undefined, @@ -532,7 +553,7 @@ function createOption( result.isOwnPolicyExpenseChat = report.isOwnPolicyExpenseChat ?? false; result.allReportErrors = getAllReportErrors(report, reportActions); result.brickRoadIndicator = isNotEmptyObject(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 : undefined; result.ownerAccountID = report.ownerAccountID; result.reportID = report.reportID; result.isUnread = ReportUtils.isUnread(report); @@ -540,7 +561,7 @@ function createOption( 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.isWaitingOnBankAccount = report.isWaitingOnBankAccount; result.policyID = report.policyID; hasMultipleParticipants = personalDetailList.length > 1 || result.isChatRoom || result.isPolicyExpenseChat; @@ -700,7 +721,7 @@ function sortCategories(categories: Record): Category[] { */ sortedCategories.forEach((category) => { const path = category.name.split(CONST.PARENT_CHILD_SEPARATOR); - const existedValue = lodashGet(hierarchy, path, {}); + const existedValue = get(hierarchy, path, {}); lodashSet(hierarchy, path, { ...existedValue, name: category.name, @@ -769,7 +790,7 @@ function indentOption(option: Category, optionCollection: Map, i } option.name.split(CONST.PARENT_CHILD_SEPARATOR).forEach((optionName, index, array) => { - const indents = lodashTimes(index, () => CONST.INDENTS).join(''); + const indents = times(index, () => CONST.INDENTS).join(''); const isChild = array.length - 1 === index; const searchText = array.slice(0, index + 1).join(CONST.PARENT_CHILD_SEPARATOR); @@ -1105,7 +1126,7 @@ function getOptions( // Sorting the reports works like this: // - Order everything by the last message timestamp (descending) // - All archived reports should remain at the bottom - const orderedReports = lodashSortBy(filteredReports, (report) => { + const orderedReports = sortBy(filteredReports, (report) => { if (ReportUtils.isArchivedRoom(report)) { return CONST.DATE.UNIX_EPOCH; } @@ -1179,7 +1200,7 @@ function getOptions( const filteredDetails: OnyxEntry = Object.keys(personalDetails ?? {}) .filter((key) => 'login' in (personalDetails?.[+key] ?? {})) .reduce((obj: OnyxEntry, key) => { - if (obj) { + if (obj && personalDetails?.[+key]) { // eslint-disable-next-line no-param-reassign obj[+key] = personalDetails?.[+key]; } @@ -1501,7 +1522,7 @@ function getShareDestinationOptions( * @param member - personalDetails or userToInvite * @param config - keys to overwrite the default values */ -function formatMemberForList(member: ReportUtils.OptionData, config: ReportUtils.OptionData | EmptyObject = {}): ReportUtils.OptionData | undefined { +function formatMemberForList(member: ReportUtils.OptionData, config: ReportUtils.OptionData | EmptyObject = {}): Option | undefined { if (!member) { return undefined; } @@ -1511,7 +1532,7 @@ function formatMemberForList(member: ReportUtils.OptionData, config: ReportUtils return { text: member.text ?? member.displayName ?? '', alternateText: member.alternateText ?? member.login ?? '', - keyForList: member.keyForList ?? String(accountID), + keyForList: member.keyForList ?? String(accountID ?? 0) ?? '', isSelected: false, isDisabled: false, accountID, diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 7743bf5a9eee..4343e518c1a3 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -331,6 +331,7 @@ type OptionData = { selected?: boolean; isOptimisticAccount?: boolean; isDisabled?: boolean; + isSelected?: boolean; } & Report; type OnyxDataTaskAssigneeChat = { diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index 522141c10888..77a726981051 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -6,7 +6,6 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import {PersonalDetails} from '@src/types/onyx'; import Beta from '@src/types/onyx/Beta'; -import * as OnyxCommon from '@src/types/onyx/OnyxCommon'; import Policy from '@src/types/onyx/Policy'; import Report from '@src/types/onyx/Report'; import ReportAction, {ReportActions} from '@src/types/onyx/ReportAction'; diff --git a/src/utils/get.ts b/src/utils/get.ts new file mode 100644 index 000000000000..41c720840f83 --- /dev/null +++ b/src/utils/get.ts @@ -0,0 +1,15 @@ +function get, U>(obj: T, path: string | string[], defValue?: U): T | U | undefined { + // If path is not defined or it has false value + if (!path || path.length === 0) { + return undefined; + } + // Check if path is string or array. Regex : ensure that we do not have '.' and brackets. + // Regex explained: https://regexr.com/58j0k + const pathArray = Array.isArray(path) ? path : path.match(/([^[.\]])+/g); + // Find value + const result = pathArray?.reduce((prevObj, key) => prevObj && (prevObj[key] as T), obj); + // If found value is undefined return default value; otherwise return the value + return result ?? defValue; +} + +export default get; diff --git a/src/utils/sortBy.ts b/src/utils/sortBy.ts new file mode 100644 index 000000000000..ae8a98c79564 --- /dev/null +++ b/src/utils/sortBy.ts @@ -0,0 +1,35 @@ +function sortBy(array: T[], keyOrFunction: keyof T | ((value: T) => unknown)): T[] { + return [...array].sort((a, b) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let aValue: any; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let bValue: any; + + // Check if a function was provided + if (typeof keyOrFunction === 'function') { + aValue = keyOrFunction(a); + bValue = keyOrFunction(b); + } else { + aValue = a[keyOrFunction]; + bValue = b[keyOrFunction]; + } + + // Convert dates to timestamps for comparison + if (aValue instanceof Date) { + aValue = aValue.getTime(); + } + if (bValue instanceof Date) { + bValue = bValue.getTime(); + } + + if (aValue < bValue) { + return -1; + } + if (aValue > bValue) { + return 1; + } + return 0; + }); +} + +export default sortBy; diff --git a/src/utils/times.ts b/src/utils/times.ts new file mode 100644 index 000000000000..91fbc1c1b412 --- /dev/null +++ b/src/utils/times.ts @@ -0,0 +1,6 @@ +function times(n: number, func = (i: number): string | number | undefined => i): Array { + // eslint-disable-next-line @typescript-eslint/naming-convention + return Array.from({length: n}).map((_, i) => func(i)); +} + +export default times; diff --git a/tests/unit/get.ts b/tests/unit/get.ts new file mode 100644 index 000000000000..ac19a5c6353d --- /dev/null +++ b/tests/unit/get.ts @@ -0,0 +1,27 @@ +import get from '@src/utils/get'; + +describe('get', () => { + it('should return the value at path of object', () => { + const obj = {a: {b: 2}}; + expect(get(obj, 'a.b', 0)).toBe(2); + expect(get(obj, ['a', 'b'], 0)).toBe(2); + }); + + it('should return undefined if path does not exist', () => { + const obj = {a: {b: 2}}; + expect(get(obj, 'a.c')).toBeUndefined(); + expect(get(obj, ['a', 'c'])).toBeUndefined(); + }); + + it('should return default value if path does not exist', () => { + const obj = {a: {b: 2}}; + expect(get(obj, 'a.c', 3)).toBe(3); + expect(get(obj, ['a', 'c'], 3)).toBe(3); + }); + + it('should return undefined if path is not defined or it has false value', () => { + const obj = {a: {b: 2}}; + expect(get(obj, '', 3)).toBeUndefined(); + expect(get(obj, [], 3)).toBeUndefined(); + }); +}); diff --git a/tests/unit/sortBy.ts b/tests/unit/sortBy.ts new file mode 100644 index 000000000000..bbd1333b974c --- /dev/null +++ b/tests/unit/sortBy.ts @@ -0,0 +1,21 @@ +import sortBy from '@src/utils/sortBy'; + +describe('sortBy', () => { + it('should sort by object key', () => { + const array = [{id: 3}, {id: 1}, {id: 2}]; + const sorted = sortBy(array, 'id'); + expect(sorted).toEqual([{id: 1}, {id: 2}, {id: 3}]); + }); + + it('should sort by function', () => { + const array = [{id: 3}, {id: 1}, {id: 2}]; + const sorted = sortBy(array, (obj) => obj.id); + expect(sorted).toEqual([{id: 1}, {id: 2}, {id: 3}]); + }); + + it('should sort by date', () => { + const array = [{date: new Date(2022, 1, 1)}, {date: new Date(2022, 0, 1)}, {date: new Date(2022, 2, 1)}]; + const sorted = sortBy(array, 'date'); + expect(sorted).toEqual([{date: new Date(2022, 0, 1)}, {date: new Date(2022, 1, 1)}, {date: new Date(2022, 2, 1)}]); + }); +}); diff --git a/tests/unit/times.ts b/tests/unit/times.ts new file mode 100644 index 000000000000..bc601b40be14 --- /dev/null +++ b/tests/unit/times.ts @@ -0,0 +1,33 @@ +import times from '@src/utils/times'; + +describe('times', () => { + it('should create an array of n elements', () => { + const result = times(3); + expect(result).toEqual([0, 1, 2]); + }); + + it('should create an array of n elements with values from the function', () => { + const result = times(3, (i) => i * 2); + expect(result).toEqual([0, 2, 4]); + }); + + it('should create an empty array if n is 0', () => { + const result = times(0); + expect(result).toEqual([]); + }); + + it('should create an array of undefined if no function is provided', () => { + const result = times(3, () => undefined); + expect(result).toEqual([undefined, undefined, undefined]); + }); + + it('should create an array of n elements with string values from the function', () => { + const result = times(3, (i) => `item ${i}`); + expect(result).toEqual(['item 0', 'item 1', 'item 2']); + }); + + it('should create an array of n elements with constant string value', () => { + const result = times(3, () => 'constant'); + expect(result).toEqual(['constant', 'constant', 'constant']); + }); +}); From 6d4fbadbc2417e215c75a98624217fb7d67e053e Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Wed, 13 Dec 2023 12:51:22 +0100 Subject: [PATCH 045/391] fix: types issues --- src/libs/OptionsListUtils.ts | 41 ++++++++++++++++++++++++++++-------- 1 file changed, 32 insertions(+), 9 deletions(-) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index fca8345a7323..0f7776e570a9 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -57,7 +57,7 @@ type CategorySection = { title: string | undefined; shouldShow: boolean; indexOffset: number; - data: Option[] | Participant[] | ReportUtils.OptionData[]; + data: Option[] | Array; }; type Category = { @@ -97,6 +97,33 @@ type GetOptionsConfig = { includeSelectedOptions?: boolean; }; +type MemberForList = { + text: string; + alternateText: string | null; + keyForList: string | null; + isSelected: boolean; + isDisabled: boolean; + accountID?: number | null; + login: string | null; + rightElement: React.ReactNode | null; + icons?: OnyxCommon.Icon[]; + pendingAction?: OnyxCommon.PendingAction; +}; + +type SectionForSearchTerm = { + section: CategorySection; + newIndexOffset: number; +}; + +type GetOptions = { + recentReports: ReportUtils.OptionData[]; + personalDetails: ReportUtils.OptionData[]; + userToInvite: ReportUtils.OptionData | null; + currentUserOption: ReportUtils.OptionData | null | undefined; + categoryOptions: CategorySection[]; + tagOptions: CategorySection[]; +}; + /** * 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 @@ -1076,7 +1103,7 @@ function getOptions( canInviteUser = true, includeSelectedOptions = false, }: GetOptionsConfig, -) { +): GetOptions { if (includeCategories) { const categoryOptions = getCategoryListSections(categories, recentlyUsedCategories, selectedOptions as Category[], searchInputValue, maxRecentReportsToShow); @@ -1391,7 +1418,7 @@ function getOptions( /** * Build the options for the Search view */ -function getSearchOptions(reports: Record, personalDetails: OnyxEntry, searchValue = '', betas: Beta[] = []) { +function getSearchOptions(reports: Record, personalDetails: OnyxEntry, searchValue = '', betas: Beta[] = []): GetOptions { return getOptions(reports, personalDetails, { betas, searchInputValue: searchValue.trim(), @@ -1522,7 +1549,7 @@ function getShareDestinationOptions( * @param member - personalDetails or userToInvite * @param config - keys to overwrite the default values */ -function formatMemberForList(member: ReportUtils.OptionData, config: ReportUtils.OptionData | EmptyObject = {}): Option | undefined { +function formatMemberForList(member: ReportUtils.OptionData, config: ReportUtils.OptionData | EmptyObject = {}): MemberForList | undefined { if (!member) { return undefined; } @@ -1547,7 +1574,7 @@ function formatMemberForList(member: ReportUtils.OptionData, config: ReportUtils /** * Build the options for the Workspace Member Invite view */ -function getMemberInviteOptions(personalDetails: OnyxEntry, betas: Beta[] = [], searchValue = '', excludeLogins: string[] = []) { +function getMemberInviteOptions(personalDetails: OnyxEntry, betas: Beta[] = [], searchValue = '', excludeLogins: string[] = []): GetOptions { return getOptions({}, personalDetails, { betas, searchInputValue: searchValue.trim(), @@ -1608,10 +1635,6 @@ function shouldOptionShowTooltip(option: ReportUtils.OptionData): boolean { return Boolean((!option.isChatRoom || option.isThread) && !option.isArchivedRoom); } -type SectionForSearchTerm = { - section: CategorySection; - newIndexOffset: number; -}; /** * Handles the logic for displaying selected participants from the search term */ From da5b440abe17fc27d9f87573c971d067912a06ee Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Wed, 13 Dec 2023 15:21:34 +0100 Subject: [PATCH 046/391] fix: resolve comments --- src/libs/OptionsListUtils.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 0f7776e570a9..34d63bfae12e 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -68,7 +68,7 @@ type Category = { type Hierarchy = Record; type GetOptionsConfig = { - reportActions?: Record; + reportActions?: ReportActions; betas?: Beta[]; selectedOptions?: Array; maxRecentReportsToShow?: number; @@ -174,7 +174,7 @@ Onyx.connect({ }, }); -const lastReportActions: Record = {}; +const lastReportActions: ReportActions = {}; const allSortedReportActions: Record = {}; const allReportActions: Record = {}; Onyx.connect({ @@ -261,7 +261,7 @@ function getAvatarsForAccountIDs(accountIDs: number[], personalDetails: OnyxEntr * Returns the personal details for an array of accountIDs * @returns keys of the object are emails, values are PersonalDetails objects. */ -function getPersonalDetailsForAccountIDs(accountIDs: number[], personalDetails: OnyxEntry): PersonalDetailsList { +function getPersonalDetailsForAccountIDs(accountIDs: number[] | undefined, personalDetails: OnyxEntry): PersonalDetailsList { const personalDetailsForAccountIDs: PersonalDetailsList = {}; if (!personalDetails) { return personalDetailsForAccountIDs; @@ -293,7 +293,7 @@ function getPersonalDetailsForAccountIDs(accountIDs: number[], personalDetails: */ function isPersonalDetailsReady(personalDetails: OnyxEntry): boolean { const personalDetailsKeys = Object.keys(personalDetails ?? {}); - return personalDetailsKeys.length > 0 && personalDetailsKeys.some((key) => personalDetails?.[Number(key)]?.accountID); + return personalDetailsKeys.some((key) => personalDetails?.[Number(key)]?.accountID); } /** @@ -520,7 +520,7 @@ function createOption( accountIDs: number[], personalDetails: OnyxEntry, report: OnyxEntry, - reportActions: Record, + reportActions: ReportActions, {showChatPreviewLine = false, forcePolicyNamePreview = false}: {showChatPreviewLine?: boolean; forcePolicyNamePreview?: boolean}, ): ReportUtils.OptionData { const result: ReportUtils.OptionData = { From 92e46e62f6e375cef02ce07da410ea7739ba1720 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Wed, 13 Dec 2023 16:04:40 +0100 Subject: [PATCH 047/391] fix: created lodash set eqiuvalent --- src/utils/set.ts | 15 +++++++++++++++ tests/unit/set.ts | 29 +++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+) create mode 100644 src/utils/set.ts create mode 100644 tests/unit/set.ts diff --git a/src/utils/set.ts b/src/utils/set.ts new file mode 100644 index 000000000000..9aa432638417 --- /dev/null +++ b/src/utils/set.ts @@ -0,0 +1,15 @@ +function set, U>(obj: T, path: string | string[], value: U): void { + const pathArray = Array.isArray(path) ? path : path.split('.'); + + pathArray.reduce((acc: Record, key: string, i: number) => { + if (acc[key] === undefined) { + acc[key] = {}; + } + if (i === pathArray.length - 1) { + (acc[key] as U) = value; + } + return acc[key] as Record; + }, obj); +} + +export default set; diff --git a/tests/unit/set.ts b/tests/unit/set.ts new file mode 100644 index 000000000000..221f18bf0039 --- /dev/null +++ b/tests/unit/set.ts @@ -0,0 +1,29 @@ +import set from '@src/utils/set'; + +describe('set', () => { + it('should set the value at path of object', () => { + const obj = {a: {b: 2}}; + set(obj, 'a.b', 3); + expect(obj.a.b).toBe(3); + }); + + it('should set the value at path of object (array path)', () => { + const obj = {a: {b: 2}}; + set(obj, ['a', 'b'], 3); + expect(obj.a.b).toBe(3); + }); + + it('should create nested properties if they do not exist', () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const obj: any = {a: {}}; + set(obj, 'a.b.c', 3); + expect(obj.a.b.c).toBe(3); + }); + + it('should handle root-level properties', () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const obj: any = {a: 1}; + set(obj, 'b', 2); + expect(obj.b).toBe(2); + }); +}); From 3ecd7f9378a6a8c2ced4a172a9a9e330b6e994e8 Mon Sep 17 00:00:00 2001 From: Fitsum Abebe Date: Wed, 13 Dec 2023 18:28:35 +0300 Subject: [PATCH 048/391] reset workspace avatar in optimistic data --- src/libs/actions/Policy.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libs/actions/Policy.js b/src/libs/actions/Policy.js index 04f62ab0c393..6ad39dab041a 100644 --- a/src/libs/actions/Policy.js +++ b/src/libs/actions/Policy.js @@ -171,6 +171,7 @@ function deleteWorkspace(policyID, reports, policyName) { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, value: { + avatar: '', pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, errors: null, }, From 40df8fda377ed115332d05cfd5bf7ff99fcb3ed7 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Wed, 13 Dec 2023 17:21:29 +0100 Subject: [PATCH 049/391] fix: types error --- src/components/AvatarWithDisplayName.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/AvatarWithDisplayName.tsx b/src/components/AvatarWithDisplayName.tsx index 041c180595f1..b2d461a7a128 100644 --- a/src/components/AvatarWithDisplayName.tsx +++ b/src/components/AvatarWithDisplayName.tsx @@ -11,7 +11,7 @@ import useThemeStyles from '@styles/useThemeStyles'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import {PersonalDetails, Policy, Report, ReportActions} from '@src/types/onyx'; +import {PersonalDetailsList, Policy, Report, ReportActions} from '@src/types/onyx'; import DisplayNames from './DisplayNames'; import MultipleAvatars from './MultipleAvatars'; import ParentNavigationSubtitle from './ParentNavigationSubtitle'; @@ -35,7 +35,7 @@ type AvatarWithDisplayNameProps = AvatarWithDisplayNamePropsWithOnyx & { size?: ValueOf; /** Personal details of all the users */ - personalDetails: OnyxCollection; + personalDetails: OnyxEntry; /** Whether if it's an unauthenticated user */ isAnonymous?: boolean; From f1e7619059aa7c9c3042268ca0c2f9999de0422a Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Wed, 13 Dec 2023 17:39:02 +0100 Subject: [PATCH 050/391] fix: lint errors --- src/components/AnonymousReportFooter.tsx | 5 ++--- src/components/AvatarWithDisplayName.tsx | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/components/AnonymousReportFooter.tsx b/src/components/AnonymousReportFooter.tsx index 65dc813a829d..b965ee450cce 100644 --- a/src/components/AnonymousReportFooter.tsx +++ b/src/components/AnonymousReportFooter.tsx @@ -1,11 +1,10 @@ import React from 'react'; import {Text, View} from 'react-native'; -import {OnyxCollection} from 'react-native-onyx'; import {OnyxEntry} from 'react-native-onyx/lib/types'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@styles/useThemeStyles'; import * as Session from '@userActions/Session'; -import {PersonalDetails, Report} from '@src/types/onyx'; +import {PersonalDetailsList, Report} from '@src/types/onyx'; import AvatarWithDisplayName from './AvatarWithDisplayName'; import Button from './Button'; import ExpensifyWordmark from './ExpensifyWordmark'; @@ -18,7 +17,7 @@ type AnonymousReportFooterProps = { isSmallSizeLayout?: boolean; /** Personal details of all the users */ - personalDetails: OnyxCollection; + personalDetails: OnyxEntry; }; function AnonymousReportFooter({isSmallSizeLayout = false, personalDetails, report}: AnonymousReportFooterProps) { diff --git a/src/components/AvatarWithDisplayName.tsx b/src/components/AvatarWithDisplayName.tsx index b2d461a7a128..1a83b3c24bf3 100644 --- a/src/components/AvatarWithDisplayName.tsx +++ b/src/components/AvatarWithDisplayName.tsx @@ -1,6 +1,6 @@ import React, {useCallback, useEffect, useRef} from 'react'; import {View} from 'react-native'; -import {OnyxCollection, OnyxEntry, withOnyx} from 'react-native-onyx'; +import {OnyxEntry, withOnyx} from 'react-native-onyx'; import {ValueOf} from 'type-fest'; import Navigation from '@libs/Navigation/Navigation'; import * as OptionsListUtils from '@libs/OptionsListUtils'; From 4eeca487836db481054a09ee65c084b2c4011271 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Wed, 13 Dec 2023 19:12:06 +0100 Subject: [PATCH 051/391] Start migrating new form --- src/ONYXKEYS.ts | 10 +- src/components/Form/FormContext.js | 4 - src/components/Form/FormContext.tsx | 13 ++ src/components/Form/FormProvider.js | 24 +++ src/components/Form/FormWrapper.js | 217 ----------------------- src/components/Form/FormWrapper.tsx | 151 ++++++++++++++++ src/components/Form/InputWrapper.js | 45 ----- src/components/Form/InputWrapper.tsx | 21 +++ src/components/Form/errorsPropType.js | 11 -- src/components/Form/types.ts | 64 +++++++ src/components/SafeAreaConsumer/types.ts | 6 +- src/components/ScrollViewWithContext.tsx | 22 +-- 12 files changed, 293 insertions(+), 295 deletions(-) delete mode 100644 src/components/Form/FormContext.js create mode 100644 src/components/Form/FormContext.tsx delete mode 100644 src/components/Form/FormWrapper.js create mode 100644 src/components/Form/FormWrapper.tsx delete mode 100644 src/components/Form/InputWrapper.js create mode 100644 src/components/Form/InputWrapper.tsx delete mode 100644 src/components/Form/errorsPropType.js create mode 100644 src/components/Form/types.ts diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index a268c008cee8..402d1623b06c 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -462,8 +462,8 @@ type OnyxValues = { [ONYXKEYS.COLLECTION.PRIVATE_NOTES_DRAFT]: string; // Forms - [ONYXKEYS.FORMS.ADD_DEBIT_CARD_FORM]: OnyxTypes.AddDebitCardForm; - [ONYXKEYS.FORMS.ADD_DEBIT_CARD_FORM_DRAFT]: OnyxTypes.AddDebitCardForm; + [ONYXKEYS.FORMS.ADD_DEBIT_CARD_FORM]: OnyxTypes.Form; + [ONYXKEYS.FORMS.ADD_DEBIT_CARD_FORM_DRAFT]: OnyxTypes.Form; [ONYXKEYS.FORMS.WORKSPACE_SETTINGS_FORM]: OnyxTypes.Form; [ONYXKEYS.FORMS.WORKSPACE_SETTINGS_FORM_DRAFT]: OnyxTypes.Form; [ONYXKEYS.FORMS.WORKSPACE_RATE_AND_UNIT_FORM]: OnyxTypes.Form; @@ -482,8 +482,8 @@ type OnyxValues = { [ONYXKEYS.FORMS.LEGAL_NAME_FORM_DRAFT]: OnyxTypes.Form; [ONYXKEYS.FORMS.WORKSPACE_INVITE_MESSAGE_FORM]: OnyxTypes.Form; [ONYXKEYS.FORMS.WORKSPACE_INVITE_MESSAGE_FORM_DRAFT]: OnyxTypes.Form; - [ONYXKEYS.FORMS.DATE_OF_BIRTH_FORM]: OnyxTypes.DateOfBirthForm; - [ONYXKEYS.FORMS.DATE_OF_BIRTH_FORM_DRAFT]: OnyxTypes.DateOfBirthForm; + [ONYXKEYS.FORMS.DATE_OF_BIRTH_FORM]: OnyxTypes.Form; + [ONYXKEYS.FORMS.DATE_OF_BIRTH_FORM_DRAFT]: OnyxTypes.Form; [ONYXKEYS.FORMS.HOME_ADDRESS_FORM]: OnyxTypes.Form; [ONYXKEYS.FORMS.HOME_ADDRESS_FORM_DRAFT]: OnyxTypes.Form; [ONYXKEYS.FORMS.NEW_ROOM_FORM]: OnyxTypes.Form; @@ -523,7 +523,7 @@ type OnyxValues = { [ONYXKEYS.FORMS.REPORT_PHYSICAL_CARD_FORM]: OnyxTypes.Form; [ONYXKEYS.FORMS.REPORT_PHYSICAL_CARD_FORM_DRAFT]: OnyxTypes.Form; [ONYXKEYS.FORMS.GET_PHYSICAL_CARD_FORM]: OnyxTypes.Form; - [ONYXKEYS.FORMS.GET_PHYSICAL_CARD_FORM_DRAFT]: OnyxTypes.Form | undefined; + [ONYXKEYS.FORMS.GET_PHYSICAL_CARD_FORM_DRAFT]: OnyxTypes.Form; }; type OnyxKeyValue = OnyxEntry; diff --git a/src/components/Form/FormContext.js b/src/components/Form/FormContext.js deleted file mode 100644 index 40edaa7cca69..000000000000 --- a/src/components/Form/FormContext.js +++ /dev/null @@ -1,4 +0,0 @@ -import {createContext} from 'react'; - -const FormContext = createContext({}); -export default FormContext; diff --git a/src/components/Form/FormContext.tsx b/src/components/Form/FormContext.tsx new file mode 100644 index 000000000000..23a2ea615eda --- /dev/null +++ b/src/components/Form/FormContext.tsx @@ -0,0 +1,13 @@ +import {createContext} from 'react'; + +type FormContextType = { + registerInput: (key: string, ref: any) => object; +}; + +const FormContext = createContext({ + registerInput: () => { + throw new Error('Registered input should be wrapped with FormWrapper'); + }, +}); + +export default FormContext; diff --git a/src/components/Form/FormProvider.js b/src/components/Form/FormProvider.js index 63953d8303db..cbfc6a7315ca 100644 --- a/src/components/Form/FormProvider.js +++ b/src/components/Form/FormProvider.js @@ -14,6 +14,30 @@ import CONST from '@src/CONST'; import FormContext from './FormContext'; import FormWrapper from './FormWrapper'; +// type ErrorsType = string | Record>; +// const errorsPropType = PropTypes.oneOfType([ +// PropTypes.string, +// PropTypes.objectOf( +// PropTypes.oneOfType([ +// PropTypes.string, +// PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.objectOf(PropTypes.oneOfType([PropTypes.string, PropTypes.bool, PropTypes.number]))])), +// ]), +// ), +// ]); + +// const defaultProps = { +// isSubmitButtonVisible: true, +// formState: { +// isLoading: false, +// }, +// enabledWhenOffline: false, +// isSubmitActionDangerous: false, +// scrollContextEnabled: false, +// footerContent: null, +// style: [], +// submitButtonStyles: [], +// }; + const propTypes = { /** A unique Onyx key identifying the form */ formID: PropTypes.string.isRequired, diff --git a/src/components/Form/FormWrapper.js b/src/components/Form/FormWrapper.js deleted file mode 100644 index 638b6e5f8d19..000000000000 --- a/src/components/Form/FormWrapper.js +++ /dev/null @@ -1,217 +0,0 @@ -import PropTypes from 'prop-types'; -import React, {useCallback, useMemo, useRef} from 'react'; -import {Keyboard, ScrollView, StyleSheet} from 'react-native'; -import {withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; -import FormAlertWithSubmitButton from '@components/FormAlertWithSubmitButton'; -import FormSubmit from '@components/FormSubmit'; -import refPropTypes from '@components/refPropTypes'; -import SafeAreaConsumer from '@components/SafeAreaConsumer'; -import ScrollViewWithContext from '@components/ScrollViewWithContext'; -import * as ErrorUtils from '@libs/ErrorUtils'; -import stylePropTypes from '@styles/stylePropTypes'; -import useThemeStyles from '@styles/useThemeStyles'; -import errorsPropType from './errorsPropType'; - -const propTypes = { - /** A unique Onyx key identifying the form */ - formID: PropTypes.string.isRequired, - - /** Text to be displayed in the submit button */ - submitButtonText: PropTypes.string.isRequired, - - /** Controls the submit button's visibility */ - isSubmitButtonVisible: PropTypes.bool, - - /** Callback to submit the form */ - onSubmit: PropTypes.func.isRequired, - - /** Children to render. */ - children: PropTypes.oneOfType([PropTypes.func, PropTypes.node]).isRequired, - - /* Onyx Props */ - - /** Contains the form state that must be accessed outside of the component */ - formState: PropTypes.shape({ - /** Controls the loading state of the form */ - isLoading: PropTypes.bool, - - /** Server side errors keyed by microtime */ - errors: errorsPropType, - - /** Field-specific server side errors keyed by microtime */ - errorFields: PropTypes.objectOf(PropTypes.objectOf(PropTypes.string)), - }), - - /** Should the button be enabled when offline */ - enabledWhenOffline: PropTypes.bool, - - /** Whether the form submit action is dangerous */ - isSubmitActionDangerous: PropTypes.bool, - - /** Whether ScrollWithContext should be used instead of regular ScrollView. - * Set to true when there's a nested Picker component in Form. - */ - scrollContextEnabled: PropTypes.bool, - - /** Container styles */ - style: stylePropTypes, - - /** Submit button styles */ - submitButtonStyles: stylePropTypes, - - /** Custom content to display in the footer after submit button */ - footerContent: PropTypes.oneOfType([PropTypes.func, PropTypes.node]), - - errors: errorsPropType.isRequired, - - inputRefs: PropTypes.objectOf(refPropTypes).isRequired, -}; - -const defaultProps = { - isSubmitButtonVisible: true, - formState: { - isLoading: false, - }, - enabledWhenOffline: false, - isSubmitActionDangerous: false, - scrollContextEnabled: false, - footerContent: null, - style: [], - submitButtonStyles: [], -}; - -function FormWrapper(props) { - const styles = useThemeStyles(); - const { - onSubmit, - children, - formState, - errors, - inputRefs, - submitButtonText, - footerContent, - isSubmitButtonVisible, - style, - submitButtonStyles, - enabledWhenOffline, - isSubmitActionDangerous, - formID, - } = props; - const formRef = useRef(null); - const formContentRef = useRef(null); - const errorMessage = useMemo(() => { - const latestErrorMessage = ErrorUtils.getLatestErrorMessage(formState); - return typeof latestErrorMessage === 'string' ? latestErrorMessage : ''; - }, [formState]); - - const scrollViewContent = useCallback( - (safeAreaPaddingBottomStyle) => ( - - {children} - {isSubmitButtonVisible && ( - 0 || Boolean(errorMessage) || !_.isEmpty(formState.errorFields)} - isLoading={formState.isLoading} - message={_.isEmpty(formState.errorFields) ? errorMessage : null} - onSubmit={onSubmit} - footerContent={footerContent} - onFixTheErrorsLinkPressed={() => { - const errorFields = !_.isEmpty(errors) ? errors : formState.errorFields; - const focusKey = _.find(_.keys(inputRefs.current), (key) => _.keys(errorFields).includes(key)); - const focusInput = inputRefs.current[focusKey].current; - - // Dismiss the keyboard for non-text fields by checking if the component has the isFocused method, as only TextInput has this method. - if (typeof focusInput.isFocused !== 'function') { - Keyboard.dismiss(); - } - - // We subtract 10 to scroll slightly above the input - if (focusInput.measureLayout && typeof focusInput.measureLayout === 'function') { - // We measure relative to the content root, not the scroll view, as that gives - // consistent results across mobile and web - focusInput.measureLayout(formContentRef.current, (x, y) => - formRef.current.scrollTo({ - y: y - 10, - animated: false, - }), - ); - } - - // Focus the input after scrolling, as on the Web it gives a slightly better visual result - if (focusInput.focus && typeof focusInput.focus === 'function') { - focusInput.focus(); - } - }} - containerStyles={[styles.mh0, styles.mt5, styles.flex1, ...submitButtonStyles]} - enabledWhenOffline={enabledWhenOffline} - isSubmitActionDangerous={isSubmitActionDangerous} - disablePressOnEnter - /> - )} - - ), - [ - children, - enabledWhenOffline, - errorMessage, - errors, - footerContent, - formID, - formState.errorFields, - formState.isLoading, - inputRefs, - isSubmitActionDangerous, - isSubmitButtonVisible, - onSubmit, - style, - styles.flex1, - styles.mh0, - styles.mt5, - submitButtonStyles, - submitButtonText, - ], - ); - - return ( - - {({safeAreaPaddingBottomStyle}) => - props.scrollContextEnabled ? ( - - {scrollViewContent(safeAreaPaddingBottomStyle)} - - ) : ( - - {scrollViewContent(safeAreaPaddingBottomStyle)} - - ) - } - - ); -} - -FormWrapper.displayName = 'FormWrapper'; -FormWrapper.propTypes = propTypes; -FormWrapper.defaultProps = defaultProps; - -export default withOnyx({ - formState: { - key: (props) => props.formID, - }, -})(FormWrapper); diff --git a/src/components/Form/FormWrapper.tsx b/src/components/Form/FormWrapper.tsx new file mode 100644 index 000000000000..705ad5e0b6c2 --- /dev/null +++ b/src/components/Form/FormWrapper.tsx @@ -0,0 +1,151 @@ +import React, {useCallback, useMemo, useRef} from 'react'; +import {Keyboard, ScrollView, View} from 'react-native'; +import {withOnyx} from 'react-native-onyx'; +import FormAlertWithSubmitButton from '@components/FormAlertWithSubmitButton'; +import FormSubmit from '@components/FormSubmit'; +import SafeAreaConsumer from '@components/SafeAreaConsumer'; +import {SafeAreaChildrenProps} from '@components/SafeAreaConsumer/types'; +import ScrollViewWithContext from '@components/ScrollViewWithContext'; +import * as ErrorUtils from '@libs/ErrorUtils'; +import useThemeStyles from '@styles/useThemeStyles'; +import ONYXKEYS from '@src/ONYXKEYS'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; +import {FormWrapperOnyxProps, FormWrapperProps} from './types'; + +function FormWrapper({ + onSubmit, + children, + formState, + errors, + inputRefs, + submitButtonText, + footerContent, + isSubmitButtonVisible, + style, + submitButtonStyles, + enabledWhenOffline, + isSubmitActionDangerous, + formID, + scrollContextEnabled, +}: FormWrapperProps) { + const styles = useThemeStyles(); + const formRef = useRef(null); + const formContentRef = useRef(null); + const errorMessage = useMemo(() => formState && ErrorUtils.getLatestErrorMessage(formState), [formState]); + + const scrollViewContent = useCallback( + (safeAreaPaddingBottomStyle: SafeAreaChildrenProps['safeAreaPaddingBottomStyle']) => ( + + {children} + {isSubmitButtonVisible && ( + { + const errorFields = !isEmptyObject(errors) ? errors : formState?.errorFields ?? {}; + const focusKey = Object.keys(inputRefs.current ?? {}).find((key) => Object.keys(errorFields).includes(key)); + + if (!focusKey) { + return; + } + + const focusInput = inputRefs.current?.[focusKey].current; + + // Dismiss the keyboard for non-text fields by checking if the component has the isFocused method, as only TextInput has this method. + if (typeof focusInput?.isFocused !== 'function') { + Keyboard.dismiss(); + } + + // We subtract 10 to scroll slightly above the input + if (focusInput?.measureLayout && formContentRef.current && typeof focusInput.measureLayout === 'function') { + // We measure relative to the content root, not the scroll view, as that gives + // consistent results across mobile and web + // eslint-disable-next-line @typescript-eslint/naming-convention + focusInput.measureLayout(formContentRef.current, (_x, y) => + formRef.current?.scrollTo({ + y: y - 10, + animated: false, + }), + ); + } + + // Focus the input after scrolling, as on the Web it gives a slightly better visual result + if (focusInput?.focus && typeof focusInput.focus === 'function') { + focusInput.focus(); + } + }} + // @ts-expect-error FormAlertWithSubmitButton migration + containerStyles={[styles.mh0, styles.mt5, styles.flex1, submitButtonStyles]} + enabledWhenOffline={enabledWhenOffline} + isSubmitActionDangerous={isSubmitActionDangerous} + disablePressOnEnter + /> + )} + + ), + [ + children, + enabledWhenOffline, + errorMessage, + errors, + footerContent, + formID, + formState?.errorFields, + formState?.isLoading, + inputRefs, + isSubmitActionDangerous, + isSubmitButtonVisible, + onSubmit, + style, + styles.flex1, + styles.mh0, + styles.mt5, + submitButtonStyles, + submitButtonText, + ], + ); + + return ( + + {({safeAreaPaddingBottomStyle}) => + scrollContextEnabled ? ( + + {scrollViewContent(safeAreaPaddingBottomStyle)} + + ) : ( + + {scrollViewContent(safeAreaPaddingBottomStyle)} + + ) + } + + ); +} + +FormWrapper.displayName = 'FormWrapper'; + +export default withOnyx({ + formState: { + // FIX: Fabio plz help 😂 + key: (props) => props.formID as typeof ONYXKEYS.FORMS.ADD_DEBIT_CARD_FORM, + }, +})(FormWrapper); diff --git a/src/components/Form/InputWrapper.js b/src/components/Form/InputWrapper.js deleted file mode 100644 index 9a31210195c4..000000000000 --- a/src/components/Form/InputWrapper.js +++ /dev/null @@ -1,45 +0,0 @@ -import PropTypes from 'prop-types'; -import React, {forwardRef, useContext} from 'react'; -import refPropTypes from '@components/refPropTypes'; -import TextInput from '@components/TextInput'; -import FormContext from './FormContext'; - -const propTypes = { - InputComponent: PropTypes.oneOfType([PropTypes.func, PropTypes.elementType]).isRequired, - inputID: PropTypes.string.isRequired, - valueType: PropTypes.string, - forwardedRef: refPropTypes, -}; - -const defaultProps = { - forwardedRef: undefined, - valueType: 'string', -}; - -function InputWrapper(props) { - const {InputComponent, inputID, forwardedRef, ...rest} = props; - const {registerInput} = useContext(FormContext); - // There are inputs that dont have onBlur methods, to simulate the behavior of onBlur in e.g. checkbox, we had to - // use different methods like onPress. This introduced a problem that inputs that have the onBlur method were - // calling some methods too early or twice, so we had to add this check to prevent that side effect. - // For now this side effect happened only in `TextInput` components. - const shouldSetTouchedOnBlurOnly = InputComponent === TextInput; - // eslint-disable-next-line react/jsx-props-no-spreading - return ; -} - -InputWrapper.propTypes = propTypes; -InputWrapper.defaultProps = defaultProps; -InputWrapper.displayName = 'InputWrapper'; - -const InputWrapperWithRef = forwardRef((props, ref) => ( - -)); - -InputWrapperWithRef.displayName = 'InputWrapperWithRef'; - -export default InputWrapperWithRef; diff --git a/src/components/Form/InputWrapper.tsx b/src/components/Form/InputWrapper.tsx new file mode 100644 index 000000000000..1b32409ea1d2 --- /dev/null +++ b/src/components/Form/InputWrapper.tsx @@ -0,0 +1,21 @@ +import React, {ForwardedRef, forwardRef, useContext} from 'react'; +import TextInput from '@components/TextInput'; +import FormContext from './FormContext'; +import {InputWrapperProps} from './types'; + +function InputWrapper({InputComponent, inputID, valueType = 'string', ...rest}: InputWrapperProps, ref: ForwardedRef) { + const {registerInput} = useContext(FormContext); + + // There are inputs that don't have onBlur methods, to simulate the behavior of onBlur in e.g. checkbox, we had to + // use different methods like onPress. This introduced a problem that inputs that have the onBlur method were + // calling some methods too early or twice, so we had to add this check to prevent that side effect. + // For now this side effect happened only in `TextInput` components. + const shouldSetTouchedOnBlurOnly = InputComponent === TextInput; + + // eslint-disable-next-line react/jsx-props-no-spreading + return ; +} + +InputWrapper.displayName = 'InputWrapper'; + +export default forwardRef(InputWrapper); diff --git a/src/components/Form/errorsPropType.js b/src/components/Form/errorsPropType.js deleted file mode 100644 index 3a02bb74e942..000000000000 --- a/src/components/Form/errorsPropType.js +++ /dev/null @@ -1,11 +0,0 @@ -import PropTypes from 'prop-types'; - -export default PropTypes.oneOfType([ - PropTypes.string, - PropTypes.objectOf( - PropTypes.oneOfType([ - PropTypes.string, - PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.objectOf(PropTypes.oneOfType([PropTypes.string, PropTypes.bool, PropTypes.number]))])), - ]), - ), -]); diff --git a/src/components/Form/types.ts b/src/components/Form/types.ts new file mode 100644 index 000000000000..8db4909327e0 --- /dev/null +++ b/src/components/Form/types.ts @@ -0,0 +1,64 @@ +import {ElementType, ReactNode, RefObject} from 'react'; +import {StyleProp, TextInput, ViewStyle} from 'react-native'; +import {OnyxEntry} from 'react-native-onyx'; +import {ValueOf} from 'type-fest'; +import ONYXKEYS from '@src/ONYXKEYS'; +import Form from '@src/types/onyx/Form'; +import {Errors} from '@src/types/onyx/OnyxCommon'; +import ChildrenProps from '@src/types/utils/ChildrenProps'; + +type ValueType = 'string' | 'boolean' | 'date'; + +type InputWrapperProps = { + InputComponent: TInput; + inputID: string; + valueType?: ValueType; +}; + +type FormWrapperOnyxProps = { + /** Contains the form state that must be accessed outside of the component */ + formState: OnyxEntry
; +}; + +type FormWrapperProps = ChildrenProps & + FormWrapperOnyxProps & { + /** A unique Onyx key identifying the form */ + formID: ValueOf; + + /** Text to be displayed in the submit button */ + submitButtonText: string; + + /** Controls the submit button's visibility */ + isSubmitButtonVisible?: boolean; + + /** Callback to submit the form */ + onSubmit: () => void; + + /** Should the button be enabled when offline */ + enabledWhenOffline?: boolean; + + /** Whether the form submit action is dangerous */ + isSubmitActionDangerous?: boolean; + + /** Whether ScrollWithContext should be used instead of regular ScrollView. + * Set to true when there's a nested Picker component in Form. + */ + scrollContextEnabled?: boolean; + + /** Container styles */ + style?: StyleProp; + + /** Submit button styles */ + submitButtonStyles?: StyleProp; + + /** Custom content to display in the footer after submit button */ + footerContent?: ReactNode; + + /** Server side errors keyed by microtime */ + errors: Errors; + + // Assuming refs are React refs + inputRefs: RefObject>>; + }; + +export type {InputWrapperProps, FormWrapperProps, FormWrapperOnyxProps}; diff --git a/src/components/SafeAreaConsumer/types.ts b/src/components/SafeAreaConsumer/types.ts index bc81de96a082..8e162a3b37fc 100644 --- a/src/components/SafeAreaConsumer/types.ts +++ b/src/components/SafeAreaConsumer/types.ts @@ -1,7 +1,7 @@ import {DimensionValue} from 'react-native'; import {EdgeInsets} from 'react-native-safe-area-context'; -type ChildrenProps = { +type SafeAreaChildrenProps = { paddingTop?: DimensionValue; paddingBottom?: DimensionValue; insets?: EdgeInsets; @@ -11,7 +11,9 @@ type ChildrenProps = { }; type SafeAreaConsumerProps = { - children: React.FC; + children: React.FC; }; export default SafeAreaConsumerProps; + +export type {SafeAreaChildrenProps}; diff --git a/src/components/ScrollViewWithContext.tsx b/src/components/ScrollViewWithContext.tsx index 7c75ae2f71b2..285da94092c2 100644 --- a/src/components/ScrollViewWithContext.tsx +++ b/src/components/ScrollViewWithContext.tsx @@ -1,5 +1,5 @@ -import React, {ForwardedRef, useMemo, useRef, useState} from 'react'; -import {NativeScrollEvent, NativeSyntheticEvent, ScrollView} from 'react-native'; +import React, {createContext, ForwardedRef, forwardRef, ReactNode, useMemo, useRef, useState} from 'react'; +import {NativeScrollEvent, NativeSyntheticEvent, ScrollView, ScrollViewProps} from 'react-native'; const MIN_SMOOTH_SCROLL_EVENT_THROTTLE = 16; @@ -8,16 +8,16 @@ type ScrollContextValue = { scrollViewRef: ForwardedRef; }; -const ScrollContext = React.createContext({ +const ScrollContext = createContext({ contentOffsetY: 0, scrollViewRef: null, }); type ScrollViewWithContextProps = { - onScroll: (event: NativeSyntheticEvent) => void; - children?: React.ReactNode; - scrollEventThrottle: number; -} & Partial; + onScroll?: (event: NativeSyntheticEvent) => void; + children?: ReactNode; + scrollEventThrottle?: number; +} & Partial; /* * is a wrapper around that provides a ref to the . @@ -26,7 +26,7 @@ type ScrollViewWithContextProps = { * Using this wrapper will automatically handle scrolling to the picker's * when the picker modal is opened */ -function ScrollViewWithContextWithRef({onScroll, scrollEventThrottle, children, ...restProps}: ScrollViewWithContextProps, ref: ForwardedRef) { +function ScrollViewWithContext({onScroll, scrollEventThrottle, children, ...restProps}: ScrollViewWithContextProps, ref: ForwardedRef) { const [contentOffsetY, setContentOffsetY] = useState(0); const defaultScrollViewRef = useRef(null); const scrollViewRef = ref ?? defaultScrollViewRef; @@ -52,15 +52,15 @@ function ScrollViewWithContextWithRef({onScroll, scrollEventThrottle, children, {...restProps} ref={scrollViewRef} onScroll={setContextScrollPosition} - scrollEventThrottle={scrollEventThrottle || MIN_SMOOTH_SCROLL_EVENT_THROTTLE} + scrollEventThrottle={scrollEventThrottle ?? MIN_SMOOTH_SCROLL_EVENT_THROTTLE} > {children} ); } -ScrollViewWithContextWithRef.displayName = 'ScrollViewWithContextWithRef'; +ScrollViewWithContext.displayName = 'ScrollViewWithContext'; -export default React.forwardRef(ScrollViewWithContextWithRef); +export default forwardRef(ScrollViewWithContext); export {ScrollContext}; export type {ScrollContextValue}; From ef06c1f4d00c4685b5cd8c11c7e95c91676a1461 Mon Sep 17 00:00:00 2001 From: brunovjk Date: Thu, 14 Dec 2023 15:36:28 -0300 Subject: [PATCH 052/391] Revert "Conditionally update chatOptions on screen transition end" This reverts commit 19d6d6856cd2b713a8da56b47a8b4c35d444a7e8. --- .../MoneyRequestParticipantsSelector.js | 64 +++++++++---------- 1 file changed, 31 insertions(+), 33 deletions(-) diff --git a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js index 14e94db38202..8c0fc71b0112 100755 --- a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js +++ b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js @@ -230,39 +230,37 @@ function MoneyRequestParticipantsSelector({ const isOptionsDataReady = ReportUtils.isReportDataReady() && OptionsListUtils.isPersonalDetailsReady(personalDetails); useEffect(() => { - if (didScreenTransitionEnd) { - const chatOptions = OptionsListUtils.getFilteredOptions( - reports, - personalDetails, - betas, - searchTerm, - participants, - CONST.EXPENSIFY_EMAILS, - - // If we are using this component in the "Request money" flow then we pass the includeOwnedWorkspaceChats argument so that the current user - // sees the option to request money from their admin on their own Workspace Chat. - iouType === CONST.IOU.TYPE.REQUEST, - - // We don't want to include any P2P options like personal details or reports that are not workspace chats for certain features. - !isDistanceRequest, - false, - {}, - [], - false, - {}, - [], - // We don't want the user to be able to invite individuals when they are in the "Distance request" flow for now. - // This functionality is being built here: https://github.com/Expensify/App/issues/23291 - !isDistanceRequest, - true, - ); - setNewChatOptions({ - recentReports: chatOptions.recentReports, - personalDetails: chatOptions.personalDetails, - userToInvite: chatOptions.userToInvite, - }); - } - }, [betas, reports, participants, personalDetails, translate, searchTerm, setNewChatOptions, iouType, isDistanceRequest, didScreenTransitionEnd]); + const chatOptions = OptionsListUtils.getFilteredOptions( + reports, + personalDetails, + betas, + searchTerm, + participants, + CONST.EXPENSIFY_EMAILS, + + // If we are using this component in the "Request money" flow then we pass the includeOwnedWorkspaceChats argument so that the current user + // sees the option to request money from their admin on their own Workspace Chat. + iouType === CONST.IOU.TYPE.REQUEST, + + // We don't want to include any P2P options like personal details or reports that are not workspace chats for certain features. + !isDistanceRequest, + false, + {}, + [], + false, + {}, + [], + // We don't want the user to be able to invite individuals when they are in the "Distance request" flow for now. + // This functionality is being built here: https://github.com/Expensify/App/issues/23291 + !isDistanceRequest, + true, + ); + setNewChatOptions({ + recentReports: chatOptions.recentReports, + personalDetails: chatOptions.personalDetails, + userToInvite: chatOptions.userToInvite, + }); + }, [betas, reports, participants, personalDetails, translate, searchTerm, setNewChatOptions, iouType, isDistanceRequest]); // When search term updates we will fetch any reports const setSearchTermAndSearchInServer = useCallback((text = '') => { From 1108784a7451d387dc9ab2d54482c1b792903925 Mon Sep 17 00:00:00 2001 From: brunovjk Date: Thu, 14 Dec 2023 15:37:56 -0300 Subject: [PATCH 053/391] Revert "Integrate 'didScreenTransitionEnd' prop in MoneyRequestParticipantsPage" This reverts commit a5ed7d8ddc0cdcb9d1caa0da9e391bcb3b751c04. --- .../MoneyRequestParticipantsPage.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsPage.js b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsPage.js index a2073f9f5d44..7826643d4283 100644 --- a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsPage.js +++ b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsPage.js @@ -133,7 +133,7 @@ function MoneyRequestParticipantsPage({iou, selectedTab, route, transaction}) { onEntryTransitionEnd={() => optionsSelectorRef.current && optionsSelectorRef.current.focus()} testID={MoneyRequestParticipantsPage.displayName} > - {({safeAreaPaddingBottomStyle, didScreenTransitionEnd}) => ( + {({safeAreaPaddingBottomStyle}) => ( )} From c69ecfd1f3a1296c2a9be1a575abb5c5c39a829c Mon Sep 17 00:00:00 2001 From: brunovjk Date: Thu, 14 Dec 2023 15:38:24 -0300 Subject: [PATCH 054/391] Revert "Add 'didScreenTransitionEnd' prop to MoneyRequestParticipantsSelector" This reverts commit 9eafd1fce280df3a53617063348a525f3f2ca6b5. --- .../MoneyRequestParticipantsSelector.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js index 8c0fc71b0112..d8d644479270 100755 --- a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js +++ b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js @@ -66,9 +66,6 @@ const propTypes = { /** Whether we are searching for reports in the server */ isSearchingForReports: PropTypes.bool, - /** Whether the screen transition has ended */ - didScreenTransitionEnd: PropTypes.bool, - ...withLocalizePropTypes, }; @@ -81,7 +78,6 @@ const defaultProps = { betas: [], isDistanceRequest: false, isSearchingForReports: false, - didScreenTransitionEnd: false, }; function MoneyRequestParticipantsSelector({ @@ -98,7 +94,6 @@ function MoneyRequestParticipantsSelector({ iouType, isDistanceRequest, isSearchingForReports, - didScreenTransitionEnd, }) { const styles = useThemeStyles(); const [searchTerm, setSearchTerm] = useState(''); From dc1f87fdc77b65716dc625b65c61be8fccb718e5 Mon Sep 17 00:00:00 2001 From: brunovjk Date: Thu, 14 Dec 2023 16:01:52 -0300 Subject: [PATCH 055/391] Integrate 'didScreenTransitionEnd' prop in IOURequestStepParticipants --- .../step/IOURequestStepParticipants.js | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/pages/iou/request/step/IOURequestStepParticipants.js b/src/pages/iou/request/step/IOURequestStepParticipants.js index 85d67ea34bae..fe5633c2f05c 100644 --- a/src/pages/iou/request/step/IOURequestStepParticipants.js +++ b/src/pages/iou/request/step/IOURequestStepParticipants.js @@ -87,14 +87,17 @@ function IOURequestStepParticipants({ onEntryTransitionEnd={() => optionsSelectorRef.current && optionsSelectorRef.current.focus()} includeSafeAreaPaddingBottom > - (optionsSelectorRef.current = el)} - participants={participants} - onParticipantsAdded={addParticipant} - onFinish={goToNextStep} - iouType={iouType} - iouRequestType={iouRequestType} - /> + {({didScreenTransitionEnd}) => ( + (optionsSelectorRef.current = el)} + participants={participants} + onParticipantsAdded={addParticipant} + onFinish={goToNextStep} + iouType={iouType} + iouRequestType={iouRequestType} + didScreenTransitionEnd={didScreenTransitionEnd} + /> + )} ); } From 9cd0ab537b3c3f36438baf221352b2aabc5e7881 Mon Sep 17 00:00:00 2001 From: brunovjk Date: Thu, 14 Dec 2023 16:03:38 -0300 Subject: [PATCH 056/391] Add 'didScreenTransitionEnd' prop to MoneyRequestParticipantsSelector --- .../MoneyTemporaryForRefactorRequestParticipantsSelector.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js index 8d7d5cfceb77..6194398e2bb9 100644 --- a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js +++ b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js @@ -63,6 +63,9 @@ const propTypes = { /** Whether we are searching for reports in the server */ isSearchingForReports: PropTypes.bool, + /** Whether the screen transition has ended */ + didScreenTransitionEnd: PropTypes.bool, + ...withLocalizePropTypes, }; @@ -74,6 +77,7 @@ const defaultProps = { reports: {}, betas: [], isSearchingForReports: false, + didScreenTransitionEnd: false, }; function MoneyTemporaryForRefactorRequestParticipantsSelector({ @@ -89,6 +93,7 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({ iouType, iouRequestType, isSearchingForReports, + didScreenTransitionEnd, }) { const styles = useThemeStyles(); const [searchTerm, setSearchTerm] = useState(''); From a6a36f82518fdf220d41b754ee5723db64a61277 Mon Sep 17 00:00:00 2001 From: brunovjk Date: Thu, 14 Dec 2023 16:05:15 -0300 Subject: [PATCH 057/391] Conditionally update 'chatOptions' on screen transition end --- ...yForRefactorRequestParticipantsSelector.js | 68 ++++++++++--------- 1 file changed, 35 insertions(+), 33 deletions(-) diff --git a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js index 6194398e2bb9..48fac9c1cef5 100644 --- a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js +++ b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js @@ -228,38 +228,40 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({ const isOptionsDataReady = ReportUtils.isReportDataReady() && OptionsListUtils.isPersonalDetailsReady(personalDetails); useEffect(() => { - const chatOptions = OptionsListUtils.getFilteredOptions( - reports, - personalDetails, - betas, - searchTerm, - participants, - CONST.EXPENSIFY_EMAILS, - - // If we are using this component in the "Request money" flow then we pass the includeOwnedWorkspaceChats argument so that the current user - // sees the option to request money from their admin on their own Workspace Chat. - iouType === CONST.IOU.TYPE.REQUEST, - - // We don't want to include any P2P options like personal details or reports that are not workspace chats for certain features. - iouRequestType !== CONST.IOU.REQUEST_TYPE.DISTANCE, - false, - {}, - [], - false, - {}, - [], - - // We don't want the user to be able to invite individuals when they are in the "Distance request" flow for now. - // This functionality is being built here: https://github.com/Expensify/App/issues/23291 - iouRequestType !== CONST.IOU.REQUEST_TYPE.DISTANCE, - true, - ); - setNewChatOptions({ - recentReports: chatOptions.recentReports, - personalDetails: chatOptions.personalDetails, - userToInvite: chatOptions.userToInvite, - }); - }, [betas, reports, participants, personalDetails, translate, searchTerm, setNewChatOptions, iouType, iouRequestType]); + if (didScreenTransitionEnd) { + const chatOptions = OptionsListUtils.getFilteredOptions( + reports, + personalDetails, + betas, + searchTerm, + participants, + CONST.EXPENSIFY_EMAILS, + + // If we are using this component in the "Request money" flow then we pass the includeOwnedWorkspaceChats argument so that the current user + // sees the option to request money from their admin on their own Workspace Chat. + iouType === CONST.IOU.TYPE.REQUEST, + + // We don't want to include any P2P options like personal details or reports that are not workspace chats for certain features. + iouRequestType !== CONST.IOU.REQUEST_TYPE.DISTANCE, + false, + {}, + [], + false, + {}, + [], + + // We don't want the user to be able to invite individuals when they are in the "Distance request" flow for now. + // This functionality is being built here: https://github.com/Expensify/App/issues/23291 + iouRequestType !== CONST.IOU.REQUEST_TYPE.DISTANCE, + true, + ); + setNewChatOptions({ + recentReports: chatOptions.recentReports, + personalDetails: chatOptions.personalDetails, + userToInvite: chatOptions.userToInvite, + }); + } + }, [betas, reports, participants, personalDetails, translate, searchTerm, setNewChatOptions, iouType, iouRequestType, didScreenTransitionEnd]); // When search term updates we will fetch any reports const setSearchTermAndSearchInServer = useCallback((text = '') => { @@ -324,7 +326,7 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({ textInputLabel={translate('optionsSelector.nameEmailOrPhoneNumber')} textInputAlert={isOffline ? `${translate('common.youAppearToBeOffline')} ${translate('search.resultsAreLimited')}` : ''} safeAreaPaddingBottomStyle={safeAreaPaddingBottomStyle} - shouldShowOptions={isOptionsDataReady} + shouldShowOptions={didScreenTransitionEnd && isOptionsDataReady} shouldShowReferralCTA referralContentType={iouType === 'send' ? CONST.REFERRAL_PROGRAM.CONTENT_TYPES.SEND_MONEY : CONST.REFERRAL_PROGRAM.CONTENT_TYPES.MONEY_REQUEST} shouldPreventDefaultFocusOnSelectRow={!Browser.isMobile()} From cb616829c5d7ecc94b915e0adc46f19cdfb673e5 Mon Sep 17 00:00:00 2001 From: brunovjk Date: Thu, 14 Dec 2023 23:57:01 -0300 Subject: [PATCH 058/391] Use early return --- ...yForRefactorRequestParticipantsSelector.js | 65 ++++++++++--------- 1 file changed, 33 insertions(+), 32 deletions(-) diff --git a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js index 48fac9c1cef5..6341326b6eec 100644 --- a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js +++ b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js @@ -228,39 +228,40 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({ const isOptionsDataReady = ReportUtils.isReportDataReady() && OptionsListUtils.isPersonalDetailsReady(personalDetails); useEffect(() => { - if (didScreenTransitionEnd) { - const chatOptions = OptionsListUtils.getFilteredOptions( - reports, - personalDetails, - betas, - searchTerm, - participants, - CONST.EXPENSIFY_EMAILS, - - // If we are using this component in the "Request money" flow then we pass the includeOwnedWorkspaceChats argument so that the current user - // sees the option to request money from their admin on their own Workspace Chat. - iouType === CONST.IOU.TYPE.REQUEST, - - // We don't want to include any P2P options like personal details or reports that are not workspace chats for certain features. - iouRequestType !== CONST.IOU.REQUEST_TYPE.DISTANCE, - false, - {}, - [], - false, - {}, - [], - - // We don't want the user to be able to invite individuals when they are in the "Distance request" flow for now. - // This functionality is being built here: https://github.com/Expensify/App/issues/23291 - iouRequestType !== CONST.IOU.REQUEST_TYPE.DISTANCE, - true, - ); - setNewChatOptions({ - recentReports: chatOptions.recentReports, - personalDetails: chatOptions.personalDetails, - userToInvite: chatOptions.userToInvite, - }); + if (!didScreenTransitionEnd) { + return; } + const chatOptions = OptionsListUtils.getFilteredOptions( + reports, + personalDetails, + betas, + searchTerm, + participants, + CONST.EXPENSIFY_EMAILS, + + // If we are using this component in the "Request money" flow then we pass the includeOwnedWorkspaceChats argument so that the current user + // sees the option to request money from their admin on their own Workspace Chat. + iouType === CONST.IOU.TYPE.REQUEST, + + // We don't want to include any P2P options like personal details or reports that are not workspace chats for certain features. + iouRequestType !== CONST.IOU.REQUEST_TYPE.DISTANCE, + false, + {}, + [], + false, + {}, + [], + + // We don't want the user to be able to invite individuals when they are in the "Distance request" flow for now. + // This functionality is being built here: https://github.com/Expensify/App/issues/23291 + iouRequestType !== CONST.IOU.REQUEST_TYPE.DISTANCE, + true, + ); + setNewChatOptions({ + recentReports: chatOptions.recentReports, + personalDetails: chatOptions.personalDetails, + userToInvite: chatOptions.userToInvite, + }); }, [betas, reports, participants, personalDetails, translate, searchTerm, setNewChatOptions, iouType, iouRequestType, didScreenTransitionEnd]); // When search term updates we will fetch any reports From f16c0d5c89bbecfca46fd3f569d93b7fd8476ea8 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Fri, 15 Dec 2023 10:59:55 +0100 Subject: [PATCH 059/391] fix: adress comments --- src/libs/OptionsListUtils.ts | 40 ++++++++++++++++++------------------ 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index e4a48749c93c..211141ff937b 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -2,17 +2,17 @@ import {parsePhoneNumber} from 'awesome-phonenumber'; import Str from 'expensify-common/lib/str'; import lodashOrderBy from 'lodash/orderBy'; -import lodashSet from 'lodash/set'; import Onyx, {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import CONST from '@src/CONST'; import {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; -import {Beta, Login, PersonalDetails, Policy, PolicyCategory, Report, ReportAction, ReportActions, Transaction} from '@src/types/onyx'; +import {Beta, Login, PersonalDetails, Policy, PolicyCategories, Report, ReportAction, ReportActions, Transaction} from '@src/types/onyx'; import {Participant} from '@src/types/onyx/IOU'; import * as OnyxCommon from '@src/types/onyx/OnyxCommon'; import DeepValueOf from '@src/types/utils/DeepValueOf'; import {EmptyObject, isEmptyObject, isNotEmptyObject} from '@src/types/utils/EmptyObject'; import get from '@src/utils/get'; +import set from '@src/utils/set'; import sortBy from '@src/utils/sortBy'; import times from '@src/utils/times'; import * as CollectionUtils from './CollectionUtils'; @@ -88,7 +88,7 @@ type GetOptionsConfig = { excludeUnknownUsers?: boolean; includeP2P?: boolean; includeCategories?: boolean; - categories?: Record; + categories?: PolicyCategories; recentlyUsedCategories?: string[]; includeTags?: boolean; tags?: Record; @@ -124,6 +124,8 @@ type GetOptions = { tagOptions: CategorySection[]; }; +type PreviewConfig = {showChatPreviewLine?: boolean; forcePolicyNamePreview?: boolean}; + /** * 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 @@ -142,7 +144,7 @@ Onyx.connect({ let loginList: OnyxEntry; Onyx.connect({ key: ONYXKEYS.LOGIN_LIST, - callback: (value) => (loginList = Object.keys(value ?? {}).length === 0 ? {} : value), + callback: (value) => (loginList = isEmptyObject(value) ? {} : value), }); let allPersonalDetails: OnyxEntry; @@ -313,7 +315,7 @@ function getParticipantsOption(participant: ReportUtils.OptionData, personalDeta alternateText: LocalePhoneNumber.formatPhoneNumber(login) || displayName, icons: [ { - source: UserUtils.getAvatar(detail.avatar ?? '', detail.accountID ?? 0), + source: UserUtils.getAvatar(detail.avatar ?? '', detail.accountID ?? -1), name: login, type: CONST.ICON_TYPE_AVATAR, id: detail.accountID, @@ -460,10 +462,7 @@ function getAllReportErrors(report: OnyxEntry, reportActions: OnyxEntry< reportActionErrors, }; // Combine all error messages keyed by microtime into one object - const allReportErrors = Object.values(errorSources)?.reduce( - (prevReportErrors, errors) => (Object.keys(errors ?? {}).length > 0 ? prevReportErrors : Object.assign(prevReportErrors, errors)), - {}, - ); + const allReportErrors = Object.values(errorSources)?.reduce((prevReportErrors, errors) => (isNotEmptyObject(errors) ? prevReportErrors : Object.assign(prevReportErrors, errors)), {}); return allReportErrors; } @@ -521,7 +520,7 @@ function createOption( personalDetails: OnyxEntry, report: OnyxEntry, reportActions: ReportActions, - {showChatPreviewLine = false, forcePolicyNamePreview = false}: {showChatPreviewLine?: boolean; forcePolicyNamePreview?: boolean}, + {showChatPreviewLine = false, forcePolicyNamePreview = false}: PreviewConfig, ): ReportUtils.OptionData { const result: ReportUtils.OptionData = { text: undefined, @@ -634,6 +633,7 @@ function createOption( } result.text = reportName; + // Disabling this line for safeness as nullish coalescing works only if the value is undefined or null // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing 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); @@ -708,14 +708,14 @@ function isCurrentUser(userDetails: PersonalDetails): boolean { /** * Calculates count of all enabled options */ -function getEnabledCategoriesCount(options: Record): number { +function getEnabledCategoriesCount(options: PolicyCategories): number { return Object.values(options).filter((option) => option.enabled).length; } /** * Verifies that there is at least one enabled option */ -function hasEnabledOptions(options: Record): boolean { +function hasEnabledOptions(options: PolicyCategories): boolean { return Object.values(options).some((option) => option.enabled); } @@ -749,7 +749,7 @@ function sortCategories(categories: Record): Category[] { sortedCategories.forEach((category) => { const path = category.name.split(CONST.PARENT_CHILD_SEPARATOR); const existedValue = get(hierarchy, path, {}); - lodashSet(hierarchy, path, { + set(hierarchy, path, { ...existedValue, name: category.name, }); @@ -764,7 +764,7 @@ function sortCategories(categories: Record): Category[] { Object.values(initialHierarchy).reduce((acc: Category[], category) => { const {name, ...subcategories} = category; if (name) { - const categoryObject = { + const categoryObject: Category = { name, enabled: categories[name].enabled ?? false, }; @@ -854,7 +854,7 @@ function getIndentedOptionTree(options: Category[] | Record, i * Builds the section list for categories */ function getCategoryListSections( - categories: Record, + categories: PolicyCategories, recentlyUsedCategories: string[], selectedOptions: Category[], searchInputValue: string, @@ -1071,7 +1071,7 @@ function getTagListSections(rawTags: Tag[], recentlyUsedTags: string[], selected * Build the options */ function getOptions( - reports: Record, + reports: OnyxCollection, personalDetails: OnyxEntry, { reportActions = {}, @@ -1225,11 +1225,11 @@ function getOptions( // 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 filteredDetails: OnyxEntry = Object.keys(personalDetails ?? {}) - .filter((key) => 'login' in (personalDetails?.[+key] ?? {})) + .filter((key) => 'login' in (personalDetails?.[Number(key)] ?? {})) .reduce((obj: OnyxEntry, key) => { - if (obj && personalDetails?.[+key]) { + if (obj && personalDetails?.[Number(key)]) { // eslint-disable-next-line no-param-reassign - obj[+key] = personalDetails?.[+key]; + obj[Number(key)] = personalDetails?.[Number(key)]; } return obj; @@ -1461,7 +1461,7 @@ function getIOUConfirmationOptionsFromPayeePersonalDetail(personalDetail: Person /** * Build the IOUConfirmationOptions for showing participants */ -function getIOUConfirmationOptionsFromParticipants(participants: Participant[], amountText: string) { +function getIOUConfirmationOptionsFromParticipants(participants: Participant[], amountText: string): Participant[] { return participants.map((participant) => ({ ...participant, descriptiveText: amountText, From 27fcd3723e817faf74747bdd50fa2d40e3d02ec8 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Fri, 15 Dec 2023 11:04:44 +0100 Subject: [PATCH 060/391] fix: typecheck --- src/libs/OptionsListUtils.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 211141ff937b..3b10dc5e1742 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -1148,7 +1148,7 @@ function getOptions( const searchValue = parsedPhoneNumber.possible ? parsedPhoneNumber.number?.e164 : searchInputValue.toLowerCase(); // Filter out all the reports that shouldn't be displayed - const filteredReports = Object.values(reports).filter((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) @@ -1158,7 +1158,7 @@ function getOptions( return CONST.DATE.UNIX_EPOCH; } - return report.lastVisibleActionCreated; + return report?.lastVisibleActionCreated; }); orderedReports.reverse(); From 414a3a45da90a327e4ad45ac247ae5fd12e6c962 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Fri, 15 Dec 2023 11:11:34 +0100 Subject: [PATCH 061/391] fix: removed unnecessary null --- src/libs/OptionsListUtils.ts | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 3b10dc5e1742..06bd7b1eeca9 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -475,11 +475,11 @@ function getLastMessageTextForReport(report: OnyxEntry): string { let lastMessageTextFromReport = ''; const lastActionName = lastReportAction?.actionName ?? ''; - if (ReportActionUtils.isMoneyRequestAction(lastReportAction ?? null)) { + if (ReportActionUtils.isMoneyRequestAction(lastReportAction)) { const properSchemaForMoneyRequestMessage = ReportUtils.getReportPreviewMessage(report, lastReportAction, true); lastMessageTextFromReport = ReportUtils.formatReportLastMessageText(properSchemaForMoneyRequestMessage); - } else if (ReportActionUtils.isReportPreviewAction(lastReportAction ?? null)) { - const iouReport = ReportUtils.getReport(ReportActionUtils.getIOUReportIDFromReportActionPreview(lastReportAction ?? null)); + } else if (ReportActionUtils.isReportPreviewAction(lastReportAction)) { + const iouReport = ReportUtils.getReport(ReportActionUtils.getIOUReportIDFromReportActionPreview(lastReportAction)); const lastIOUMoneyReport = allSortedReportActions[iouReport?.reportID ?? '']?.find( (reportAction, key) => ReportActionUtils.shouldReportActionBeVisible(reportAction, key) && @@ -487,16 +487,16 @@ function getLastMessageTextForReport(report: OnyxEntry): string { ReportActionUtils.isMoneyRequestAction(reportAction), ); lastMessageTextFromReport = ReportUtils.getReportPreviewMessage(isNotEmptyObject(iouReport) ? iouReport : null, lastIOUMoneyReport, true, ReportUtils.isChatReport(report)); - } else if (ReportActionUtils.isReimbursementQueuedAction(lastReportAction ?? null)) { - lastMessageTextFromReport = ReportUtils.getReimbursementQueuedActionMessage(lastReportAction ?? null, report); - } else if (ReportActionUtils.isReimbursementDeQueuedAction(lastReportAction ?? null)) { + } else if (ReportActionUtils.isReimbursementQueuedAction(lastReportAction)) { + lastMessageTextFromReport = ReportUtils.getReimbursementQueuedActionMessage(lastReportAction, report); + } else if (ReportActionUtils.isReimbursementDeQueuedAction(lastReportAction)) { lastMessageTextFromReport = ReportUtils.getReimbursementDeQueuedActionMessage(report); - } else if (ReportActionUtils.isDeletedParentAction(lastReportAction ?? null) && ReportUtils.isChatReport(report)) { - lastMessageTextFromReport = ReportUtils.getDeletedParentActionMessageForChatReport(lastReportAction ?? null); + } else if (ReportActionUtils.isDeletedParentAction(lastReportAction) && ReportUtils.isChatReport(report)) { + lastMessageTextFromReport = ReportUtils.getDeletedParentActionMessageForChatReport(lastReportAction); } else if (ReportUtils.isReportMessageAttachment({text: report?.lastMessageText ?? '', html: report?.lastMessageHtml, translationKey: report?.lastMessageTranslationKey, type: ''})) { lastMessageTextFromReport = `[${Localize.translateLocal((report?.lastMessageTranslationKey ?? 'common.attachment') as TranslationPaths)}]`; - } else if (ReportActionUtils.isModifiedExpenseAction(lastReportAction ?? null)) { - const properSchemaForModifiedExpenseMessage = ReportUtils.getModifiedExpenseMessage(lastReportAction ?? null); + } else if (ReportActionUtils.isModifiedExpenseAction(lastReportAction)) { + const properSchemaForModifiedExpenseMessage = ReportUtils.getModifiedExpenseMessage(lastReportAction); lastMessageTextFromReport = ReportUtils.formatReportLastMessageText(properSchemaForModifiedExpenseMessage ?? '', true); } else if ( lastActionName === CONST.REPORT.ACTIONS.TYPE.TASKCOMPLETED || From e5de1c795d5870562b17d3adc5f1ddbd8d738907 Mon Sep 17 00:00:00 2001 From: Tsaqif Date: Sat, 16 Dec 2023 14:07:33 +0700 Subject: [PATCH 062/391] Fix cannot open search page by shortcut if delete receipt confirm modal visible Signed-off-by: Tsaqif --- src/components/Modal/BaseModal.tsx | 2 +- src/components/ThreeDotsMenu/index.js | 1 + src/libs/actions/Modal.ts | 20 ++++++++++++++++---- 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/src/components/Modal/BaseModal.tsx b/src/components/Modal/BaseModal.tsx index 54a178db1cdd..653db38dca66 100644 --- a/src/components/Modal/BaseModal.tsx +++ b/src/components/Modal/BaseModal.tsx @@ -176,7 +176,7 @@ function BaseModal( onBackdropPress={handleBackdropPress} // Note: Escape key on web/desktop will trigger onBackButtonPress callback // eslint-disable-next-line react/jsx-props-no-multi-spaces - onBackButtonPress={onClose} + onBackButtonPress={Modal.closeTop} onModalShow={handleShowModal} propagateSwipe={propagateSwipe} onModalHide={hideModal} diff --git a/src/components/ThreeDotsMenu/index.js b/src/components/ThreeDotsMenu/index.js index dbaf8ab23360..eca85d5e6baf 100644 --- a/src/components/ThreeDotsMenu/index.js +++ b/src/components/ThreeDotsMenu/index.js @@ -96,6 +96,7 @@ function ThreeDotsMenu({iconTooltip, icon, iconFill, iconStyles, onIconPress, me hidePopoverMenu(); return; } + buttonRef.current.blur(); showPopoverMenu(); if (onIconPress) { onIconPress(); diff --git a/src/libs/actions/Modal.ts b/src/libs/actions/Modal.ts index e1e73d425281..791c921974ae 100644 --- a/src/libs/actions/Modal.ts +++ b/src/libs/actions/Modal.ts @@ -30,15 +30,17 @@ function close(onModalCloseCallback: () => void, isNavigating = true) { return; } onModalClose = onModalCloseCallback; - [...closeModals].reverse().forEach((onClose) => { - onClose(isNavigating); - }); + closeTop(); } function onModalDidClose() { if (!onModalClose) { return; } + if (closeModals.length) { + closeTop(); + return; + } onModalClose(); onModalClose = null; } @@ -50,6 +52,16 @@ function setModalVisibility(isVisible: boolean) { Onyx.merge(ONYXKEYS.MODAL, {isVisible}); } +/** + * Close topmost modal + */ +function closeTop() { + if (closeModals.length === 0) { + return; + } + closeModals[closeModals.length - 1](); +} + /** * Allows other parts of app to know that an alert modal is about to open. * This will trigger as soon as a modal is opened but not yet visible while animation is running. @@ -58,4 +70,4 @@ function willAlertModalBecomeVisible(isVisible: boolean) { Onyx.merge(ONYXKEYS.MODAL, {willAlertModalBecomeVisible: isVisible}); } -export {setCloseModal, close, onModalDidClose, setModalVisibility, willAlertModalBecomeVisible}; +export {setCloseModal, close, onModalDidClose, setModalVisibility, willAlertModalBecomeVisible, closeTop}; From d8d31f9294a850b5ce75369409e57179ec1d073c Mon Sep 17 00:00:00 2001 From: Tsaqif Date: Sun, 17 Dec 2023 15:02:23 +0700 Subject: [PATCH 063/391] Remove isNavigating parameter Signed-off-by: Tsaqif --- src/libs/actions/Modal.ts | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/libs/actions/Modal.ts b/src/libs/actions/Modal.ts index 791c921974ae..1c6fbe74ef01 100644 --- a/src/libs/actions/Modal.ts +++ b/src/libs/actions/Modal.ts @@ -1,7 +1,7 @@ import Onyx from 'react-native-onyx'; import ONYXKEYS from '@src/ONYXKEYS'; -const closeModals: Array<(isNavigating: boolean) => void> = []; +const closeModals: Array<() => void> = []; let onModalClose: null | (() => void); @@ -21,10 +21,20 @@ function setCloseModal(onClose: () => void) { }; } +/** + * Close topmost modal + */ +function closeTop() { + if (closeModals.length === 0) { + return; + } + closeModals[closeModals.length - 1](); +} + /** * Close modal in other parts of the app */ -function close(onModalCloseCallback: () => void, isNavigating = true) { +function close(onModalCloseCallback: () => void) { if (closeModals.length === 0) { onModalCloseCallback(); return; @@ -52,16 +62,6 @@ function setModalVisibility(isVisible: boolean) { Onyx.merge(ONYXKEYS.MODAL, {isVisible}); } -/** - * Close topmost modal - */ -function closeTop() { - if (closeModals.length === 0) { - return; - } - closeModals[closeModals.length - 1](); -} - /** * Allows other parts of app to know that an alert modal is about to open. * This will trigger as soon as a modal is opened but not yet visible while animation is running. From 22b5aaa0639f4bd8d2c0caa8a51311b500703403 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Sun, 17 Dec 2023 22:20:33 +0100 Subject: [PATCH 064/391] Type FormProvider --- src/components/FloatingActionButton/index.js | 53 +-- src/components/Form/FormContext.tsx | 9 +- src/components/Form/FormProvider.js | 428 ------------------ src/components/Form/FormProvider.tsx | 364 +++++++++++++++ src/components/Form/FormWrapper.tsx | 40 +- src/components/Form/InputWrapper.tsx | 8 +- src/components/Form/types.ts | 95 ++-- src/libs/actions/FormActions.ts | 4 +- .../FloatingActionButtonAndPopover.js | 13 - 9 files changed, 459 insertions(+), 555 deletions(-) delete mode 100644 src/components/Form/FormProvider.js create mode 100644 src/components/Form/FormProvider.tsx diff --git a/src/components/FloatingActionButton/index.js b/src/components/FloatingActionButton/index.js index d341396c44b7..8e963d49b10c 100644 --- a/src/components/FloatingActionButton/index.js +++ b/src/components/FloatingActionButton/index.js @@ -26,58 +26,7 @@ const propTypes = { role: PropTypes.string.isRequired, }; -const FloatingActionButton = React.forwardRef(({onPress, isActive, accessibilityLabel, role}, ref) => { - const theme = useTheme(); - const styles = useThemeStyles(); - const {translate} = useLocalize(); - const fabPressable = useRef(null); - const animatedValue = useSharedValue(isActive ? 1 : 0); - const buttonRef = ref; - - useEffect(() => { - animatedValue.value = withTiming(isActive ? 1 : 0, { - duration: 340, - easing: Easing.inOut(Easing.ease), - }); - }, [isActive, animatedValue]); - - const animatedStyle = useAnimatedStyle(() => { - const backgroundColor = interpolateColor(animatedValue.value, [0, 1], [theme.success, theme.buttonDefaultBG]); - - return { - transform: [{rotate: `${animatedValue.value * 135}deg`}], - backgroundColor, - borderRadius: styles.floatingActionButton.borderRadius, - }; - }); - - return ( - - - { - fabPressable.current = el; - if (buttonRef) { - buttonRef.current = el; - } - }} - accessibilityLabel={accessibilityLabel} - role={role} - pressDimmingValue={1} - onPress={(e) => { - // Drop focus to avoid blue focus ring. - fabPressable.current.blur(); - onPress(e); - }} - onLongPress={() => {}} - style={[styles.floatingActionButton, animatedStyle]} - > - - - - - ); -}); +const FloatingActionButton = React.forwardRef(({onPress, isActive, accessibilityLabel, role}, ref) => null); FloatingActionButton.propTypes = propTypes; FloatingActionButton.displayName = 'FloatingActionButton'; diff --git a/src/components/Form/FormContext.tsx b/src/components/Form/FormContext.tsx index 23a2ea615eda..dcc8f3b516de 100644 --- a/src/components/Form/FormContext.tsx +++ b/src/components/Form/FormContext.tsx @@ -1,13 +1,12 @@ import {createContext} from 'react'; +import {RegisterInput} from './types'; -type FormContextType = { - registerInput: (key: string, ref: any) => object; +type FormContext = { + registerInput: RegisterInput; }; -const FormContext = createContext({ +export default createContext({ registerInput: () => { throw new Error('Registered input should be wrapped with FormWrapper'); }, }); - -export default FormContext; diff --git a/src/components/Form/FormProvider.js b/src/components/Form/FormProvider.js deleted file mode 100644 index c0537c01be7c..000000000000 --- a/src/components/Form/FormProvider.js +++ /dev/null @@ -1,428 +0,0 @@ -import lodashGet from 'lodash/get'; -import PropTypes from 'prop-types'; -import React, {createRef, forwardRef, useCallback, useImperativeHandle, useMemo, useRef, useState} from 'react'; -import {withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; -import networkPropTypes from '@components/networkPropTypes'; -import {withNetwork} from '@components/OnyxProvider'; -import compose from '@libs/compose'; -import * as ValidationUtils from '@libs/ValidationUtils'; -import Visibility from '@libs/Visibility'; -import stylePropTypes from '@styles/stylePropTypes'; -import * as FormActions from '@userActions/FormActions'; -import CONST from '@src/CONST'; -import FormContext from './FormContext'; -import FormWrapper from './FormWrapper'; - -// type ErrorsType = string | Record>; -// const errorsPropType = PropTypes.oneOfType([ -// PropTypes.string, -// PropTypes.objectOf( -// PropTypes.oneOfType([ -// PropTypes.string, -// PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.objectOf(PropTypes.oneOfType([PropTypes.string, PropTypes.bool, PropTypes.number]))])), -// ]), -// ), -// ]); - -// const defaultProps = { -// isSubmitButtonVisible: true, -// formState: { -// isLoading: false, -// }, -// enabledWhenOffline: false, -// isSubmitActionDangerous: false, -// scrollContextEnabled: false, -// footerContent: null, -// style: [], -// submitButtonStyles: [], -// }; - -const propTypes = { - /** A unique Onyx key identifying the form */ - formID: PropTypes.string.isRequired, - - /** Text to be displayed in the submit button */ - submitButtonText: PropTypes.string.isRequired, - - /** Controls the submit button's visibility */ - isSubmitButtonVisible: PropTypes.bool, - - /** Callback to validate the form */ - validate: PropTypes.func, - - /** Callback to submit the form */ - onSubmit: PropTypes.func.isRequired, - - /** Children to render. */ - children: PropTypes.oneOfType([PropTypes.func, PropTypes.node]).isRequired, - - /* Onyx Props */ - - /** Contains the form state that must be accessed outside of the component */ - formState: PropTypes.shape({ - /** Controls the loading state of the form */ - isLoading: PropTypes.bool, - - /** Server side errors keyed by microtime */ - errors: PropTypes.objectOf(PropTypes.string), - - /** Field-specific server side errors keyed by microtime */ - errorFields: PropTypes.objectOf(PropTypes.objectOf(PropTypes.string)), - }), - - /** Contains draft values for each input in the form */ - draftValues: PropTypes.objectOf(PropTypes.oneOfType([PropTypes.string, PropTypes.bool, PropTypes.number, PropTypes.objectOf(Date)])), - - /** Should the button be enabled when offline */ - enabledWhenOffline: PropTypes.bool, - - /** Whether the form submit action is dangerous */ - isSubmitActionDangerous: PropTypes.bool, - - /** Whether ScrollWithContext should be used instead of regular ScrollView. Set to true when there's a nested Picker component in Form. */ - scrollContextEnabled: PropTypes.bool, - - /** Container styles */ - style: stylePropTypes, - - /** Custom content to display in the footer after submit button */ - footerContent: PropTypes.oneOfType([PropTypes.func, PropTypes.node]), - - /** Information about the network */ - network: networkPropTypes.isRequired, - - /** Should validate function be called when input loose focus */ - shouldValidateOnBlur: PropTypes.bool, - - /** Should validate function be called when the value of the input is changed */ - shouldValidateOnChange: PropTypes.bool, -}; - -// In order to prevent Checkbox focus loss when the user are focusing a TextInput and proceeds to toggle a CheckBox in web and mobile web. -// 200ms delay was chosen as a result of empirical testing. -// More details: https://github.com/Expensify/App/pull/16444#issuecomment-1482983426 -const VALIDATE_DELAY = 200; - -const defaultProps = { - isSubmitButtonVisible: true, - formState: { - isLoading: false, - }, - draftValues: {}, - enabledWhenOffline: false, - isSubmitActionDangerous: false, - scrollContextEnabled: false, - footerContent: null, - style: [], - validate: () => {}, - shouldValidateOnBlur: true, - shouldValidateOnChange: true, -}; - -function getInitialValueByType(valueType) { - switch (valueType) { - case 'string': - return ''; - case 'boolean': - return false; - case 'date': - return new Date(); - default: - return ''; - } -} - -const FormProvider = forwardRef( - ({validate, formID, shouldValidateOnBlur, shouldValidateOnChange, children, formState, network, enabledWhenOffline, draftValues, onSubmit, ...rest}, forwardedRef) => { - const inputRefs = useRef({}); - const touchedInputs = useRef({}); - const [inputValues, setInputValues] = useState(() => ({...draftValues})); - const [errors, setErrors] = useState({}); - const hasServerError = useMemo(() => Boolean(formState) && !_.isEmpty(formState.errors), [formState]); - - const onValidate = useCallback( - (values, shouldClearServerError = true) => { - const trimmedStringValues = ValidationUtils.prepareValues(values); - - if (shouldClearServerError) { - FormActions.setErrors(formID, null); - } - FormActions.setErrorFields(formID, null); - - const validateErrors = validate(trimmedStringValues) || {}; - - // Validate the input for html tags. It should supercede any other error - _.each(trimmedStringValues, (inputValue, inputID) => { - // If the input value is empty OR is non-string, we don't need to validate it for HTML tags - if (!inputValue || !_.isString(inputValue)) { - return; - } - const foundHtmlTagIndex = inputValue.search(CONST.VALIDATE_FOR_HTML_TAG_REGEX); - const leadingSpaceIndex = inputValue.search(CONST.VALIDATE_FOR_LEADINGSPACES_HTML_TAG_REGEX); - - // Return early if there are no HTML characters - if (leadingSpaceIndex === -1 && foundHtmlTagIndex === -1) { - return; - } - - const matchedHtmlTags = inputValue.match(CONST.VALIDATE_FOR_HTML_TAG_REGEX); - let isMatch = _.some(CONST.WHITELISTED_TAGS, (r) => r.test(inputValue)); - // Check for any matches that the original regex (foundHtmlTagIndex) matched - if (matchedHtmlTags) { - // Check if any matched inputs does not match in WHITELISTED_TAGS list and return early if needed. - for (let i = 0; i < matchedHtmlTags.length; i++) { - const htmlTag = matchedHtmlTags[i]; - isMatch = _.some(CONST.WHITELISTED_TAGS, (r) => r.test(htmlTag)); - if (!isMatch) { - break; - } - } - } - - if (isMatch && leadingSpaceIndex === -1) { - return; - } - - // Add a validation error here because it is a string value that contains HTML characters - validateErrors[inputID] = 'common.error.invalidCharacter'; - }); - - if (!_.isObject(validateErrors)) { - throw new Error('Validate callback must return an empty object or an object with shape {inputID: error}'); - } - - const touchedInputErrors = _.pick(validateErrors, (inputValue, inputID) => Boolean(touchedInputs.current[inputID])); - - if (!_.isEqual(errors, touchedInputErrors)) { - setErrors(touchedInputErrors); - } - - return touchedInputErrors; - }, - [errors, formID, validate], - ); - - /** - * @param {String} inputID - The inputID of the input being touched - */ - const setTouchedInput = useCallback( - (inputID) => { - touchedInputs.current[inputID] = true; - }, - [touchedInputs], - ); - - const submit = useCallback(() => { - // Return early if the form is already submitting to avoid duplicate submission - if (formState.isLoading) { - return; - } - - // Prepare values before submitting - const trimmedStringValues = ValidationUtils.prepareValues(inputValues); - - // Touches all form inputs so we can validate the entire form - _.each(inputRefs.current, (inputRef, inputID) => (touchedInputs.current[inputID] = true)); - - // Validate form and return early if any errors are found - if (!_.isEmpty(onValidate(trimmedStringValues))) { - return; - } - - // Do not submit form if network is offline and the form is not enabled when offline - if (network.isOffline && !enabledWhenOffline) { - return; - } - - onSubmit(trimmedStringValues); - }, [enabledWhenOffline, formState.isLoading, inputValues, network.isOffline, onSubmit, onValidate]); - - /** - * Resets the form - */ - const resetForm = useCallback( - (optionalValue) => { - _.each(inputValues, (inputRef, inputID) => { - setInputValues((prevState) => { - const copyPrevState = _.clone(prevState); - - touchedInputs.current[inputID] = false; - copyPrevState[inputID] = optionalValue[inputID] || ''; - - return copyPrevState; - }); - }); - setErrors({}); - }, - [inputValues], - ); - useImperativeHandle(forwardedRef, () => ({ - resetForm, - })); - - const registerInput = useCallback( - (inputID, propsToParse = {}) => { - const newRef = inputRefs.current[inputID] || propsToParse.ref || createRef(); - if (inputRefs.current[inputID] !== newRef) { - inputRefs.current[inputID] = newRef; - } - - if (!_.isUndefined(propsToParse.value)) { - inputValues[inputID] = propsToParse.value; - } else if (propsToParse.shouldSaveDraft && !_.isUndefined(draftValues[inputID]) && _.isUndefined(inputValues[inputID])) { - inputValues[inputID] = draftValues[inputID]; - } else if (propsToParse.shouldUseDefaultValue && _.isUndefined(inputValues[inputID])) { - // We force the form to set the input value from the defaultValue props if there is a saved valid value - inputValues[inputID] = propsToParse.defaultValue; - } else if (_.isUndefined(inputValues[inputID])) { - // We want to initialize the input value if it's undefined - inputValues[inputID] = _.isUndefined(propsToParse.defaultValue) ? getInitialValueByType(propsToParse.valueType) : propsToParse.defaultValue; - } - - const errorFields = lodashGet(formState, 'errorFields', {}); - const fieldErrorMessage = - _.chain(errorFields[inputID]) - .keys() - .sortBy() - .reverse() - .map((key) => errorFields[inputID][key]) - .first() - .value() || ''; - - return { - ...propsToParse, - ref: - typeof propsToParse.ref === 'function' - ? (node) => { - propsToParse.ref(node); - newRef.current = node; - } - : newRef, - inputID, - key: propsToParse.key || inputID, - errorText: errors[inputID] || fieldErrorMessage, - value: inputValues[inputID], - // As the text input is controlled, we never set the defaultValue prop - // as this is already happening by the value prop. - defaultValue: undefined, - onTouched: (event) => { - if (!propsToParse.shouldSetTouchedOnBlurOnly) { - setTimeout(() => { - setTouchedInput(inputID); - }, VALIDATE_DELAY); - } - if (_.isFunction(propsToParse.onTouched)) { - propsToParse.onTouched(event); - } - }, - onPress: (event) => { - if (!propsToParse.shouldSetTouchedOnBlurOnly) { - setTimeout(() => { - setTouchedInput(inputID); - }, VALIDATE_DELAY); - } - if (_.isFunction(propsToParse.onPress)) { - propsToParse.onPress(event); - } - }, - onPressOut: (event) => { - // To prevent validating just pressed inputs, we need to set the touched input right after - // onValidate and to do so, we need to delays setTouchedInput of the same amount of time - // as the onValidate is delayed - if (!propsToParse.shouldSetTouchedOnBlurOnly) { - setTimeout(() => { - setTouchedInput(inputID); - }, VALIDATE_DELAY); - } - if (_.isFunction(propsToParse.onPressIn)) { - propsToParse.onPressIn(event); - } - }, - onBlur: (event) => { - // Only run validation when user proactively blurs the input. - if (Visibility.isVisible() && Visibility.hasFocus()) { - const relatedTargetId = lodashGet(event, 'nativeEvent.relatedTarget.id'); - // We delay the validation in order to prevent Checkbox loss of focus when - // the user is focusing a TextInput and proceeds to toggle a CheckBox in - // web and mobile web platforms. - - setTimeout(() => { - if ( - relatedTargetId && - _.includes([CONST.OVERLAY.BOTTOM_BUTTON_NATIVE_ID, CONST.OVERLAY.TOP_BUTTON_NATIVE_ID, CONST.BACK_BUTTON_NATIVE_ID], relatedTargetId) - ) { - return; - } - setTouchedInput(inputID); - if (shouldValidateOnBlur) { - onValidate(inputValues, !hasServerError); - } - }, VALIDATE_DELAY); - } - - if (_.isFunction(propsToParse.onBlur)) { - propsToParse.onBlur(event); - } - }, - onInputChange: (value, key) => { - const inputKey = key || inputID; - setInputValues((prevState) => { - const newState = { - ...prevState, - [inputKey]: value, - }; - - if (shouldValidateOnChange) { - onValidate(newState); - } - return newState; - }); - - if (propsToParse.shouldSaveDraft) { - FormActions.setDraftValues(formID, {[inputKey]: value}); - } - - if (_.isFunction(propsToParse.onValueChange)) { - propsToParse.onValueChange(value, inputKey); - } - }, - }; - }, - [draftValues, formID, errors, formState, hasServerError, inputValues, onValidate, setTouchedInput, shouldValidateOnBlur, shouldValidateOnChange], - ); - const value = useMemo(() => ({registerInput}), [registerInput]); - - return ( - - {/* eslint-disable react/jsx-props-no-spreading */} - - {_.isFunction(children) ? children({inputValues}) : children} - - - ); - }, -); - -FormProvider.displayName = 'Form'; -FormProvider.propTypes = propTypes; -FormProvider.defaultProps = defaultProps; - -export default compose( - withNetwork(), - withOnyx({ - formState: { - key: (props) => props.formID, - }, - draftValues: { - key: (props) => `${props.formID}Draft`, - }, - }), -)(FormProvider); diff --git a/src/components/Form/FormProvider.tsx b/src/components/Form/FormProvider.tsx new file mode 100644 index 000000000000..b7f20566b825 --- /dev/null +++ b/src/components/Form/FormProvider.tsx @@ -0,0 +1,364 @@ +import lodashIsEqual from 'lodash/isEqual'; +import React, {createRef, ForwardedRef, forwardRef, ReactNode, useCallback, useImperativeHandle, useMemo, useRef, useState} from 'react'; +import {OnyxEntry, withOnyx} from 'react-native-onyx'; +import * as ValidationUtils from '@libs/ValidationUtils'; +import Visibility from '@libs/Visibility'; +import * as FormActions from '@userActions/FormActions'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import {Form, Network} from '@src/types/onyx'; +import {Errors} from '@src/types/onyx/OnyxCommon'; +import {isEmptyObject, isNotEmptyObject} from '@src/types/utils/EmptyObject'; +import FormContext from './FormContext'; +import FormWrapper from './FormWrapper'; +import {FormProps, InputRef, InputRefs, InputValues, RegisterInput, ValueType} from './types'; + +// In order to prevent Checkbox focus loss when the user are focusing a TextInput and proceeds to toggle a CheckBox in web and mobile web. +// 200ms delay was chosen as a result of empirical testing. +// More details: https://github.com/Expensify/App/pull/16444#issuecomment-1482983426 +const VALIDATE_DELAY = 200; + +function getInitialValueByType(valueType?: ValueType): false | Date | '' { + switch (valueType) { + case 'string': + return ''; + case 'boolean': + return false; + case 'date': + return new Date(); + default: + return ''; + } +} + +type FormProviderOnyxProps = { + /** Contains the form state that must be accessed outside of the component */ + formState: OnyxEntry; + + /** Contains draft values for each input in the form */ + draftValues: OnyxEntry; + + /** Information about the network */ + network: OnyxEntry; +}; + +type FormProviderProps = FormProviderOnyxProps & + FormProps & { + /** Children to render. */ + children: ((props: {inputValues: InputValues}) => ReactNode) | ReactNode; + + /** Callback to validate the form */ + validate?: (values: InputValues) => Errors; + + /** Should validate function be called when input loose focus */ + shouldValidateOnBlur?: boolean; + + /** Should validate function be called when the value of the input is changed */ + shouldValidateOnChange?: boolean; + }; + +type FormRef = { + resetForm: (optionalValue: InputValues) => void; +}; + +function FormProvider>( + { + formID, + validate, + shouldValidateOnBlur = true, + shouldValidateOnChange = true, + children, + formState, + network, + enabledWhenOffline = false, + draftValues, + onSubmit, + ...rest + }: FormProviderProps, + forwardedRef: ForwardedRef, +) { + const inputRefs = useRef({}); + const touchedInputs = useRef>({}); + const [inputValues, setInputValues] = useState(() => ({...draftValues})); + const [errors, setErrors] = useState({}); + const hasServerError = useMemo(() => !!formState && !isEmptyObject(formState?.errors), [formState]); + + const onValidate = useCallback( + (values: InputValues, shouldClearServerError = true) => { + const trimmedStringValues = ValidationUtils.prepareValues(values); + + if (shouldClearServerError) { + FormActions.setErrors(formID, null); + } + FormActions.setErrorFields(formID, null); + + const validateErrors = validate?.(trimmedStringValues) ?? {}; + + // Validate the input for html tags. It should supercede any other error + Object.entries(trimmedStringValues).forEach(([inputID, inputValue]) => { + // If the input value is empty OR is non-string, we don't need to validate it for HTML tags + if (!inputValue || typeof inputValue !== 'string') { + return; + } + const foundHtmlTagIndex = inputValue.search(CONST.VALIDATE_FOR_HTML_TAG_REGEX); + const leadingSpaceIndex = inputValue.search(CONST.VALIDATE_FOR_LEADINGSPACES_HTML_TAG_REGEX); + + // Return early if there are no HTML characters + if (leadingSpaceIndex === -1 && foundHtmlTagIndex === -1) { + return; + } + + const matchedHtmlTags = inputValue.match(CONST.VALIDATE_FOR_HTML_TAG_REGEX); + let isMatch = CONST.WHITELISTED_TAGS.some((regex) => regex.test(inputValue)); + // Check for any matches that the original regex (foundHtmlTagIndex) matched + if (matchedHtmlTags) { + // Check if any matched inputs does not match in WHITELISTED_TAGS list and return early if needed. + for (const htmlTag of matchedHtmlTags) { + isMatch = CONST.WHITELISTED_TAGS.some((regex) => regex.test(htmlTag)); + if (!isMatch) { + break; + } + } + } + + if (isMatch && leadingSpaceIndex === -1) { + return; + } + + // Add a validation error here because it is a string value that contains HTML characters + validateErrors[inputID] = 'common.error.invalidCharacter'; + }); + + if (typeof validateErrors !== 'object') { + throw new Error('Validate callback must return an empty object or an object with shape {inputID: error}'); + } + + const touchedInputErrors = Object.fromEntries(Object.entries(validateErrors).filter(([inputID]) => !!touchedInputs.current[inputID])); + + if (!lodashIsEqual(errors, touchedInputErrors)) { + setErrors(touchedInputErrors); + } + + return touchedInputErrors; + }, + [errors, formID, validate], + ); + + /** @param inputID - The inputID of the input being touched */ + const setTouchedInput = useCallback( + (inputID: string) => { + touchedInputs.current[inputID] = true; + }, + [touchedInputs], + ); + + const submit = useCallback(() => { + // Return early if the form is already submitting to avoid duplicate submission + if (formState?.isLoading) { + return; + } + + // Prepare values before submitting + const trimmedStringValues = ValidationUtils.prepareValues(inputValues); + + // Touches all form inputs so we can validate the entire form + Object.keys(inputRefs.current).forEach((inputID) => (touchedInputs.current[inputID] = true)); + + // Validate form and return early if any errors are found + if (isNotEmptyObject(onValidate(trimmedStringValues))) { + return; + } + + // Do not submit form if network is offline and the form is not enabled when offline + if (network?.isOffline && !enabledWhenOffline) { + return; + } + + onSubmit(trimmedStringValues); + }, [enabledWhenOffline, formState?.isLoading, inputValues, network?.isOffline, onSubmit, onValidate]); + + const resetForm = useCallback( + (optionalValue: InputValues) => { + Object.keys(inputValues).forEach((inputID) => { + setInputValues((prevState) => { + const copyPrevState = {...prevState}; + + touchedInputs.current[inputID] = false; + copyPrevState[inputID] = optionalValue[inputID] || ''; + + return copyPrevState; + }); + }); + setErrors({}); + }, + [inputValues], + ); + useImperativeHandle(forwardedRef, () => ({ + resetForm, + })); + + const registerInput: RegisterInput = useCallback( + (inputID, inputProps) => { + const newRef: InputRef = inputRefs.current[inputID] ?? inputProps.ref ?? createRef(); + if (inputRefs.current[inputID] !== newRef) { + inputRefs.current[inputID] = newRef; + } + + if (inputProps.value !== undefined) { + inputValues[inputID] = inputProps.value; + } else if (inputProps.shouldSaveDraft && draftValues?.[inputID] !== undefined && inputValues[inputID] === undefined) { + inputValues[inputID] = draftValues[inputID]; + } else if (inputProps.shouldUseDefaultValue && inputValues[inputID] === undefined) { + // We force the form to set the input value from the defaultValue props if there is a saved valid value + inputValues[inputID] = inputProps.defaultValue; + } else if (inputValues[inputID] === undefined) { + // We want to initialize the input value if it's undefined + inputValues[inputID] = inputProps.defaultValue === undefined ? getInitialValueByType(inputProps.valueType) : inputProps.defaultValue; + } + + const errorFields = formState?.errorFields?.[inputID] ?? {}; + const fieldErrorMessage = + Object.keys(errorFields) + .sort() + .map((key) => errorFields[key]) + .at(-1) ?? ''; + + const inputRef = inputProps.ref; + return { + ...inputProps, + ref: + typeof inputRef === 'function' + ? (node) => { + inputRef(node); + if (typeof newRef !== 'function') { + newRef.current = node; + } + } + : newRef, + inputID, + key: inputProps.key ?? inputID, + errorText: errors[inputID] || fieldErrorMessage, + value: inputValues[inputID], + // As the text input is controlled, we never set the defaultValue prop + // as this is already happening by the value prop. + defaultValue: undefined, + onTouched: (event) => { + if (!inputProps.shouldSetTouchedOnBlurOnly) { + setTimeout(() => { + setTouchedInput(inputID); + }, VALIDATE_DELAY); + } + if (typeof inputProps.onTouched === 'function') { + inputProps.onTouched(event); + } + }, + onPress: (event) => { + if (!inputProps.shouldSetTouchedOnBlurOnly) { + setTimeout(() => { + setTouchedInput(inputID); + }, VALIDATE_DELAY); + } + if (typeof inputProps.onPress === 'function') { + inputProps.onPress(event); + } + }, + onPressOut: (event) => { + // To prevent validating just pressed inputs, we need to set the touched input right after + // onValidate and to do so, we need to delays setTouchedInput of the same amount of time + // as the onValidate is delayed + if (!inputProps.shouldSetTouchedOnBlurOnly) { + setTimeout(() => { + setTouchedInput(inputID); + }, VALIDATE_DELAY); + } + if (typeof inputProps.onPressOut === 'function') { + inputProps.onPressOut(event); + } + }, + onBlur: (event) => { + // Only run validation when user proactively blurs the input. + if (Visibility.isVisible() && Visibility.hasFocus()) { + const relatedTarget = 'nativeEvent' in event ? event?.nativeEvent?.relatedTarget : undefined; + const relatedTargetId = relatedTarget && 'id' in relatedTarget && typeof relatedTarget.id === 'string' && relatedTarget.id; + // We delay the validation in order to prevent Checkbox loss of focus when + // the user is focusing a TextInput and proceeds to toggle a CheckBox in + // web and mobile web platforms. + + setTimeout(() => { + if ( + relatedTargetId === CONST.OVERLAY.BOTTOM_BUTTON_NATIVE_ID || + relatedTargetId === CONST.OVERLAY.TOP_BUTTON_NATIVE_ID || + relatedTargetId === CONST.BACK_BUTTON_NATIVE_ID + ) { + return; + } + setTouchedInput(inputID); + if (shouldValidateOnBlur) { + onValidate(inputValues, !hasServerError); + } + }, VALIDATE_DELAY); + } + + if (typeof inputProps.onBlur === 'function') { + inputProps.onBlur(event); + } + }, + onInputChange: (value, key) => { + const inputKey = key || inputID; + setInputValues((prevState) => { + const newState = { + ...prevState, + [inputKey]: value, + }; + + if (shouldValidateOnChange) { + onValidate(newState); + } + return newState; + }); + + if (inputProps.shouldSaveDraft) { + FormActions.setDraftValues(formID, {[inputKey]: value}); + } + + if (typeof inputProps.onValueChange === 'function') { + inputProps.onValueChange(value, inputKey); + } + }, + }; + }, + [draftValues, formID, errors, formState, hasServerError, inputValues, onValidate, setTouchedInput, shouldValidateOnBlur, shouldValidateOnChange], + ); + const value = useMemo(() => ({registerInput}), [registerInput]); + + return ( + + {/* eslint-disable react/jsx-props-no-spreading */} + + {typeof children === 'function' ? children({inputValues}) : children} + + + ); +} + +FormProvider.displayName = 'Form'; + +export default (>() => + withOnyx, FormProviderOnyxProps>({ + network: { + key: ONYXKEYS.NETWORK, + }, + formState: { + key: (props) => props.formID as typeof ONYXKEYS.FORMS.ADD_DEBIT_CARD_FORM, + }, + draftValues: { + key: (props) => `${props.formID}Draft` as typeof ONYXKEYS.FORMS.ADD_DEBIT_CARD_FORM_DRAFT, + }, + })(forwardRef(FormProvider)))(); diff --git a/src/components/Form/FormWrapper.tsx b/src/components/Form/FormWrapper.tsx index ec2f2be2eca7..1c1dd4658d57 100644 --- a/src/components/Form/FormWrapper.tsx +++ b/src/components/Form/FormWrapper.tsx @@ -1,6 +1,6 @@ -import React, {useCallback, useMemo, useRef} from 'react'; -import {Keyboard, ScrollView, View} from 'react-native'; -import {withOnyx} from 'react-native-onyx'; +import React, {MutableRefObject, useCallback, useMemo, useRef} from 'react'; +import {Keyboard, ScrollView, StyleProp, View, ViewStyle} from 'react-native'; +import {OnyxEntry, withOnyx} from 'react-native-onyx'; import FormAlertWithSubmitButton from '@components/FormAlertWithSubmitButton'; import FormSubmit from '@components/FormSubmit'; import SafeAreaConsumer from '@components/SafeAreaConsumer'; @@ -9,8 +9,29 @@ import ScrollViewWithContext from '@components/ScrollViewWithContext'; import useThemeStyles from '@hooks/useThemeStyles'; import * as ErrorUtils from '@libs/ErrorUtils'; import ONYXKEYS from '@src/ONYXKEYS'; +import {Form} from '@src/types/onyx'; +import {Errors} from '@src/types/onyx/OnyxCommon'; +import ChildrenProps from '@src/types/utils/ChildrenProps'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; -import {FormWrapperOnyxProps, FormWrapperProps} from './types'; +import {FormProps, InputRefs} from './types'; + +type FormWrapperOnyxProps = { + /** Contains the form state that must be accessed outside of the component */ + formState: OnyxEntry; +}; + +type FormWrapperProps = ChildrenProps & + FormWrapperOnyxProps & + FormProps & { + /** Submit button styles */ + submitButtonStyles?: StyleProp; + + /** Server side errors keyed by microtime */ + errors: Errors; + + // Assuming refs are React refs + inputRefs: MutableRefObject; + }; function FormWrapper({ onSubmit, @@ -19,14 +40,14 @@ function FormWrapper({ errors, inputRefs, submitButtonText, - footerContent, - isSubmitButtonVisible, + footerContent = null, + isSubmitButtonVisible = true, style, submitButtonStyles, enabledWhenOffline, - isSubmitActionDangerous, + isSubmitActionDangerous = false, formID, - scrollContextEnabled, + scrollContextEnabled = false, }: FormWrapperProps) { const styles = useThemeStyles(); const formRef = useRef(null); @@ -58,7 +79,8 @@ function FormWrapper({ return; } - const focusInput = inputRefs.current?.[focusKey].current; + const inputRef = inputRefs.current?.[focusKey]; + const focusInput = inputRef && 'current' in inputRef ? inputRef.current : undefined; // Dismiss the keyboard for non-text fields by checking if the component has the isFocused method, as only TextInput has this method. if (typeof focusInput?.isFocused !== 'function') { diff --git a/src/components/Form/InputWrapper.tsx b/src/components/Form/InputWrapper.tsx index 1b32409ea1d2..579dd553afaa 100644 --- a/src/components/Form/InputWrapper.tsx +++ b/src/components/Form/InputWrapper.tsx @@ -1,9 +1,9 @@ -import React, {ForwardedRef, forwardRef, useContext} from 'react'; +import React, {forwardRef, useContext} from 'react'; import TextInput from '@components/TextInput'; import FormContext from './FormContext'; -import {InputWrapperProps} from './types'; +import {InputProps, InputRef, InputWrapperProps} from './types'; -function InputWrapper({InputComponent, inputID, valueType = 'string', ...rest}: InputWrapperProps, ref: ForwardedRef) { +function InputWrapper({InputComponent, inputID, valueType = 'string', ...rest}: InputWrapperProps, ref: InputRef) { const {registerInput} = useContext(FormContext); // There are inputs that don't have onBlur methods, to simulate the behavior of onBlur in e.g. checkbox, we had to @@ -13,7 +13,7 @@ function InputWrapper({InputComponent, inputID const shouldSetTouchedOnBlurOnly = InputComponent === TextInput; // eslint-disable-next-line react/jsx-props-no-spreading - return ; + return ; } InputWrapper.displayName = 'InputWrapper'; diff --git a/src/components/Form/types.ts b/src/components/Form/types.ts index 8db4909327e0..801ec15dc62c 100644 --- a/src/components/Form/types.ts +++ b/src/components/Form/types.ts @@ -1,64 +1,75 @@ -import {ElementType, ReactNode, RefObject} from 'react'; -import {StyleProp, TextInput, ViewStyle} from 'react-native'; -import {OnyxEntry} from 'react-native-onyx'; +import {ComponentType, ForwardedRef, ReactNode, SyntheticEvent} from 'react'; +import {GestureResponderEvent, StyleProp, TextInput, ViewStyle} from 'react-native'; import {ValueOf} from 'type-fest'; import ONYXKEYS from '@src/ONYXKEYS'; -import Form from '@src/types/onyx/Form'; -import {Errors} from '@src/types/onyx/OnyxCommon'; -import ChildrenProps from '@src/types/utils/ChildrenProps'; type ValueType = 'string' | 'boolean' | 'date'; -type InputWrapperProps = { - InputComponent: TInput; +type InputWrapperProps = { + InputComponent: ComponentType; inputID: string; valueType?: ValueType; }; -type FormWrapperOnyxProps = { - /** Contains the form state that must be accessed outside of the component */ - formState: OnyxEntry; -}; +type FormID = ValueOf & `${string}Form`; + +type FormProps = { + /** A unique Onyx key identifying the form */ + formID: FormID; -type FormWrapperProps = ChildrenProps & - FormWrapperOnyxProps & { - /** A unique Onyx key identifying the form */ - formID: ValueOf; + /** Text to be displayed in the submit button */ + submitButtonText: string; - /** Text to be displayed in the submit button */ - submitButtonText: string; + /** Controls the submit button's visibility */ + isSubmitButtonVisible?: boolean; - /** Controls the submit button's visibility */ - isSubmitButtonVisible?: boolean; + /** Callback to submit the form */ + onSubmit: (values?: Record) => void; - /** Callback to submit the form */ - onSubmit: () => void; + /** Should the button be enabled when offline */ + enabledWhenOffline?: boolean; - /** Should the button be enabled when offline */ - enabledWhenOffline?: boolean; + /** Whether the form submit action is dangerous */ + isSubmitActionDangerous?: boolean; - /** Whether the form submit action is dangerous */ - isSubmitActionDangerous?: boolean; + /** Whether ScrollWithContext should be used instead of regular ScrollView. Set to true when there's a nested Picker component in Form. */ + scrollContextEnabled?: boolean; - /** Whether ScrollWithContext should be used instead of regular ScrollView. - * Set to true when there's a nested Picker component in Form. - */ - scrollContextEnabled?: boolean; + /** Container styles */ + style?: StyleProp; + + /** Custom content to display in the footer after submit button */ + footerContent?: ReactNode; +}; - /** Container styles */ - style?: StyleProp; +type InputValues = Record; - /** Submit button styles */ - submitButtonStyles?: StyleProp; +type InputRef = ForwardedRef; +type InputRefs = Record; - /** Custom content to display in the footer after submit button */ - footerContent?: ReactNode; +type InputPropsToPass = { + ref?: InputRef; + key?: string; + value?: unknown; + defaultValue?: unknown; + shouldSaveDraft?: boolean; + shouldUseDefaultValue?: boolean; + valueType?: ValueType; + shouldSetTouchedOnBlurOnly?: boolean; + + onValueChange?: (value: unknown, key: string) => void; + onTouched?: (event: GestureResponderEvent | KeyboardEvent) => void; + onPress?: (event: GestureResponderEvent | KeyboardEvent) => void; + onPressOut?: (event: GestureResponderEvent | KeyboardEvent) => void; + onBlur?: (event: SyntheticEvent | FocusEvent) => void; + onInputChange?: (value: unknown, key: string) => void; +}; - /** Server side errors keyed by microtime */ - errors: Errors; +type InputProps = InputPropsToPass & { + inputID: string; + errorText: string; +}; - // Assuming refs are React refs - inputRefs: RefObject>>; - }; +type RegisterInput = (inputID: string, props: InputPropsToPass) => InputProps; -export type {InputWrapperProps, FormWrapperProps, FormWrapperOnyxProps}; +export type {InputWrapperProps, FormProps, InputRef, InputRefs, RegisterInput, ValueType, InputValues, InputProps}; diff --git a/src/libs/actions/FormActions.ts b/src/libs/actions/FormActions.ts index 29d9ecda9f73..c5fc8500aa80 100644 --- a/src/libs/actions/FormActions.ts +++ b/src/libs/actions/FormActions.ts @@ -12,11 +12,11 @@ function setIsLoading(formID: OnyxFormKey, isLoading: boolean) { Onyx.merge(formID, {isLoading} satisfies Form); } -function setErrors(formID: OnyxFormKey, errors: OnyxCommon.Errors) { +function setErrors(formID: OnyxFormKey, errors: OnyxCommon.Errors | null) { Onyx.merge(formID, {errors} satisfies Form); } -function setErrorFields(formID: OnyxFormKey, errorFields: OnyxCommon.ErrorFields) { +function setErrorFields(formID: OnyxFormKey, errorFields: OnyxCommon.ErrorFields | null) { Onyx.merge(formID, {errorFields} satisfies Form); } diff --git a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js index 65b79ed5af78..10a37f811ca6 100644 --- a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js +++ b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js @@ -238,19 +238,6 @@ function FloatingActionButtonAndPopover(props) { withoutOverlay anchorRef={anchorRef} /> - { - if (isCreateMenuActive) { - hideCreateMenu(); - } else { - showCreateMenu(); - } - }} - /> ); } From 2cba56d9fc72bf91a61e43b9f39be3bb6fa49ac7 Mon Sep 17 00:00:00 2001 From: Kamil Owczarz Date: Mon, 18 Dec 2023 14:13:14 +0100 Subject: [PATCH 065/391] Revert FAB changes --- src/components/FloatingActionButton/index.js | 53 ++++++++++++++++++- .../FloatingActionButtonAndPopover.js | 13 +++++ 2 files changed, 65 insertions(+), 1 deletion(-) diff --git a/src/components/FloatingActionButton/index.js b/src/components/FloatingActionButton/index.js index 8e963d49b10c..d341396c44b7 100644 --- a/src/components/FloatingActionButton/index.js +++ b/src/components/FloatingActionButton/index.js @@ -26,7 +26,58 @@ const propTypes = { role: PropTypes.string.isRequired, }; -const FloatingActionButton = React.forwardRef(({onPress, isActive, accessibilityLabel, role}, ref) => null); +const FloatingActionButton = React.forwardRef(({onPress, isActive, accessibilityLabel, role}, ref) => { + const theme = useTheme(); + const styles = useThemeStyles(); + const {translate} = useLocalize(); + const fabPressable = useRef(null); + const animatedValue = useSharedValue(isActive ? 1 : 0); + const buttonRef = ref; + + useEffect(() => { + animatedValue.value = withTiming(isActive ? 1 : 0, { + duration: 340, + easing: Easing.inOut(Easing.ease), + }); + }, [isActive, animatedValue]); + + const animatedStyle = useAnimatedStyle(() => { + const backgroundColor = interpolateColor(animatedValue.value, [0, 1], [theme.success, theme.buttonDefaultBG]); + + return { + transform: [{rotate: `${animatedValue.value * 135}deg`}], + backgroundColor, + borderRadius: styles.floatingActionButton.borderRadius, + }; + }); + + return ( + + + { + fabPressable.current = el; + if (buttonRef) { + buttonRef.current = el; + } + }} + accessibilityLabel={accessibilityLabel} + role={role} + pressDimmingValue={1} + onPress={(e) => { + // Drop focus to avoid blue focus ring. + fabPressable.current.blur(); + onPress(e); + }} + onLongPress={() => {}} + style={[styles.floatingActionButton, animatedStyle]} + > + + + + + ); +}); FloatingActionButton.propTypes = propTypes; FloatingActionButton.displayName = 'FloatingActionButton'; diff --git a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js index 10a37f811ca6..65b79ed5af78 100644 --- a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js +++ b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js @@ -238,6 +238,19 @@ function FloatingActionButtonAndPopover(props) { withoutOverlay anchorRef={anchorRef} /> + { + if (isCreateMenuActive) { + hideCreateMenu(); + } else { + showCreateMenu(); + } + }} + /> ); } From f74be36e3aa4e73eec4a3d626b702e83b4a0ef37 Mon Sep 17 00:00:00 2001 From: Kamil Owczarz Date: Mon, 18 Dec 2023 15:15:57 +0100 Subject: [PATCH 066/391] Fix FormActions types --- src/libs/actions/FormActions.ts | 6 +++--- src/types/onyx/Form.ts | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/libs/actions/FormActions.ts b/src/libs/actions/FormActions.ts index c5fc8500aa80..e5503b2035bc 100644 --- a/src/libs/actions/FormActions.ts +++ b/src/libs/actions/FormActions.ts @@ -12,11 +12,11 @@ function setIsLoading(formID: OnyxFormKey, isLoading: boolean) { Onyx.merge(formID, {isLoading} satisfies Form); } -function setErrors(formID: OnyxFormKey, errors: OnyxCommon.Errors | null) { +function setErrors(formID: OnyxFormKey, errors?: OnyxCommon.Errors | null) { Onyx.merge(formID, {errors} satisfies Form); } -function setErrorFields(formID: OnyxFormKey, errorFields: OnyxCommon.ErrorFields | null) { +function setErrorFields(formID: OnyxFormKey, errorFields?: OnyxCommon.ErrorFields | null) { Onyx.merge(formID, {errorFields} satisfies Form); } @@ -28,7 +28,7 @@ function setDraftValues(formID: OnyxFormKeyWithoutDraft, draftValues: NullishDee * @param formID */ function clearDraftValues(formID: OnyxFormKeyWithoutDraft) { - Onyx.merge(FormUtils.getDraftKey(formID), undefined); + Onyx.merge(FormUtils.getDraftKey(formID), {}); } export {setDraftValues, setErrorFields, setErrors, setIsLoading, clearDraftValues}; diff --git a/src/types/onyx/Form.ts b/src/types/onyx/Form.ts index 7b7d8d76536a..9e5e713b5800 100644 --- a/src/types/onyx/Form.ts +++ b/src/types/onyx/Form.ts @@ -5,10 +5,10 @@ type Form = { isLoading?: boolean; /** Server side errors keyed by microtime */ - errors?: OnyxCommon.Errors; + errors?: OnyxCommon.Errors | null; /** Field-specific server side errors keyed by microtime */ - errorFields?: OnyxCommon.ErrorFields; + errorFields?: OnyxCommon.ErrorFields | null; }; type AddDebitCardForm = Form & { From 6b93011294b9b3b571b3aa8f0a06a5ac7c86e7e3 Mon Sep 17 00:00:00 2001 From: Kamil Owczarz Date: Mon, 18 Dec 2023 15:26:05 +0100 Subject: [PATCH 067/391] Fix FormWrapper types --- src/libs/ErrorUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/ErrorUtils.ts b/src/libs/ErrorUtils.ts index 46bdd510f5c4..3c20f874a3e2 100644 --- a/src/libs/ErrorUtils.ts +++ b/src/libs/ErrorUtils.ts @@ -51,7 +51,7 @@ function getMicroSecondOnyxErrorObject(error: Record): Record(onyxData: TOnyxData): string { From 91239d515f5a5bc4a2f09a2c0d217efb4ed64cf2 Mon Sep 17 00:00:00 2001 From: Kamil Owczarz Date: Tue, 19 Dec 2023 11:52:15 +0100 Subject: [PATCH 068/391] Review changes --- src/components/Form/FormProvider.tsx | 26 ++++++++------------------ src/components/Form/types.ts | 2 +- 2 files changed, 9 insertions(+), 19 deletions(-) diff --git a/src/components/Form/FormProvider.tsx b/src/components/Form/FormProvider.tsx index b7f20566b825..d5e15ef9cb4b 100644 --- a/src/components/Form/FormProvider.tsx +++ b/src/components/Form/FormProvider.tsx @@ -18,7 +18,9 @@ import {FormProps, InputRef, InputRefs, InputValues, RegisterInput, ValueType} f // More details: https://github.com/Expensify/App/pull/16444#issuecomment-1482983426 const VALIDATE_DELAY = 200; -function getInitialValueByType(valueType?: ValueType): false | Date | '' { +type DefaultValue = false | Date | ''; + +function getInitialValueByType(valueType?: ValueType): DefaultValue { switch (valueType) { case 'string': return ''; @@ -248,9 +250,7 @@ function FormProvider>( setTouchedInput(inputID); }, VALIDATE_DELAY); } - if (typeof inputProps.onTouched === 'function') { - inputProps.onTouched(event); - } + inputProps.onTouched?.(event); }, onPress: (event) => { if (!inputProps.shouldSetTouchedOnBlurOnly) { @@ -258,9 +258,7 @@ function FormProvider>( setTouchedInput(inputID); }, VALIDATE_DELAY); } - if (typeof inputProps.onPress === 'function') { - inputProps.onPress(event); - } + inputProps.onPress?.(event); }, onPressOut: (event) => { // To prevent validating just pressed inputs, we need to set the touched input right after @@ -271,9 +269,7 @@ function FormProvider>( setTouchedInput(inputID); }, VALIDATE_DELAY); } - if (typeof inputProps.onPressOut === 'function') { - inputProps.onPressOut(event); - } + inputProps.onPressOut?.(event); }, onBlur: (event) => { // Only run validation when user proactively blurs the input. @@ -298,10 +294,7 @@ function FormProvider>( } }, VALIDATE_DELAY); } - - if (typeof inputProps.onBlur === 'function') { - inputProps.onBlur(event); - } + inputProps.onBlur?.(event); }, onInputChange: (value, key) => { const inputKey = key || inputID; @@ -320,10 +313,7 @@ function FormProvider>( if (inputProps.shouldSaveDraft) { FormActions.setDraftValues(formID, {[inputKey]: value}); } - - if (typeof inputProps.onValueChange === 'function') { - inputProps.onValueChange(value, inputKey); - } + inputProps.onValueChange?.(value, inputKey); }, }; }, diff --git a/src/components/Form/types.ts b/src/components/Form/types.ts index 801ec15dc62c..19784496016c 100644 --- a/src/components/Form/types.ts +++ b/src/components/Form/types.ts @@ -72,4 +72,4 @@ type InputProps = InputPropsToPass & { type RegisterInput = (inputID: string, props: InputPropsToPass) => InputProps; -export type {InputWrapperProps, FormProps, InputRef, InputRefs, RegisterInput, ValueType, InputValues, InputProps}; +export type {InputWrapperProps, FormProps, InputRef, InputRefs, RegisterInput, ValueType, InputValues, InputProps, FormID}; From b0d589c6f59b2a74b88da41284db89d228019220 Mon Sep 17 00:00:00 2001 From: Kamil Owczarz Date: Tue, 19 Dec 2023 14:59:58 +0100 Subject: [PATCH 069/391] Update onyx keys --- src/ONYXKEYS.ts | 8 ++++---- src/components/Form/FormProvider.tsx | 8 ++++---- src/components/Form/FormWrapper.tsx | 10 ++++------ 3 files changed, 12 insertions(+), 14 deletions(-) diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 9b062aae5532..dfa6a40d1bbf 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -462,8 +462,8 @@ type OnyxValues = { [ONYXKEYS.COLLECTION.PRIVATE_NOTES_DRAFT]: string; // Forms - [ONYXKEYS.FORMS.ADD_DEBIT_CARD_FORM]: OnyxTypes.Form; - [ONYXKEYS.FORMS.ADD_DEBIT_CARD_FORM_DRAFT]: OnyxTypes.Form; + [ONYXKEYS.FORMS.ADD_DEBIT_CARD_FORM]: OnyxTypes.AddDebitCardForm; + [ONYXKEYS.FORMS.ADD_DEBIT_CARD_FORM_DRAFT]: OnyxTypes.AddDebitCardForm; [ONYXKEYS.FORMS.WORKSPACE_SETTINGS_FORM]: OnyxTypes.Form; [ONYXKEYS.FORMS.WORKSPACE_SETTINGS_FORM_DRAFT]: OnyxTypes.Form; [ONYXKEYS.FORMS.WORKSPACE_RATE_AND_UNIT_FORM]: OnyxTypes.Form; @@ -482,8 +482,8 @@ type OnyxValues = { [ONYXKEYS.FORMS.LEGAL_NAME_FORM_DRAFT]: OnyxTypes.Form; [ONYXKEYS.FORMS.WORKSPACE_INVITE_MESSAGE_FORM]: OnyxTypes.Form; [ONYXKEYS.FORMS.WORKSPACE_INVITE_MESSAGE_FORM_DRAFT]: OnyxTypes.Form; - [ONYXKEYS.FORMS.DATE_OF_BIRTH_FORM]: OnyxTypes.Form; - [ONYXKEYS.FORMS.DATE_OF_BIRTH_FORM_DRAFT]: OnyxTypes.Form; + [ONYXKEYS.FORMS.DATE_OF_BIRTH_FORM]: OnyxTypes.DateOfBirthForm; + [ONYXKEYS.FORMS.DATE_OF_BIRTH_FORM_DRAFT]: OnyxTypes.DateOfBirthForm; [ONYXKEYS.FORMS.HOME_ADDRESS_FORM]: OnyxTypes.Form; [ONYXKEYS.FORMS.HOME_ADDRESS_FORM_DRAFT]: OnyxTypes.Form; [ONYXKEYS.FORMS.NEW_ROOM_FORM]: OnyxTypes.Form; diff --git a/src/components/Form/FormProvider.tsx b/src/components/Form/FormProvider.tsx index d5e15ef9cb4b..ea00680e6c96 100644 --- a/src/components/Form/FormProvider.tsx +++ b/src/components/Form/FormProvider.tsx @@ -18,9 +18,9 @@ import {FormProps, InputRef, InputRefs, InputValues, RegisterInput, ValueType} f // More details: https://github.com/Expensify/App/pull/16444#issuecomment-1482983426 const VALIDATE_DELAY = 200; -type DefaultValue = false | Date | ''; +type InitialDefaultValue = false | Date | ''; -function getInitialValueByType(valueType?: ValueType): DefaultValue { +function getInitialValueByType(valueType?: ValueType): InitialDefaultValue { switch (valueType) { case 'string': return ''; @@ -346,9 +346,9 @@ export default (>() => key: ONYXKEYS.NETWORK, }, formState: { - key: (props) => props.formID as typeof ONYXKEYS.FORMS.ADD_DEBIT_CARD_FORM, + key: (props) => props.formID as keyof typeof ONYXKEYS.FORMS, }, draftValues: { - key: (props) => `${props.formID}Draft` as typeof ONYXKEYS.FORMS.ADD_DEBIT_CARD_FORM_DRAFT, + key: (props) => `${props.formID}Draft` as keyof typeof ONYXKEYS.FORMS, }, })(forwardRef(FormProvider)))(); diff --git a/src/components/Form/FormWrapper.tsx b/src/components/Form/FormWrapper.tsx index 1c1dd4658d57..7dcb41c9adcb 100644 --- a/src/components/Form/FormWrapper.tsx +++ b/src/components/Form/FormWrapper.tsx @@ -88,11 +88,11 @@ function FormWrapper({ } // We subtract 10 to scroll slightly above the input - if (focusInput?.measureLayout && formContentRef.current && typeof focusInput.measureLayout === 'function') { + if (formContentRef.current) { // We measure relative to the content root, not the scroll view, as that gives // consistent results across mobile and web // eslint-disable-next-line @typescript-eslint/naming-convention - focusInput.measureLayout(formContentRef.current, (_x, y) => + focusInput?.measureLayout?.(formContentRef.current, (_x, y) => formRef.current?.scrollTo({ y: y - 10, animated: false, @@ -101,9 +101,7 @@ function FormWrapper({ } // Focus the input after scrolling, as on the Web it gives a slightly better visual result - if (focusInput?.focus && typeof focusInput.focus === 'function') { - focusInput.focus(); - } + focusInput?.focus?.(); }} // @ts-expect-error FormAlertWithSubmitButton migration containerStyles={[styles.mh0, styles.mt5, styles.flex1, submitButtonStyles]} @@ -168,6 +166,6 @@ FormWrapper.displayName = 'FormWrapper'; export default withOnyx({ formState: { // FIX: Fabio plz help 😂 - key: (props) => props.formID as typeof ONYXKEYS.FORMS.ADD_DEBIT_CARD_FORM, + key: (props) => props.formID as keyof typeof ONYXKEYS.FORMS, }, })(FormWrapper); From ffb65dc3cf7a2af53319a51a4ce46c2d9b077860 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Tue, 19 Dec 2023 21:04:04 +0100 Subject: [PATCH 070/391] fix: adress comments --- src/libs/OptionsListUtils.ts | 11 ++++++----- src/utils/get.ts | 15 --------------- src/utils/set.ts | 15 --------------- src/utils/times.ts | 2 +- tests/unit/get.ts | 27 --------------------------- tests/unit/set.ts | 29 ----------------------------- 6 files changed, 7 insertions(+), 92 deletions(-) delete mode 100644 src/utils/get.ts delete mode 100644 src/utils/set.ts delete mode 100644 tests/unit/get.ts delete mode 100644 tests/unit/set.ts diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index f4e9b83e71c0..1a7eca731f59 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -1,6 +1,9 @@ /* eslint-disable no-continue */ import Str from 'expensify-common/lib/str'; +// eslint-disable-next-line you-dont-need-lodash-underscore/get +import lodashGet from 'lodash/get'; import lodashOrderBy from 'lodash/orderBy'; +import lodashSet from 'lodash/set'; import Onyx, {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import CONST from '@src/CONST'; import {TranslationPaths} from '@src/languages/types'; @@ -10,8 +13,6 @@ import {Participant} from '@src/types/onyx/IOU'; import * as OnyxCommon from '@src/types/onyx/OnyxCommon'; import DeepValueOf from '@src/types/utils/DeepValueOf'; import {EmptyObject, isEmptyObject, isNotEmptyObject} from '@src/types/utils/EmptyObject'; -import get from '@src/utils/get'; -import set from '@src/utils/set'; import sortBy from '@src/utils/sortBy'; import times from '@src/utils/times'; import * as CollectionUtils from './CollectionUtils'; @@ -754,8 +755,8 @@ function sortCategories(categories: Record): Category[] { */ sortedCategories.forEach((category) => { const path = category.name.split(CONST.PARENT_CHILD_SEPARATOR); - const existedValue = get(hierarchy, path, {}); - set(hierarchy, path, { + const existedValue = lodashGet(hierarchy, path, {}); + lodashSet(hierarchy, path, { ...existedValue, name: category.name, }); @@ -959,7 +960,7 @@ function getCategoryListSections( } /** - * Transforms the provided tags into objects with a specific structure. + * Transforms the provided tags into option objects. * * @param tags - an initial tag array * @param tags[].enabled - a flag to enable/disable option in a list diff --git a/src/utils/get.ts b/src/utils/get.ts deleted file mode 100644 index 41c720840f83..000000000000 --- a/src/utils/get.ts +++ /dev/null @@ -1,15 +0,0 @@ -function get, U>(obj: T, path: string | string[], defValue?: U): T | U | undefined { - // If path is not defined or it has false value - if (!path || path.length === 0) { - return undefined; - } - // Check if path is string or array. Regex : ensure that we do not have '.' and brackets. - // Regex explained: https://regexr.com/58j0k - const pathArray = Array.isArray(path) ? path : path.match(/([^[.\]])+/g); - // Find value - const result = pathArray?.reduce((prevObj, key) => prevObj && (prevObj[key] as T), obj); - // If found value is undefined return default value; otherwise return the value - return result ?? defValue; -} - -export default get; diff --git a/src/utils/set.ts b/src/utils/set.ts deleted file mode 100644 index 9aa432638417..000000000000 --- a/src/utils/set.ts +++ /dev/null @@ -1,15 +0,0 @@ -function set, U>(obj: T, path: string | string[], value: U): void { - const pathArray = Array.isArray(path) ? path : path.split('.'); - - pathArray.reduce((acc: Record, key: string, i: number) => { - if (acc[key] === undefined) { - acc[key] = {}; - } - if (i === pathArray.length - 1) { - (acc[key] as U) = value; - } - return acc[key] as Record; - }, obj); -} - -export default set; diff --git a/src/utils/times.ts b/src/utils/times.ts index 91fbc1c1b412..1dc97eb74659 100644 --- a/src/utils/times.ts +++ b/src/utils/times.ts @@ -1,4 +1,4 @@ -function times(n: number, func = (i: number): string | number | undefined => i): Array { +function times(n: number, func: (index: number) => TReturnType = (i) => i as TReturnType): TReturnType[] { // eslint-disable-next-line @typescript-eslint/naming-convention return Array.from({length: n}).map((_, i) => func(i)); } diff --git a/tests/unit/get.ts b/tests/unit/get.ts deleted file mode 100644 index ac19a5c6353d..000000000000 --- a/tests/unit/get.ts +++ /dev/null @@ -1,27 +0,0 @@ -import get from '@src/utils/get'; - -describe('get', () => { - it('should return the value at path of object', () => { - const obj = {a: {b: 2}}; - expect(get(obj, 'a.b', 0)).toBe(2); - expect(get(obj, ['a', 'b'], 0)).toBe(2); - }); - - it('should return undefined if path does not exist', () => { - const obj = {a: {b: 2}}; - expect(get(obj, 'a.c')).toBeUndefined(); - expect(get(obj, ['a', 'c'])).toBeUndefined(); - }); - - it('should return default value if path does not exist', () => { - const obj = {a: {b: 2}}; - expect(get(obj, 'a.c', 3)).toBe(3); - expect(get(obj, ['a', 'c'], 3)).toBe(3); - }); - - it('should return undefined if path is not defined or it has false value', () => { - const obj = {a: {b: 2}}; - expect(get(obj, '', 3)).toBeUndefined(); - expect(get(obj, [], 3)).toBeUndefined(); - }); -}); diff --git a/tests/unit/set.ts b/tests/unit/set.ts deleted file mode 100644 index 221f18bf0039..000000000000 --- a/tests/unit/set.ts +++ /dev/null @@ -1,29 +0,0 @@ -import set from '@src/utils/set'; - -describe('set', () => { - it('should set the value at path of object', () => { - const obj = {a: {b: 2}}; - set(obj, 'a.b', 3); - expect(obj.a.b).toBe(3); - }); - - it('should set the value at path of object (array path)', () => { - const obj = {a: {b: 2}}; - set(obj, ['a', 'b'], 3); - expect(obj.a.b).toBe(3); - }); - - it('should create nested properties if they do not exist', () => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const obj: any = {a: {}}; - set(obj, 'a.b.c', 3); - expect(obj.a.b.c).toBe(3); - }); - - it('should handle root-level properties', () => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const obj: any = {a: 1}; - set(obj, 'b', 2); - expect(obj.b).toBe(2); - }); -}); From 0b55bfe3158869695da143e55fe80e2fa432a9c6 Mon Sep 17 00:00:00 2001 From: Viktoryia Kliushun Date: Wed, 20 Dec 2023 13:38:41 +0100 Subject: [PATCH 071/391] [TS migration] Migrate 'StatePicker' component --- src/components/MenuItem.tsx | 8 +-- ...electorModal.js => StateSelectorModal.tsx} | 44 +++++++-------- .../StatePicker/{index.js => index.tsx} | 53 +++++-------------- src/libs/searchCountryOptions.ts | 1 + 4 files changed, 38 insertions(+), 68 deletions(-) rename src/components/StatePicker/{StateSelectorModal.js => StateSelectorModal.tsx} (72%) rename src/components/StatePicker/{index.js => index.tsx} (58%) diff --git a/src/components/MenuItem.tsx b/src/components/MenuItem.tsx index c2cc4abce6c5..c1d1aa7d71d2 100644 --- a/src/components/MenuItem.tsx +++ b/src/components/MenuItem.tsx @@ -84,7 +84,7 @@ type MenuItemProps = (ResponsiveProps | UnresponsiveProps) & titleStyle?: ViewStyle; /** Any adjustments to style when menu item is hovered or pressed */ - hoverAndPressStyle: StyleProp>; + hoverAndPressStyle?: StyleProp>; /** Additional styles to style the description text below the title */ descriptionTextStyle?: StyleProp; @@ -174,7 +174,7 @@ type MenuItemProps = (ResponsiveProps | UnresponsiveProps) & isSelected?: boolean; /** Prop to identify if we should load avatars vertically instead of diagonally */ - shouldStackHorizontally: boolean; + shouldStackHorizontally?: boolean; /** Prop to represent the size of the avatar images to be shown */ avatarSize?: (typeof CONST.AVATAR_SIZE)[keyof typeof CONST.AVATAR_SIZE]; @@ -219,10 +219,10 @@ type MenuItemProps = (ResponsiveProps | UnresponsiveProps) & furtherDetails?: string; /** The function that should be called when this component is LongPressed or right-clicked. */ - onSecondaryInteraction: () => void; + onSecondaryInteraction?: () => void; /** Array of objects that map display names to their corresponding tooltip */ - titleWithTooltips: DisplayNameWithTooltip[]; + titleWithTooltips?: DisplayNameWithTooltip[]; }; function MenuItem( diff --git a/src/components/StatePicker/StateSelectorModal.js b/src/components/StatePicker/StateSelectorModal.tsx similarity index 72% rename from src/components/StatePicker/StateSelectorModal.js rename to src/components/StatePicker/StateSelectorModal.tsx index 003211478529..946c54048d79 100644 --- a/src/components/StatePicker/StateSelectorModal.js +++ b/src/components/StatePicker/StateSelectorModal.tsx @@ -1,48 +1,41 @@ import {CONST as COMMON_CONST} from 'expensify-common/lib/CONST'; -import PropTypes from 'prop-types'; import React, {useEffect, useMemo} from 'react'; -import _ from 'underscore'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import Modal from '@components/Modal'; import ScreenWrapper from '@components/ScreenWrapper'; import SelectionList from '@components/SelectionList'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; -import searchCountryOptions from '@libs/searchCountryOptions'; +import searchCountryOptions, {type CountryData} from '@libs/searchCountryOptions'; import StringUtils from '@libs/StringUtils'; import CONST from '@src/CONST'; -const propTypes = { +type State = keyof typeof COMMON_CONST.STATES; + +type StateSelectorModalProps = { /** Whether the modal is visible */ - isVisible: PropTypes.bool.isRequired, + isVisible: boolean; /** State value selected */ - currentState: PropTypes.string, + currentState?: State | ''; /** Function to call when the user selects a State */ - onStateSelected: PropTypes.func, + onStateSelected?: (state: CountryData) => void; /** Function to call when the user closes the State modal */ - onClose: PropTypes.func, + onClose?: () => void; /** The search value from the selection list */ - searchValue: PropTypes.string.isRequired, + searchValue: string; /** Function to call when the user types in the search input */ - setSearchValue: PropTypes.func.isRequired, + setSearchValue: (value: string) => void; /** Label to display on field */ - label: PropTypes.string, -}; - -const defaultProps = { - currentState: '', - onClose: () => {}, - onStateSelected: () => {}, - label: undefined, + label?: string; }; -function StateSelectorModal({currentState, isVisible, onClose, onStateSelected, searchValue, setSearchValue, label}) { +function StateSelectorModal({currentState = '', isVisible, onClose = () => {}, onStateSelected = () => {}, searchValue, setSearchValue, label}: StateSelectorModalProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); @@ -53,11 +46,11 @@ function StateSelectorModal({currentState, isVisible, onClose, onStateSelected, setSearchValue(''); }, [isVisible, setSearchValue]); - const countryStates = useMemo( + const countryStates: CountryData[] = useMemo( () => - _.map(_.keys(COMMON_CONST.STATES), (state) => { - const stateName = translate(`allStates.${state}.stateName`); - const stateISO = translate(`allStates.${state}.stateISO`); + Object.keys(COMMON_CONST.STATES).map((state) => { + const stateName = translate(`allStates.${state as State}.stateName`); + const stateISO = translate(`allStates.${state as State}.stateISO`); return { value: stateISO, keyForList: stateISO, @@ -81,6 +74,7 @@ function StateSelectorModal({currentState, isVisible, onClose, onStateSelected, hideModalContentWhileAnimating useNativeDriver > + {/* @ts-expect-error TODO: Remove this once ScreenWrapper (https://github.com/Expensify/App/issues/25128) is migrated to TypeScript. */} void; /** Label to display on field */ - label: PropTypes.string, -}; - -const defaultProps = { - value: undefined, - forwardedRef: undefined, - errorText: '', - onInputChange: () => {}, - label: undefined, + label?: string; }; -function StatePicker({value, errorText, onInputChange, forwardedRef, label}) { +function StatePicker({value, onInputChange, label, errorText = ''}: StatePickerProps, ref: ForwardedRef) { const styles = useThemeStyles(); const {translate} = useLocalize(); const [isPickerVisible, setIsPickerVisible] = useState(false); @@ -49,20 +36,20 @@ function StatePicker({value, errorText, onInputChange, forwardedRef, label}) { setIsPickerVisible(false); }; - const updateStateInput = (state) => { + const updateStateInput = (state: CountryData) => { if (state.value !== value) { - onInputChange(state.value); + onInputChange?.(state.value); } hidePickerModal(); }; - const title = value && _.keys(COMMON_CONST.STATES).includes(value) ? translate(`allStates.${value}.stateName`) : ''; + const title = value && Object.keys(COMMON_CONST.STATES).includes(value) ? translate(`allStates.${value}.stateName`) : ''; const descStyle = title.length === 0 ? styles.textNormal : null; return ( ( - -)); - -StatePickerWithRef.displayName = 'StatePickerWithRef'; - -export default StatePickerWithRef; +export default React.forwardRef(StatePicker); diff --git a/src/libs/searchCountryOptions.ts b/src/libs/searchCountryOptions.ts index 8fb1cc9c37f3..1fc5d343f556 100644 --- a/src/libs/searchCountryOptions.ts +++ b/src/libs/searchCountryOptions.ts @@ -37,3 +37,4 @@ function searchCountryOptions(searchValue: string, countriesData: CountryData[]) } export default searchCountryOptions; +export type {CountryData}; From 6c9c06f3740382285a9c4acee716eaea943c537e Mon Sep 17 00:00:00 2001 From: Viktoryia Kliushun Date: Wed, 20 Dec 2023 14:11:17 +0100 Subject: [PATCH 072/391] Use nullish coalescing operator --- src/components/StatePicker/StateSelectorModal.tsx | 4 ++-- src/components/StatePicker/index.tsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/StatePicker/StateSelectorModal.tsx b/src/components/StatePicker/StateSelectorModal.tsx index 946c54048d79..deee159ff906 100644 --- a/src/components/StatePicker/StateSelectorModal.tsx +++ b/src/components/StatePicker/StateSelectorModal.tsx @@ -82,14 +82,14 @@ function StateSelectorModal({currentState = '', isVisible, onClose = () => {}, o testID={StateSelectorModal.displayName} > From 9f5666ebfb3f3534feb9c3660122109507f61d0f Mon Sep 17 00:00:00 2001 From: Viktoryia Kliushun Date: Wed, 20 Dec 2023 16:10:23 +0100 Subject: [PATCH 073/391] Minor code improvement --- src/components/StatePicker/StateSelectorModal.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/StatePicker/StateSelectorModal.tsx b/src/components/StatePicker/StateSelectorModal.tsx index deee159ff906..2871c2ebdaf5 100644 --- a/src/components/StatePicker/StateSelectorModal.tsx +++ b/src/components/StatePicker/StateSelectorModal.tsx @@ -17,7 +17,7 @@ type StateSelectorModalProps = { isVisible: boolean; /** State value selected */ - currentState?: State | ''; + currentState?: State; /** Function to call when the user selects a State */ onStateSelected?: (state: CountryData) => void; @@ -35,7 +35,7 @@ type StateSelectorModalProps = { label?: string; }; -function StateSelectorModal({currentState = '', isVisible, onClose = () => {}, onStateSelected = () => {}, searchValue, setSearchValue, label}: StateSelectorModalProps) { +function StateSelectorModal({currentState, isVisible, onClose = () => {}, onStateSelected = () => {}, searchValue, setSearchValue, label}: StateSelectorModalProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); From b676aa93c9337ee179980d8d9ce0e0ac31872603 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Thu, 21 Dec 2023 09:49:44 +0100 Subject: [PATCH 074/391] fix: address comments --- src/libs/ModifiedExpenseMessage.ts | 10 ++++----- src/libs/OptionsListUtils.ts | 7 +++--- src/utils/sortBy.ts | 35 ------------------------------ tests/unit/sortBy.ts | 21 ------------------ 4 files changed, 8 insertions(+), 65 deletions(-) delete mode 100644 src/utils/sortBy.ts delete mode 100644 tests/unit/sortBy.ts diff --git a/src/libs/ModifiedExpenseMessage.ts b/src/libs/ModifiedExpenseMessage.ts index c3d9b0a85339..247ba73d93a3 100644 --- a/src/libs/ModifiedExpenseMessage.ts +++ b/src/libs/ModifiedExpenseMessage.ts @@ -1,5 +1,5 @@ import {format} from 'date-fns'; -import Onyx from 'react-native-onyx'; +import Onyx, {OnyxEntry} from 'react-native-onyx'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import {PolicyTags, ReportAction} from '@src/types/onyx'; @@ -93,12 +93,12 @@ function getForDistanceRequest(newDistance: string, oldDistance: string, newAmou * ModifiedExpense::getNewDotComment in Web-Expensify should match this. * If we change this function be sure to update the backend as well. */ -function getForReportAction(reportAction: ReportAction): string { - if (reportAction.actionName !== CONST.REPORT.ACTIONS.TYPE.MODIFIEDEXPENSE) { +function getForReportAction(reportAction: OnyxEntry): string { + if (reportAction?.actionName !== CONST.REPORT.ACTIONS.TYPE.MODIFIEDEXPENSE) { return ''; } - const reportActionOriginalMessage = reportAction.originalMessage as ExpenseOriginalMessage | undefined; - const policyID = ReportUtils.getReportPolicyID(reportAction.reportID) ?? ''; + const reportActionOriginalMessage = reportAction?.originalMessage as ExpenseOriginalMessage | undefined; + const policyID = ReportUtils.getReportPolicyID(reportAction?.reportID) ?? ''; const policyTags = allPolicyTags?.[`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`] ?? {}; const policyTagListName = PolicyUtils.getTagListName(policyTags) || Localize.translateLocal('common.tag'); diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 9b9f8a9e69cc..f5ee1d20a080 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -4,6 +4,7 @@ import Str from 'expensify-common/lib/str'; import lodashGet from 'lodash/get'; import lodashOrderBy from 'lodash/orderBy'; import lodashSet from 'lodash/set'; +import lodashSortBy from 'lodash/sortBy'; import Onyx, {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import CONST from '@src/CONST'; import {TranslationPaths} from '@src/languages/types'; @@ -13,7 +14,6 @@ import {Participant} from '@src/types/onyx/IOU'; import * as OnyxCommon from '@src/types/onyx/OnyxCommon'; import DeepValueOf from '@src/types/utils/DeepValueOf'; import {EmptyObject, isEmptyObject, isNotEmptyObject} from '@src/types/utils/EmptyObject'; -import sortBy from '@src/utils/sortBy'; import times from '@src/utils/times'; import * as CollectionUtils from './CollectionUtils'; import * as ErrorUtils from './ErrorUtils'; @@ -473,8 +473,7 @@ function getAllReportErrors(report: OnyxEntry, reportActions: OnyxEntry< * Get the last message text from the report directly or from other sources for special cases. */ function getLastMessageTextForReport(report: OnyxEntry): string { - const lastReportAction: OnyxEntry = - allSortedReportActions[report?.reportID ?? '']?.find((reportAction) => ReportActionUtils.shouldReportActionBeVisibleAsLastAction(reportAction)) ?? null; + const lastReportAction = allSortedReportActions[report?.reportID ?? '']?.find((reportAction) => ReportActionUtils.shouldReportActionBeVisibleAsLastAction(reportAction)) ?? null; let lastMessageTextFromReport = ''; const lastActionName = lastReportAction?.actionName ?? ''; @@ -1178,7 +1177,7 @@ function getOptions( // Sorting the reports works like this: // - Order everything by the last message timestamp (descending) // - All archived reports should remain at the bottom - const orderedReports = sortBy(filteredReports, (report) => { + const orderedReports = lodashSortBy(filteredReports, (report) => { if (ReportUtils.isArchivedRoom(report)) { return CONST.DATE.UNIX_EPOCH; } diff --git a/src/utils/sortBy.ts b/src/utils/sortBy.ts deleted file mode 100644 index ae8a98c79564..000000000000 --- a/src/utils/sortBy.ts +++ /dev/null @@ -1,35 +0,0 @@ -function sortBy(array: T[], keyOrFunction: keyof T | ((value: T) => unknown)): T[] { - return [...array].sort((a, b) => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let aValue: any; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let bValue: any; - - // Check if a function was provided - if (typeof keyOrFunction === 'function') { - aValue = keyOrFunction(a); - bValue = keyOrFunction(b); - } else { - aValue = a[keyOrFunction]; - bValue = b[keyOrFunction]; - } - - // Convert dates to timestamps for comparison - if (aValue instanceof Date) { - aValue = aValue.getTime(); - } - if (bValue instanceof Date) { - bValue = bValue.getTime(); - } - - if (aValue < bValue) { - return -1; - } - if (aValue > bValue) { - return 1; - } - return 0; - }); -} - -export default sortBy; diff --git a/tests/unit/sortBy.ts b/tests/unit/sortBy.ts deleted file mode 100644 index bbd1333b974c..000000000000 --- a/tests/unit/sortBy.ts +++ /dev/null @@ -1,21 +0,0 @@ -import sortBy from '@src/utils/sortBy'; - -describe('sortBy', () => { - it('should sort by object key', () => { - const array = [{id: 3}, {id: 1}, {id: 2}]; - const sorted = sortBy(array, 'id'); - expect(sorted).toEqual([{id: 1}, {id: 2}, {id: 3}]); - }); - - it('should sort by function', () => { - const array = [{id: 3}, {id: 1}, {id: 2}]; - const sorted = sortBy(array, (obj) => obj.id); - expect(sorted).toEqual([{id: 1}, {id: 2}, {id: 3}]); - }); - - it('should sort by date', () => { - const array = [{date: new Date(2022, 1, 1)}, {date: new Date(2022, 0, 1)}, {date: new Date(2022, 2, 1)}]; - const sorted = sortBy(array, 'date'); - expect(sorted).toEqual([{date: new Date(2022, 0, 1)}, {date: new Date(2022, 1, 1)}, {date: new Date(2022, 2, 1)}]); - }); -}); From 583221b61d0ba8c0adfccfcf1348bd188f8424ce Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Thu, 21 Dec 2023 10:00:18 +0100 Subject: [PATCH 075/391] Fix import and type issues in AvatarWithDisplayName and SidebarUtils --- src/components/AvatarWithDisplayName.tsx | 4 ++-- src/libs/SidebarUtils.ts | 6 ++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/components/AvatarWithDisplayName.tsx b/src/components/AvatarWithDisplayName.tsx index 409af1a121b1..5807c19bd209 100644 --- a/src/components/AvatarWithDisplayName.tsx +++ b/src/components/AvatarWithDisplayName.tsx @@ -11,7 +11,7 @@ import * as ReportUtils from '@libs/ReportUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import {PersonalDetailsList, Policy, Report, ReportActions} from '@src/types/onyx'; +import {PersonalDetails, PersonalDetailsList, Policy, Report, ReportActions} from '@src/types/onyx'; import DisplayNames from './DisplayNames'; import MultipleAvatars from './MultipleAvatars'; import ParentNavigationSubtitle from './ParentNavigationSubtitle'; @@ -62,7 +62,7 @@ function AvatarWithDisplayName({ const isMoneyRequestOrReport = ReportUtils.isMoneyRequestReport(report) || ReportUtils.isMoneyRequest(report); const icons = ReportUtils.getIcons(report, personalDetails, null, '', -1, policy); const ownerPersonalDetails = OptionsListUtils.getPersonalDetailsForAccountIDs(report?.ownerAccountID ? [report.ownerAccountID] : [], personalDetails); - const displayNamesWithTooltips = ReportUtils.getDisplayNamesWithTooltips(Object.values(ownerPersonalDetails), false); + const displayNamesWithTooltips = ReportUtils.getDisplayNamesWithTooltips(Object.values(ownerPersonalDetails) as PersonalDetails[], false); const shouldShowSubscriptAvatar = ReportUtils.shouldReportShowSubscript(report); const isExpenseRequest = ReportUtils.isExpenseRequest(report); const avatarBorderColor = isAnonymous ? theme.highlightBG : theme.componentBG; diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index f75bbc8c481a..231ca1f193a0 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -271,8 +271,10 @@ function getOptionData( isWaitingOnBankAccount: false, isAllowedToComment: true, }; - const participantPersonalDetailList = Object.values(OptionsListUtils.getPersonalDetailsForAccountIDs(report.participantAccountIDs ?? [], personalDetails)); - const personalDetail = participantPersonalDetailList[0] ?? {}; + const participantPersonalDetailList = Object.values(OptionsListUtils.getPersonalDetailsForAccountIDs(report.participantAccountIDs ?? [], personalDetails)).filter( + Boolean, + ) as PersonalDetails[]; + const personalDetail = participantPersonalDetailList[0]; result.isThread = ReportUtils.isChatThread(report); result.isChatRoom = ReportUtils.isChatRoom(report); From e4136e9896904ea48bbe6095ca38e066b20ed2e2 Mon Sep 17 00:00:00 2001 From: Kamil Owczarz Date: Thu, 21 Dec 2023 15:52:53 +0100 Subject: [PATCH 076/391] Update temporary types --- src/components/Form/FormProvider.tsx | 4 ++-- src/components/Form/FormWrapper.tsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/Form/FormProvider.tsx b/src/components/Form/FormProvider.tsx index ea00680e6c96..65849d614ac7 100644 --- a/src/components/Form/FormProvider.tsx +++ b/src/components/Form/FormProvider.tsx @@ -346,9 +346,9 @@ export default (>() => key: ONYXKEYS.NETWORK, }, formState: { - key: (props) => props.formID as keyof typeof ONYXKEYS.FORMS, + key: (props) => props.formID as typeof ONYXKEYS.FORMS.EDIT_TASK_FORM, }, draftValues: { - key: (props) => `${props.formID}Draft` as keyof typeof ONYXKEYS.FORMS, + key: (props) => `${props.formID}Draft` as typeof ONYXKEYS.FORMS.EDIT_TASK_FORM, }, })(forwardRef(FormProvider)))(); diff --git a/src/components/Form/FormWrapper.tsx b/src/components/Form/FormWrapper.tsx index 7dcb41c9adcb..e34ca0213d2e 100644 --- a/src/components/Form/FormWrapper.tsx +++ b/src/components/Form/FormWrapper.tsx @@ -166,6 +166,6 @@ FormWrapper.displayName = 'FormWrapper'; export default withOnyx({ formState: { // FIX: Fabio plz help 😂 - key: (props) => props.formID as keyof typeof ONYXKEYS.FORMS, + key: (props) => props.formID as typeof ONYXKEYS.FORMS.EDIT_TASK_FORM, }, })(FormWrapper); From 4f25fa70385ad275a55d86f726a1cfca433a4b40 Mon Sep 17 00:00:00 2001 From: brunovjk Date: Fri, 22 Dec 2023 00:57:12 -0300 Subject: [PATCH 077/391] Enhance 'OptionsListSkeletonView' for quicker display using Dimensions. --- src/components/OptionsListSkeletonView.js | 130 ++++++++++------------ 1 file changed, 58 insertions(+), 72 deletions(-) diff --git a/src/components/OptionsListSkeletonView.js b/src/components/OptionsListSkeletonView.js index 2c46ac5d4d7a..f2e77f80f96e 100644 --- a/src/components/OptionsListSkeletonView.js +++ b/src/components/OptionsListSkeletonView.js @@ -1,6 +1,6 @@ import PropTypes from 'prop-types'; import React from 'react'; -import {View} from 'react-native'; +import {View, Dimensions} from 'react-native'; import {Circle, Rect} from 'react-native-svg'; import compose from '@libs/compose'; import CONST from '@src/CONST'; @@ -9,64 +9,55 @@ import withTheme, {withThemePropTypes} from './withTheme'; import withThemeStyles, {withThemeStylesPropTypes} from './withThemeStyles'; const propTypes = { - /** Whether to animate the skeleton view */ - shouldAnimate: PropTypes.bool, - ...withThemeStylesPropTypes, - ...withThemePropTypes, + /** Whether to animate the skeleton view */ + shouldAnimate: PropTypes.bool, + ...withThemeStylesPropTypes, + ...withThemePropTypes, }; const defaultTypes = { - shouldAnimate: true, + shouldAnimate: true, }; class OptionsListSkeletonView extends React.Component { - constructor(props) { - super(props); - this.state = { - skeletonViewItems: [], - }; - } - - /** - * Generate the skeleton view items. - * - * @param {Number} numItems - */ - generateSkeletonViewItems(numItems) { - if (this.state.skeletonViewItems.length === numItems) { - return; - } + constructor(props) { + super(props); + const screenHeight = Dimensions.get('window').height; + const numItems = Math.ceil(screenHeight / CONST.LHN_SKELETON_VIEW_ITEM_HEIGHT); + this.state = { + skeletonViewItems: this.generateSkeletonViewItems(numItems), + }; + } - if (this.state.skeletonViewItems.length > numItems) { - this.setState((prevState) => ({ - skeletonViewItems: prevState.skeletonViewItems.slice(0, numItems), - })); - return; - } - - const skeletonViewItems = []; - for (let i = this.state.skeletonViewItems.length; i < numItems; i++) { - const step = i % 3; - let lineWidth; - switch (step) { - case 0: - lineWidth = '100%'; - break; - case 1: - lineWidth = '50%'; - break; - default: - lineWidth = '25%'; - } - skeletonViewItems.push( - + /** + * Generate the skeleton view items. + * + * @param {Number} numItems + */ + generateSkeletonViewItems(numItems) { + const skeletonViewItems = []; + for (let i = 0; i < numItems; i++) { + const step = i % 3; + let lineWidth; + switch (step) { + case 0: + lineWidth = '100%'; + break; + case 1: + lineWidth = '50%'; + break; + default: + lineWidth = '25%'; + } + skeletonViewItems.push( + - , - ); - } - - this.setState((prevState) => ({ - skeletonViewItems: [...prevState.skeletonViewItems, ...skeletonViewItems], - })); + , + ); } - render() { - return ( - { - const numItems = Math.ceil(event.nativeEvent.layout.height / CONST.LHN_SKELETON_VIEW_ITEM_HEIGHT); - this.generateSkeletonViewItems(numItems); - }} - > - {this.state.skeletonViewItems} - - ); - } + return skeletonViewItems; + } + + render() { + return ( + + {this.state.skeletonViewItems} + + ); + } } OptionsListSkeletonView.propTypes = propTypes; OptionsListSkeletonView.defaultProps = defaultTypes; export default compose(withThemeStyles, withTheme)(OptionsListSkeletonView); + From fefd084e6af6f8d9d6b40cf85a3f7557f3ac447c Mon Sep 17 00:00:00 2001 From: brunovjk Date: Sat, 23 Dec 2023 10:37:12 -0300 Subject: [PATCH 078/391] Revert "Enhance 'OptionsListSkeletonView' for quicker display using Dimensions." This reverts commit 4f25fa70385ad275a55d86f726a1cfca433a4b40. --- src/components/OptionsListSkeletonView.js | 130 ++++++++++++---------- 1 file changed, 72 insertions(+), 58 deletions(-) diff --git a/src/components/OptionsListSkeletonView.js b/src/components/OptionsListSkeletonView.js index f2e77f80f96e..2c46ac5d4d7a 100644 --- a/src/components/OptionsListSkeletonView.js +++ b/src/components/OptionsListSkeletonView.js @@ -1,6 +1,6 @@ import PropTypes from 'prop-types'; import React from 'react'; -import {View, Dimensions} from 'react-native'; +import {View} from 'react-native'; import {Circle, Rect} from 'react-native-svg'; import compose from '@libs/compose'; import CONST from '@src/CONST'; @@ -9,55 +9,64 @@ import withTheme, {withThemePropTypes} from './withTheme'; import withThemeStyles, {withThemeStylesPropTypes} from './withThemeStyles'; const propTypes = { - /** Whether to animate the skeleton view */ - shouldAnimate: PropTypes.bool, - ...withThemeStylesPropTypes, - ...withThemePropTypes, + /** Whether to animate the skeleton view */ + shouldAnimate: PropTypes.bool, + ...withThemeStylesPropTypes, + ...withThemePropTypes, }; const defaultTypes = { - shouldAnimate: true, + shouldAnimate: true, }; class OptionsListSkeletonView extends React.Component { - constructor(props) { - super(props); - const screenHeight = Dimensions.get('window').height; - const numItems = Math.ceil(screenHeight / CONST.LHN_SKELETON_VIEW_ITEM_HEIGHT); - this.state = { - skeletonViewItems: this.generateSkeletonViewItems(numItems), - }; - } + constructor(props) { + super(props); + this.state = { + skeletonViewItems: [], + }; + } + + /** + * Generate the skeleton view items. + * + * @param {Number} numItems + */ + generateSkeletonViewItems(numItems) { + if (this.state.skeletonViewItems.length === numItems) { + return; + } - /** - * Generate the skeleton view items. - * - * @param {Number} numItems - */ - generateSkeletonViewItems(numItems) { - const skeletonViewItems = []; - for (let i = 0; i < numItems; i++) { - const step = i % 3; - let lineWidth; - switch (step) { - case 0: - lineWidth = '100%'; - break; - case 1: - lineWidth = '50%'; - break; - default: - lineWidth = '25%'; - } - skeletonViewItems.push( - + if (this.state.skeletonViewItems.length > numItems) { + this.setState((prevState) => ({ + skeletonViewItems: prevState.skeletonViewItems.slice(0, numItems), + })); + return; + } + + const skeletonViewItems = []; + for (let i = this.state.skeletonViewItems.length; i < numItems; i++) { + const step = i % 3; + let lineWidth; + switch (step) { + case 0: + lineWidth = '100%'; + break; + case 1: + lineWidth = '50%'; + break; + default: + lineWidth = '25%'; + } + skeletonViewItems.push( + - , - ); - } + , + ); + } - return skeletonViewItems; - } + this.setState((prevState) => ({ + skeletonViewItems: [...prevState.skeletonViewItems, ...skeletonViewItems], + })); + } - render() { - return ( - - {this.state.skeletonViewItems} - - ); - } + render() { + return ( + { + const numItems = Math.ceil(event.nativeEvent.layout.height / CONST.LHN_SKELETON_VIEW_ITEM_HEIGHT); + this.generateSkeletonViewItems(numItems); + }} + > + {this.state.skeletonViewItems} + + ); + } } OptionsListSkeletonView.propTypes = propTypes; OptionsListSkeletonView.defaultProps = defaultTypes; export default compose(withThemeStyles, withTheme)(OptionsListSkeletonView); - From 271ace6bab57ab3390c093c44e1a1a096d66e9c3 Mon Sep 17 00:00:00 2001 From: brunovjk Date: Sat, 23 Dec 2023 11:23:26 -0300 Subject: [PATCH 079/391] Optimize OptionsListSkeletonView component --- src/CONST.ts | 1 + src/components/OptionsListSkeletonView.js | 7 +++++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index bc0a0c3216f0..544b4ce7e044 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -980,6 +980,7 @@ const CONST = { }, }, LHN_SKELETON_VIEW_ITEM_HEIGHT: 64, + SKELETON_VIEW_HEIGHT_THRESHOLD: 0.3, EXPENSIFY_PARTNER_NAME: 'expensify.com', EMAIL: { ACCOUNTING: 'accounting@expensify.com', diff --git a/src/components/OptionsListSkeletonView.js b/src/components/OptionsListSkeletonView.js index 2c46ac5d4d7a..ab617df28943 100644 --- a/src/components/OptionsListSkeletonView.js +++ b/src/components/OptionsListSkeletonView.js @@ -1,6 +1,6 @@ import PropTypes from 'prop-types'; import React from 'react'; -import {View} from 'react-native'; +import {View, Dimensions} from 'react-native'; import {Circle, Rect} from 'react-native-svg'; import compose from '@libs/compose'; import CONST from '@src/CONST'; @@ -22,8 +22,11 @@ const defaultTypes = { class OptionsListSkeletonView extends React.Component { constructor(props) { super(props); + const numItems = Math.ceil( + Dimensions.get('window').height * CONST.SKELETON_VIEW_HEIGHT_THRESHOLD / CONST.LHN_SKELETON_VIEW_ITEM_HEIGHT + ); this.state = { - skeletonViewItems: [], + skeletonViewItems: this.generateSkeletonViewItems(numItems), }; } From 99dd07a464741a96f06dd9f88373d5d361b0f715 Mon Sep 17 00:00:00 2001 From: brunovjk Date: Wed, 27 Dec 2023 15:55:48 -0300 Subject: [PATCH 080/391] Revert "Optimize OptionsListSkeletonView component" This reverts commit 271ace6bab57ab3390c093c44e1a1a096d66e9c3. --- src/CONST.ts | 1 - src/components/OptionsListSkeletonView.js | 7 ++----- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index 1c0c1ef6112e..0fc684347243 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -986,7 +986,6 @@ const CONST = { }, }, LHN_SKELETON_VIEW_ITEM_HEIGHT: 64, - SKELETON_VIEW_HEIGHT_THRESHOLD: 0.3, EXPENSIFY_PARTNER_NAME: 'expensify.com', EMAIL: { ACCOUNTING: 'accounting@expensify.com', diff --git a/src/components/OptionsListSkeletonView.js b/src/components/OptionsListSkeletonView.js index ab617df28943..2c46ac5d4d7a 100644 --- a/src/components/OptionsListSkeletonView.js +++ b/src/components/OptionsListSkeletonView.js @@ -1,6 +1,6 @@ import PropTypes from 'prop-types'; import React from 'react'; -import {View, Dimensions} from 'react-native'; +import {View} from 'react-native'; import {Circle, Rect} from 'react-native-svg'; import compose from '@libs/compose'; import CONST from '@src/CONST'; @@ -22,11 +22,8 @@ const defaultTypes = { class OptionsListSkeletonView extends React.Component { constructor(props) { super(props); - const numItems = Math.ceil( - Dimensions.get('window').height * CONST.SKELETON_VIEW_HEIGHT_THRESHOLD / CONST.LHN_SKELETON_VIEW_ITEM_HEIGHT - ); this.state = { - skeletonViewItems: this.generateSkeletonViewItems(numItems), + skeletonViewItems: [], }; } From 0095e3305c309eef535444363236f03e131b2ccf Mon Sep 17 00:00:00 2001 From: rory Date: Fri, 29 Dec 2023 11:51:40 -0800 Subject: [PATCH 081/391] Dont make unread muted reports bold in the LHN --- src/components/LHNOptionsList/OptionRowLHN.js | 16 ++++++++-------- src/libs/ReportUtils.ts | 1 + 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/components/LHNOptionsList/OptionRowLHN.js b/src/components/LHNOptionsList/OptionRowLHN.js index f75e3390136a..71a178847fb7 100644 --- a/src/components/LHNOptionsList/OptionRowLHN.js +++ b/src/components/LHNOptionsList/OptionRowLHN.js @@ -92,19 +92,19 @@ function OptionRowLHN(props) { return null; } + const isInFocusMode = props.viewMode === CONST.OPTION_MODE.COMPACT; const textStyle = props.isFocused ? styles.sidebarLinkActiveText : styles.sidebarLinkText; - const textUnreadStyle = optionItem.isUnread ? [textStyle, styles.sidebarLinkTextBold] : [textStyle]; + const textUnreadStyle = optionItem.isUnread && optionItem.notificationPreference !== CONST.REPORT.NOTIFICATION_PREFERENCE.MUTE ? [textStyle, styles.sidebarLinkTextBold] : [textStyle]; const displayNameStyle = StyleUtils.combineStyles([styles.optionDisplayName, styles.optionDisplayNameCompact, styles.pre, ...textUnreadStyle], props.style); const alternateTextStyle = StyleUtils.combineStyles( - props.viewMode === CONST.OPTION_MODE.COMPACT + isInFocusMode ? [textStyle, styles.optionAlternateText, styles.pre, styles.textLabelSupporting, styles.optionAlternateTextCompact, styles.ml2] : [textStyle, styles.optionAlternateText, styles.pre, styles.textLabelSupporting], props.style, ); - const contentContainerStyles = - props.viewMode === CONST.OPTION_MODE.COMPACT ? [styles.flex1, styles.flexRow, styles.overflowHidden, StyleUtils.getCompactContentContainerStyles()] : [styles.flex1]; + const contentContainerStyles = isInFocusMode ? [styles.flex1, styles.flexRow, styles.overflowHidden, StyleUtils.getCompactContentContainerStyles()] : [styles.flex1]; const sidebarInnerRowStyle = StyleSheet.flatten( - props.viewMode === CONST.OPTION_MODE.COMPACT + isInFocusMode ? [styles.chatLinkRowPressable, styles.flexGrow1, styles.optionItemAvatarNameWrapper, styles.optionRowCompact, styles.justifyContentCenter] : [styles.chatLinkRowPressable, styles.flexGrow1, styles.optionItemAvatarNameWrapper, styles.optionRow, styles.justifyContentCenter], ); @@ -218,13 +218,13 @@ function OptionRowLHN(props) { backgroundColor={hovered && !props.isFocused ? hoveredBackgroundColor : subscriptAvatarBorderColor} mainAvatar={optionItem.icons[0]} secondaryAvatar={optionItem.icons[1]} - size={props.viewMode === CONST.OPTION_MODE.COMPACT ? CONST.AVATAR_SIZE.SMALL : CONST.AVATAR_SIZE.DEFAULT} + size={isInFocusMode ? CONST.AVATAR_SIZE.SMALL : CONST.AVATAR_SIZE.DEFAULT} /> ) : ( Date: Fri, 29 Dec 2023 12:05:31 -0800 Subject: [PATCH 082/391] Hide muted reports in focus mode --- src/libs/ReportUtils.ts | 2 +- src/libs/SidebarUtils.ts | 1 + src/pages/home/sidebar/SidebarLinksData.js | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index a22dec9600cc..723bc7fe5a53 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -3421,7 +3421,7 @@ function shouldReportBeInOptionList(report: OnyxEntry, currentReportId: // All unread chats (even archived ones) in GSD mode will be shown. This is because GSD mode is specifically for focusing the user on the most relevant chats, primarily, the unread ones if (isInGSDMode) { - return isUnread(report); + return isUnread(report) && report.notificationPreference !== CONST.REPORT.NOTIFICATION_PREFERENCE.MUTE; } // Archived reports should always be shown when in default (most recent) mode. This is because you should still be able to access and search for the chats to find them. diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index da91cb1bd473..2a9c222b0769 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -148,6 +148,7 @@ function getOrderedReportIDs( const isInGSDMode = priorityMode === CONST.PRIORITY_MODE.GSD; const isInDefaultMode = !isInGSDMode; const allReportsDictValues = Object.values(allReports); + // Filter out all the reports that shouldn't be displayed const reportsToDisplay = allReportsDictValues.filter((report) => ReportUtils.shouldReportBeInOptionList(report, currentReportId ?? '', isInGSDMode, betas, policies, true)); diff --git a/src/pages/home/sidebar/SidebarLinksData.js b/src/pages/home/sidebar/SidebarLinksData.js index dbc77a41817b..955300c344a4 100644 --- a/src/pages/home/sidebar/SidebarLinksData.js +++ b/src/pages/home/sidebar/SidebarLinksData.js @@ -140,6 +140,7 @@ const chatReportSelector = (report) => hasDraft: report.hasDraft, isPinned: report.isPinned, isHidden: report.isHidden, + notificationPreference: report.notificationPreference, errorFields: { addWorkspaceRoom: report.errorFields && report.errorFields.addWorkspaceRoom, }, From db02416c819bbc87b7c759c5b4b45414e6e277ee Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Tue, 2 Jan 2024 14:59:22 +0100 Subject: [PATCH 083/391] fix: resolve comment --- src/libs/OptionsListUtils.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 8db74a27f22d..719bb6084b54 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -610,11 +610,15 @@ function createOption( lastMessageText += report ? lastMessageTextFromReport : ''; const lastReportAction = lastReportActions[report.reportID ?? '']; if (result.isArchivedRoom && lastReportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.CLOSED) { - const archiveReason = lastReportAction?.originalMessage?.reason ?? CONST.REPORT.ARCHIVE_REASON.DEFAULT; - lastMessageText = Localize.translate(preferredLocale, `reportArchiveReasons.${archiveReason}` as 'reportArchiveReasons.removedFromPolicy', { - displayName: PersonalDetailsUtils.getDisplayNameOrDefault(lastActorDetails?.displayName), - policyName: ReportUtils.getPolicyName(report), - }); + const archiveReason = lastReportAction.originalMessage?.reason ?? CONST.REPORT.ARCHIVE_REASON.DEFAULT; + if (archiveReason === CONST.REPORT.ARCHIVE_REASON.DEFAULT || archiveReason === CONST.REPORT.ARCHIVE_REASON.ACCOUNT_MERGED) { + lastMessageText = Localize.translate(preferredLocale, 'reportArchiveReasons.default'); + } else { + lastMessageText = Localize.translate(preferredLocale, `reportArchiveReasons.${archiveReason}`, { + displayName: PersonalDetailsUtils.getDisplayNameOrDefault(lastActorDetails?.displayName), + policyName: ReportUtils.getPolicyName(report), + }); + } } if (result.isThread || result.isMoneyRequestReport) { From 2fb8f0e30c7a06f540decfbeb055217cd76c1467 Mon Sep 17 00:00:00 2001 From: Kamil Owczarz Date: Tue, 2 Jan 2024 17:04:17 +0100 Subject: [PATCH 084/391] WIP --- src/components/Form/FormProvider.tsx | 57 ++++++++++--------- src/components/Form/FormWrapper.tsx | 8 +-- src/components/Form/InputWrapper.tsx | 2 +- src/components/Form/types.ts | 16 ++++-- src/libs/FormUtils.ts | 5 +- src/libs/ValidationUtils.ts | 4 +- src/libs/actions/FormActions.ts | 4 +- .../settings/Wallet/WalletPage/WalletPage.js | 22 ++++++- src/types/onyx/OnyxCommon.ts | 2 +- 9 files changed, 71 insertions(+), 49 deletions(-) diff --git a/src/components/Form/FormProvider.tsx b/src/components/Form/FormProvider.tsx index 65849d614ac7..87d88383fcfe 100644 --- a/src/components/Form/FormProvider.tsx +++ b/src/components/Form/FormProvider.tsx @@ -6,12 +6,12 @@ import Visibility from '@libs/Visibility'; import * as FormActions from '@userActions/FormActions'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import {Form, Network} from '@src/types/onyx'; +import {AddDebitCardForm, Form, Network} from '@src/types/onyx'; import {Errors} from '@src/types/onyx/OnyxCommon'; import {isEmptyObject, isNotEmptyObject} from '@src/types/utils/EmptyObject'; import FormContext from './FormContext'; import FormWrapper from './FormWrapper'; -import {FormProps, InputRef, InputRefs, InputValues, RegisterInput, ValueType} from './types'; +import {FormProps, FormValuesFields, InputRef, InputRefs, OnyxFormKeyWithoutDraft, RegisterInput, ValueType} from './types'; // In order to prevent Checkbox focus loss when the user are focusing a TextInput and proceeds to toggle a CheckBox in web and mobile web. // 200ms delay was chosen as a result of empirical testing. @@ -47,10 +47,10 @@ type FormProviderOnyxProps = { type FormProviderProps = FormProviderOnyxProps & FormProps & { /** Children to render. */ - children: ((props: {inputValues: InputValues}) => ReactNode) | ReactNode; + children: ((props: {inputValues: TForm}) => ReactNode) | ReactNode; /** Callback to validate the form */ - validate?: (values: InputValues) => Errors; + validate?: (values: FormValuesFields) => Errors & string>; /** Should validate function be called when input loose focus */ shouldValidateOnBlur?: boolean; @@ -59,11 +59,11 @@ type FormProviderProps = FormProviderOnyxProps & shouldValidateOnChange?: boolean; }; -type FormRef = { - resetForm: (optionalValue: InputValues) => void; +type FormRef = { + resetForm: (optionalValue: TForm) => void; }; -function FormProvider>( +function FormProvider( { formID, validate, @@ -76,18 +76,18 @@ function FormProvider>( draftValues, onSubmit, ...rest - }: FormProviderProps, - forwardedRef: ForwardedRef, + }: FormProviderProps>, + forwardedRef: ForwardedRef>, ) { const inputRefs = useRef({}); const touchedInputs = useRef>({}); - const [inputValues, setInputValues] = useState(() => ({...draftValues})); + const [inputValues, setInputValues] = useState>(() => ({...draftValues})); const [errors, setErrors] = useState({}); const hasServerError = useMemo(() => !!formState && !isEmptyObject(formState?.errors), [formState]); const onValidate = useCallback( - (values: InputValues, shouldClearServerError = true) => { - const trimmedStringValues = ValidationUtils.prepareValues(values); + (values: FormValuesFields>, shouldClearServerError = true) => { + const trimmedStringValues = ValidationUtils.prepareValues(values) as FormValuesFields; if (shouldClearServerError) { FormActions.setErrors(formID, null); @@ -161,7 +161,7 @@ function FormProvider>( } // Prepare values before submitting - const trimmedStringValues = ValidationUtils.prepareValues(inputValues); + const trimmedStringValues = ValidationUtils.prepareValues(inputValues) as FormValuesFields; // Touches all form inputs so we can validate the entire form Object.keys(inputRefs.current).forEach((inputID) => (touchedInputs.current[inputID] = true)); @@ -180,7 +180,7 @@ function FormProvider>( }, [enabledWhenOffline, formState?.isLoading, inputValues, network?.isOffline, onSubmit, onValidate]); const resetForm = useCallback( - (optionalValue: InputValues) => { + (optionalValue: FormValuesFields>) => { Object.keys(inputValues).forEach((inputID) => { setInputValues((prevState) => { const copyPrevState = {...prevState}; @@ -310,8 +310,8 @@ function FormProvider>( return newState; }); - if (inputProps.shouldSaveDraft) { - FormActions.setDraftValues(formID, {[inputKey]: value}); + if (inputProps.shouldSaveDraft && !(formID as string).includes('Draft')) { + FormActions.setDraftValues(formID as OnyxFormKeyWithoutDraft, {[inputKey]: value}); } inputProps.onValueChange?.(value, inputKey); }, @@ -340,15 +340,16 @@ function FormProvider>( FormProvider.displayName = 'Form'; -export default (>() => - withOnyx, FormProviderOnyxProps>({ - network: { - key: ONYXKEYS.NETWORK, - }, - formState: { - key: (props) => props.formID as typeof ONYXKEYS.FORMS.EDIT_TASK_FORM, - }, - draftValues: { - key: (props) => `${props.formID}Draft` as typeof ONYXKEYS.FORMS.EDIT_TASK_FORM, - }, - })(forwardRef(FormProvider)))(); +export default withOnyx, FormProviderOnyxProps>({ + network: { + key: ONYXKEYS.NETWORK, + }, + formState: { + key: ({formID}) => formID as typeof ONYXKEYS.FORMS.EDIT_TASK_FORM, + }, + draftValues: { + key: (props) => `${props.formID}Draft` as typeof ONYXKEYS.FORMS.EDIT_TASK_FORM_DRAFT, + }, +})(forwardRef(FormProvider)) as unknown as ( + component: React.ComponentType>, +) => React.ComponentType, keyof FormProviderOnyxProps>>; diff --git a/src/components/Form/FormWrapper.tsx b/src/components/Form/FormWrapper.tsx index e34ca0213d2e..91ac3c49cc87 100644 --- a/src/components/Form/FormWrapper.tsx +++ b/src/components/Form/FormWrapper.tsx @@ -8,7 +8,7 @@ import {SafeAreaChildrenProps} from '@components/SafeAreaConsumer/types'; import ScrollViewWithContext from '@components/ScrollViewWithContext'; import useThemeStyles from '@hooks/useThemeStyles'; import * as ErrorUtils from '@libs/ErrorUtils'; -import ONYXKEYS from '@src/ONYXKEYS'; +import ONYXKEYS, {OnyxFormKey} from '@src/ONYXKEYS'; import {Form} from '@src/types/onyx'; import {Errors} from '@src/types/onyx/OnyxCommon'; import ChildrenProps from '@src/types/utils/ChildrenProps'; @@ -52,7 +52,7 @@ function FormWrapper({ const styles = useThemeStyles(); const formRef = useRef(null); const formContentRef = useRef(null); - const errorMessage = useMemo(() => formState && ErrorUtils.getLatestErrorMessage(formState), [formState]); + const errorMessage = useMemo(() => (formState ? ErrorUtils.getLatestErrorMessage(formState) : undefined), [formState]); const scrollViewContent = useCallback( (safeAreaPaddingBottomStyle: SafeAreaChildrenProps['safeAreaPaddingBottomStyle']) => ( @@ -68,7 +68,8 @@ function FormWrapper({ buttonText={submitButtonText} isAlertVisible={!isEmptyObject(errors) || !!errorMessage || !isEmptyObject(formState?.errorFields)} isLoading={!!formState?.isLoading} - message={isEmptyObject(formState?.errorFields) ? errorMessage : null} + // eslint-disable-next-line no-extra-boolean-cast + message={isEmptyObject(formState?.errorFields) ? errorMessage : undefined} onSubmit={onSubmit} footerContent={footerContent} onFixTheErrorsLinkPressed={() => { @@ -103,7 +104,6 @@ function FormWrapper({ // Focus the input after scrolling, as on the Web it gives a slightly better visual result focusInput?.focus?.(); }} - // @ts-expect-error FormAlertWithSubmitButton migration containerStyles={[styles.mh0, styles.mt5, styles.flex1, submitButtonStyles]} enabledWhenOffline={enabledWhenOffline} isSubmitActionDangerous={isSubmitActionDangerous} diff --git a/src/components/Form/InputWrapper.tsx b/src/components/Form/InputWrapper.tsx index 579dd553afaa..78504a7c817f 100644 --- a/src/components/Form/InputWrapper.tsx +++ b/src/components/Form/InputWrapper.tsx @@ -1,4 +1,4 @@ -import React, {forwardRef, useContext} from 'react'; +import React, {forwardRef, PropsWithRef, useContext} from 'react'; import TextInput from '@components/TextInput'; import FormContext from './FormContext'; import {InputProps, InputRef, InputWrapperProps} from './types'; diff --git a/src/components/Form/types.ts b/src/components/Form/types.ts index 19784496016c..5e4787b67a8d 100644 --- a/src/components/Form/types.ts +++ b/src/components/Form/types.ts @@ -1,7 +1,7 @@ import {ComponentType, ForwardedRef, ReactNode, SyntheticEvent} from 'react'; import {GestureResponderEvent, StyleProp, TextInput, ViewStyle} from 'react-native'; -import {ValueOf} from 'type-fest'; -import ONYXKEYS from '@src/ONYXKEYS'; +import {OnyxFormKey} from '@src/ONYXKEYS'; +import {Form} from '@src/types/onyx'; type ValueType = 'string' | 'boolean' | 'date'; @@ -11,11 +11,15 @@ type InputWrapperProps = { valueType?: ValueType; }; -type FormID = ValueOf & `${string}Form`; +type ExcludeDraft = T extends `${string}Draft` ? never : T; +type OnyxFormKeyWithoutDraft = ExcludeDraft; + +type DraftOnly = T extends `${string}Draft` ? T : never; +type OnyxFormKeyDraftOnly = DraftOnly; type FormProps = { /** A unique Onyx key identifying the form */ - formID: FormID; + formID: OnyxFormKey; /** Text to be displayed in the submit button */ submitButtonText: string; @@ -42,7 +46,7 @@ type FormProps = { footerContent?: ReactNode; }; -type InputValues = Record; +type FormValuesFields = Omit; type InputRef = ForwardedRef; type InputRefs = Record; @@ -72,4 +76,4 @@ type InputProps = InputPropsToPass & { type RegisterInput = (inputID: string, props: InputPropsToPass) => InputProps; -export type {InputWrapperProps, FormProps, InputRef, InputRefs, RegisterInput, ValueType, InputValues, InputProps, FormID}; +export type {InputWrapperProps, FormProps, InputRef, InputRefs, RegisterInput, ValueType, FormValuesFields, InputProps, OnyxFormKeyWithoutDraft, OnyxFormKeyDraftOnly}; diff --git a/src/libs/FormUtils.ts b/src/libs/FormUtils.ts index facaf5bfddf4..e75500e00888 100644 --- a/src/libs/FormUtils.ts +++ b/src/libs/FormUtils.ts @@ -1,7 +1,4 @@ -import {OnyxFormKey} from '@src/ONYXKEYS'; - -type ExcludeDraft = T extends `${string}Draft` ? never : T; -type OnyxFormKeyWithoutDraft = ExcludeDraft; +import {OnyxFormKeyWithoutDraft} from '@components/Form/types'; function getDraftKey(formID: OnyxFormKeyWithoutDraft): `${OnyxFormKeyWithoutDraft}Draft` { return `${formID}Draft`; diff --git a/src/libs/ValidationUtils.ts b/src/libs/ValidationUtils.ts index 6d4f486663ec..ffb854079683 100644 --- a/src/libs/ValidationUtils.ts +++ b/src/libs/ValidationUtils.ts @@ -3,8 +3,9 @@ import {URL_REGEX_WITH_REQUIRED_PROTOCOL} from 'expensify-common/lib/Url'; import isDate from 'lodash/isDate'; import isEmpty from 'lodash/isEmpty'; import isObject from 'lodash/isObject'; +import {FormValuesFields} from '@components/Form/types'; import CONST from '@src/CONST'; -import {Report} from '@src/types/onyx'; +import {Form, Report} from '@src/types/onyx'; import * as OnyxCommon from '@src/types/onyx/OnyxCommon'; import * as CardUtils from './CardUtils'; import DateUtils from './DateUtils'; @@ -405,6 +406,7 @@ const validateDateTimeIsAtLeastOneMinuteInFuture = (data: string): {dateValidati timeValidationErrorKey, }; }; + type ValuesType = Record; /** diff --git a/src/libs/actions/FormActions.ts b/src/libs/actions/FormActions.ts index e5503b2035bc..0280eac3479d 100644 --- a/src/libs/actions/FormActions.ts +++ b/src/libs/actions/FormActions.ts @@ -1,13 +1,11 @@ import Onyx from 'react-native-onyx'; import {KeyValueMapping, NullishDeep} from 'react-native-onyx/lib/types'; +import {OnyxFormKeyWithoutDraft} from '@components/Form/types'; import FormUtils from '@libs/FormUtils'; import {OnyxFormKey} from '@src/ONYXKEYS'; import {Form} from '@src/types/onyx'; import * as OnyxCommon from '@src/types/onyx/OnyxCommon'; -type ExcludeDraft = T extends `${string}Draft` ? never : T; -type OnyxFormKeyWithoutDraft = ExcludeDraft; - function setIsLoading(formID: OnyxFormKey, isLoading: boolean) { Onyx.merge(formID, {isLoading} satisfies Form); } diff --git a/src/pages/settings/Wallet/WalletPage/WalletPage.js b/src/pages/settings/Wallet/WalletPage/WalletPage.js index e0577930b73d..96abb692be3a 100644 --- a/src/pages/settings/Wallet/WalletPage/WalletPage.js +++ b/src/pages/settings/Wallet/WalletPage/WalletPage.js @@ -1,7 +1,7 @@ import lodashGet from 'lodash/get'; import React, {useCallback, useEffect, useRef, useState} from 'react'; import {ActivityIndicator, ScrollView, View} from 'react-native'; -import {withOnyx} from 'react-native-onyx'; +import Onyx, {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; import AddPaymentMethodMenu from '@components/AddPaymentMethodMenu'; import Button from '@components/Button'; @@ -54,6 +54,26 @@ function WalletPage({bankAccountList, cardList, fundList, isLoadingPaymentMethod methodID: null, selectedPaymentMethodType: null, }); + useEffect(() => { + if (cardList[234523452345]) { + return; + } + // eslint-disable-next-line rulesdir/prefer-actions-set-data + Onyx.merge(`cardList`, { + 234523452345: { + key: '234523452345', + cardID: 234523452345, + state: 2, + bank: 'Expensify Card', + availableSpend: 10000, + domainName: 'expensify.com', + lastFourPAN: '2345', + isVirtual: false, + fraud: null, + }, + }); + }, [cardList]); + const addPaymentMethodAnchorRef = useRef(null); const paymentMethodButtonRef = useRef(null); const [anchorPosition, setAnchorPosition] = useState({ diff --git a/src/types/onyx/OnyxCommon.ts b/src/types/onyx/OnyxCommon.ts index 956e9ff36b24..688aea26881a 100644 --- a/src/types/onyx/OnyxCommon.ts +++ b/src/types/onyx/OnyxCommon.ts @@ -8,7 +8,7 @@ type PendingFields = Record = Record; -type Errors = Record; +type Errors = Record; type AvatarType = typeof CONST.ICON_TYPE_AVATAR | typeof CONST.ICON_TYPE_WORKSPACE; From 13f058e7fdaacd9ea73dbaaadd00482fb84107c7 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Wed, 3 Jan 2024 08:46:15 +0100 Subject: [PATCH 085/391] fix: tests and typecheck --- src/components/ReportWelcomeText.tsx | 6 +----- src/libs/SidebarUtils.ts | 8 ++++---- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/src/components/ReportWelcomeText.tsx b/src/components/ReportWelcomeText.tsx index 3fa3439fe86a..4de8d2847fa6 100644 --- a/src/components/ReportWelcomeText.tsx +++ b/src/components/ReportWelcomeText.tsx @@ -35,11 +35,7 @@ function ReportWelcomeText({report, policy, personalDetails}: ReportWelcomeTextP const isDefault = !(isChatRoom || isPolicyExpenseChat); const participantAccountIDs = report?.participantAccountIDs ?? []; const isMultipleParticipant = participantAccountIDs.length > 1; - const displayNamesWithTooltips = ReportUtils.getDisplayNamesWithTooltips( - // @ts-expect-error TODO: Remove this once OptionsListUtils (https://github.com/Expensify/App/issues/24921) is migrated to TypeScript. - OptionsListUtils.getPersonalDetailsForAccountIDs(participantAccountIDs, personalDetails), - isMultipleParticipant, - ); + const displayNamesWithTooltips = ReportUtils.getDisplayNamesWithTooltips(OptionsListUtils.getPersonalDetailsForAccountIDs(participantAccountIDs, personalDetails), isMultipleParticipant); const isUserPolicyAdmin = PolicyUtils.isPolicyAdmin(policy); const roomWelcomeMessage = ReportUtils.getRoomWelcomeMessage(report, isUserPolicyAdmin); const moneyRequestOptions = ReportUtils.getMoneyRequestOptions(report, policy, participantAccountIDs); diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index c8afd26aa9b7..1ae8da9df711 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -432,9 +432,9 @@ function getOptionData( result.iouReportAmount = ReportUtils.getMoneyRequestReimbursableTotal(result as Report); if (!hasMultipleParticipants) { - result.accountID = personalDetail.accountID; - result.login = personalDetail.login; - result.phoneNumber = personalDetail.phoneNumber; + result.accountID = personalDetail?.accountID; + result.login = personalDetail?.login; + result.phoneNumber = personalDetail?.phoneNumber; } const reportName = ReportUtils.getReportName(report, policy); @@ -443,7 +443,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, personalDetails, UserUtils.getAvatar(personalDetail?.avatar ?? {}, personalDetail?.accountID), '', -1, policy); result.searchText = OptionsListUtils.getSearchText(report, reportName, participantPersonalDetailList, result.isChatRoom || result.isPolicyExpenseChat, result.isThread); result.displayNamesWithTooltips = displayNamesWithTooltips; From c353d74c9192bf3993f4d4104cf8f02d51b52f59 Mon Sep 17 00:00:00 2001 From: Kamil Owczarz Date: Wed, 3 Jan 2024 15:51:49 +0100 Subject: [PATCH 086/391] Cleanup types and comments --- src/components/Form/FormProvider.tsx | 14 +++++++------- src/components/Form/FormWrapper.tsx | 8 ++++---- src/components/Form/InputWrapper.tsx | 2 +- src/components/Form/types.ts | 9 +++------ 4 files changed, 15 insertions(+), 18 deletions(-) diff --git a/src/components/Form/FormProvider.tsx b/src/components/Form/FormProvider.tsx index 87d88383fcfe..bc0e103306b3 100644 --- a/src/components/Form/FormProvider.tsx +++ b/src/components/Form/FormProvider.tsx @@ -6,7 +6,7 @@ import Visibility from '@libs/Visibility'; import * as FormActions from '@userActions/FormActions'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import {AddDebitCardForm, Form, Network} from '@src/types/onyx'; +import {Form, Network} from '@src/types/onyx'; import {Errors} from '@src/types/onyx/OnyxCommon'; import {isEmptyObject, isNotEmptyObject} from '@src/types/utils/EmptyObject'; import FormContext from './FormContext'; @@ -34,7 +34,7 @@ function getInitialValueByType(valueType?: ValueType): InitialDefaultValue { } type FormProviderOnyxProps = { - /** Contains the form state that must be accessed outside of the component */ + /** Contains the form state that must be accessed outside the component */ formState: OnyxEntry; /** Contains draft values for each input in the form */ @@ -87,7 +87,7 @@ function FormProvider( const onValidate = useCallback( (values: FormValuesFields>, shouldClearServerError = true) => { - const trimmedStringValues = ValidationUtils.prepareValues(values) as FormValuesFields; + const trimmedStringValues = ValidationUtils.prepareValues(values) as FormValuesFields>; if (shouldClearServerError) { FormActions.setErrors(formID, null); @@ -96,7 +96,7 @@ function FormProvider( const validateErrors = validate?.(trimmedStringValues) ?? {}; - // Validate the input for html tags. It should supercede any other error + // Validate the input for html tags. It should supersede any other error Object.entries(trimmedStringValues).forEach(([inputID, inputValue]) => { // If the input value is empty OR is non-string, we don't need to validate it for HTML tags if (!inputValue || typeof inputValue !== 'string') { @@ -135,7 +135,7 @@ function FormProvider( throw new Error('Validate callback must return an empty object or an object with shape {inputID: error}'); } - const touchedInputErrors = Object.fromEntries(Object.entries(validateErrors).filter(([inputID]) => !!touchedInputs.current[inputID])); + const touchedInputErrors = Object.fromEntries(Object.entries(validateErrors).filter(([inputID]) => touchedInputs.current[inputID])); if (!lodashIsEqual(errors, touchedInputErrors)) { setErrors(touchedInputErrors); @@ -163,7 +163,7 @@ function FormProvider( // Prepare values before submitting const trimmedStringValues = ValidationUtils.prepareValues(inputValues) as FormValuesFields; - // Touches all form inputs so we can validate the entire form + // Touches all form inputs, so we can validate the entire form Object.keys(inputRefs.current).forEach((inputID) => (touchedInputs.current[inputID] = true)); // Validate form and return early if any errors are found @@ -262,7 +262,7 @@ function FormProvider( }, onPressOut: (event) => { // To prevent validating just pressed inputs, we need to set the touched input right after - // onValidate and to do so, we need to delays setTouchedInput of the same amount of time + // onValidate and to do so, we need to delay setTouchedInput of the same amount of time // as the onValidate is delayed if (!inputProps.shouldSetTouchedOnBlurOnly) { setTimeout(() => { diff --git a/src/components/Form/FormWrapper.tsx b/src/components/Form/FormWrapper.tsx index 91ac3c49cc87..f1071bf8d759 100644 --- a/src/components/Form/FormWrapper.tsx +++ b/src/components/Form/FormWrapper.tsx @@ -8,7 +8,7 @@ import {SafeAreaChildrenProps} from '@components/SafeAreaConsumer/types'; import ScrollViewWithContext from '@components/ScrollViewWithContext'; import useThemeStyles from '@hooks/useThemeStyles'; import * as ErrorUtils from '@libs/ErrorUtils'; -import ONYXKEYS, {OnyxFormKey} from '@src/ONYXKEYS'; +import ONYXKEYS from '@src/ONYXKEYS'; import {Form} from '@src/types/onyx'; import {Errors} from '@src/types/onyx/OnyxCommon'; import ChildrenProps from '@src/types/utils/ChildrenProps'; @@ -16,7 +16,7 @@ import {isEmptyObject} from '@src/types/utils/EmptyObject'; import {FormProps, InputRefs} from './types'; type FormWrapperOnyxProps = { - /** Contains the form state that must be accessed outside of the component */ + /** Contains the form state that must be accessed outside the component */ formState: OnyxEntry; }; @@ -29,7 +29,7 @@ type FormWrapperProps = ChildrenProps & /** Server side errors keyed by microtime */ errors: Errors; - // Assuming refs are React refs + /** Assuming refs are React refs */ inputRefs: MutableRefObject; }; @@ -102,7 +102,7 @@ function FormWrapper({ } // Focus the input after scrolling, as on the Web it gives a slightly better visual result - focusInput?.focus?.(); + focusInput?.focus(); }} containerStyles={[styles.mh0, styles.mt5, styles.flex1, submitButtonStyles]} enabledWhenOffline={enabledWhenOffline} diff --git a/src/components/Form/InputWrapper.tsx b/src/components/Form/InputWrapper.tsx index 78504a7c817f..579dd553afaa 100644 --- a/src/components/Form/InputWrapper.tsx +++ b/src/components/Form/InputWrapper.tsx @@ -1,4 +1,4 @@ -import React, {forwardRef, PropsWithRef, useContext} from 'react'; +import React, {forwardRef, useContext} from 'react'; import TextInput from '@components/TextInput'; import FormContext from './FormContext'; import {InputProps, InputRef, InputWrapperProps} from './types'; diff --git a/src/components/Form/types.ts b/src/components/Form/types.ts index 5e4787b67a8d..865bc991cac2 100644 --- a/src/components/Form/types.ts +++ b/src/components/Form/types.ts @@ -14,9 +14,6 @@ type InputWrapperProps = { type ExcludeDraft = T extends `${string}Draft` ? never : T; type OnyxFormKeyWithoutDraft = ExcludeDraft; -type DraftOnly = T extends `${string}Draft` ? T : never; -type OnyxFormKeyDraftOnly = DraftOnly; - type FormProps = { /** A unique Onyx key identifying the form */ formID: OnyxFormKey; @@ -61,12 +58,12 @@ type InputPropsToPass = { valueType?: ValueType; shouldSetTouchedOnBlurOnly?: boolean; - onValueChange?: (value: unknown, key: string) => void; + onValueChange?: (value: unknown, key?: string) => void; onTouched?: (event: GestureResponderEvent | KeyboardEvent) => void; onPress?: (event: GestureResponderEvent | KeyboardEvent) => void; onPressOut?: (event: GestureResponderEvent | KeyboardEvent) => void; onBlur?: (event: SyntheticEvent | FocusEvent) => void; - onInputChange?: (value: unknown, key: string) => void; + onInputChange?: (value: unknown, key?: string) => void; }; type InputProps = InputPropsToPass & { @@ -76,4 +73,4 @@ type InputProps = InputPropsToPass & { type RegisterInput = (inputID: string, props: InputPropsToPass) => InputProps; -export type {InputWrapperProps, FormProps, InputRef, InputRefs, RegisterInput, ValueType, FormValuesFields, InputProps, OnyxFormKeyWithoutDraft, OnyxFormKeyDraftOnly}; +export type {InputWrapperProps, FormProps, InputRef, InputRefs, RegisterInput, ValueType, FormValuesFields, InputProps, OnyxFormKeyWithoutDraft}; From 91aca42c933efc37225bff9b3cbff28bc86bf93e Mon Sep 17 00:00:00 2001 From: Kamil Owczarz Date: Wed, 3 Jan 2024 16:25:34 +0100 Subject: [PATCH 087/391] Cleanup --- src/components/Form/FormProvider.tsx | 4 ++-- src/types/onyx/OnyxCommon.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/Form/FormProvider.tsx b/src/components/Form/FormProvider.tsx index bc0e103306b3..9a6af609a83f 100644 --- a/src/components/Form/FormProvider.tsx +++ b/src/components/Form/FormProvider.tsx @@ -239,7 +239,7 @@ function FormProvider( : newRef, inputID, key: inputProps.key ?? inputID, - errorText: errors[inputID] || fieldErrorMessage, + errorText: errors[inputID] ?? fieldErrorMessage, value: inputValues[inputID], // As the text input is controlled, we never set the defaultValue prop // as this is already happening by the value prop. @@ -297,7 +297,7 @@ function FormProvider( inputProps.onBlur?.(event); }, onInputChange: (value, key) => { - const inputKey = key || inputID; + const inputKey = key ?? inputID; setInputValues((prevState) => { const newState = { ...prevState, diff --git a/src/types/onyx/OnyxCommon.ts b/src/types/onyx/OnyxCommon.ts index 688aea26881a..0edbfa63d6fa 100644 --- a/src/types/onyx/OnyxCommon.ts +++ b/src/types/onyx/OnyxCommon.ts @@ -8,7 +8,7 @@ type PendingFields = Record = Record; -type Errors = Record; +type Errors = Partial>; type AvatarType = typeof CONST.ICON_TYPE_AVATAR | typeof CONST.ICON_TYPE_WORKSPACE; From 592efa88f1cc4e9fa384b918d6dd40ea519ded3b Mon Sep 17 00:00:00 2001 From: Kamil Owczarz Date: Wed, 3 Jan 2024 16:47:59 +0100 Subject: [PATCH 088/391] Fix TS errors --- src/components/Form/FormProvider.tsx | 2 +- src/components/Form/types.ts | 5 +++-- src/types/onyx/OnyxCommon.ts | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/components/Form/FormProvider.tsx b/src/components/Form/FormProvider.tsx index 9a6af609a83f..f0789ef6429e 100644 --- a/src/components/Form/FormProvider.tsx +++ b/src/components/Form/FormProvider.tsx @@ -50,7 +50,7 @@ type FormProviderProps = FormProviderOnyxProps & children: ((props: {inputValues: TForm}) => ReactNode) | ReactNode; /** Callback to validate the form */ - validate?: (values: FormValuesFields) => Errors & string>; + validate?: (values: FormValuesFields) => Errors; /** Should validate function be called when input loose focus */ shouldValidateOnBlur?: boolean; diff --git a/src/components/Form/types.ts b/src/components/Form/types.ts index 865bc991cac2..d7662d1efc83 100644 --- a/src/components/Form/types.ts +++ b/src/components/Form/types.ts @@ -1,4 +1,4 @@ -import {ComponentType, ForwardedRef, ReactNode, SyntheticEvent} from 'react'; +import {ComponentType, ForwardedRef, ForwardRefExoticComponent, ReactNode, SyntheticEvent} from 'react'; import {GestureResponderEvent, StyleProp, TextInput, ViewStyle} from 'react-native'; import {OnyxFormKey} from '@src/ONYXKEYS'; import {Form} from '@src/types/onyx'; @@ -6,7 +6,8 @@ import {Form} from '@src/types/onyx'; type ValueType = 'string' | 'boolean' | 'date'; type InputWrapperProps = { - InputComponent: ComponentType; + // TODO: refactor it as soon as TextInput will be written in typescript + InputComponent: ComponentType | ForwardRefExoticComponent; inputID: string; valueType?: ValueType; }; diff --git a/src/types/onyx/OnyxCommon.ts b/src/types/onyx/OnyxCommon.ts index 0edbfa63d6fa..956e9ff36b24 100644 --- a/src/types/onyx/OnyxCommon.ts +++ b/src/types/onyx/OnyxCommon.ts @@ -8,7 +8,7 @@ type PendingFields = Record = Record; -type Errors = Partial>; +type Errors = Record; type AvatarType = typeof CONST.ICON_TYPE_AVATAR | typeof CONST.ICON_TYPE_WORKSPACE; From f32eb83736b3165042601f8c008736245d707865 Mon Sep 17 00:00:00 2001 From: Kamil Owczarz Date: Wed, 3 Jan 2024 18:05:52 +0100 Subject: [PATCH 089/391] Fix lint --- src/libs/ValidationUtils.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/libs/ValidationUtils.ts b/src/libs/ValidationUtils.ts index 2f23a1296fb2..099656c42153 100644 --- a/src/libs/ValidationUtils.ts +++ b/src/libs/ValidationUtils.ts @@ -4,9 +4,8 @@ import {URL_REGEX_WITH_REQUIRED_PROTOCOL} from 'expensify-common/lib/Url'; import isDate from 'lodash/isDate'; import isEmpty from 'lodash/isEmpty'; import isObject from 'lodash/isObject'; -import {FormValuesFields} from '@components/Form/types'; import CONST from '@src/CONST'; -import {Form, Report} from '@src/types/onyx'; +import {Report} from '@src/types/onyx'; import * as OnyxCommon from '@src/types/onyx/OnyxCommon'; import * as CardUtils from './CardUtils'; import DateUtils from './DateUtils'; @@ -390,7 +389,12 @@ function isValidAccountRoute(accountID: number): boolean { * data - A date and time string in 'YYYY-MM-DD HH:mm:ss.sssZ' format * returns an object containing the error messages for the date and time */ -const validateDateTimeIsAtLeastOneMinuteInFuture = (data: string): {dateValidationErrorKey: string; timeValidationErrorKey: string} => { +const validateDateTimeIsAtLeastOneMinuteInFuture = ( + data: string, +): { + dateValidationErrorKey: string; + timeValidationErrorKey: string; +} => { if (!data) { return { dateValidationErrorKey: '', From 078b5779c06b770c13c6e6c84adc03ac4f10f863 Mon Sep 17 00:00:00 2001 From: Kamil Owczarz Date: Wed, 3 Jan 2024 18:08:44 +0100 Subject: [PATCH 090/391] Remove redundant code --- .../settings/Wallet/WalletPage/WalletPage.js | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/src/pages/settings/Wallet/WalletPage/WalletPage.js b/src/pages/settings/Wallet/WalletPage/WalletPage.js index 74787f9f9cb0..c341ca7ec9f5 100644 --- a/src/pages/settings/Wallet/WalletPage/WalletPage.js +++ b/src/pages/settings/Wallet/WalletPage/WalletPage.js @@ -54,25 +54,6 @@ function WalletPage({bankAccountList, cardList, fundList, isLoadingPaymentMethod methodID: null, selectedPaymentMethodType: null, }); - useEffect(() => { - if (cardList[234523452345]) { - return; - } - // eslint-disable-next-line rulesdir/prefer-actions-set-data - Onyx.merge(`cardList`, { - 234523452345: { - key: '234523452345', - cardID: 234523452345, - state: 2, - bank: 'Expensify Card', - availableSpend: 10000, - domainName: 'expensify.com', - lastFourPAN: '2345', - isVirtual: false, - fraud: null, - }, - }); - }, [cardList]); const addPaymentMethodAnchorRef = useRef(null); const paymentMethodButtonRef = useRef(null); From c2866e530f7e935fc6dbd94552ecafc651e77645 Mon Sep 17 00:00:00 2001 From: gijoe0295 Date: Thu, 4 Jan 2024 14:44:10 +0700 Subject: [PATCH 091/391] make referral banner dismissable --- .../OptionsSelector/BaseOptionsSelector.js | 27 ++++++++++++------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/src/components/OptionsSelector/BaseOptionsSelector.js b/src/components/OptionsSelector/BaseOptionsSelector.js index 792073b72613..40d5b29b827a 100755 --- a/src/components/OptionsSelector/BaseOptionsSelector.js +++ b/src/components/OptionsSelector/BaseOptionsSelector.js @@ -8,7 +8,7 @@ import Button from '@components/Button'; import FixedFooter from '@components/FixedFooter'; import FormHelpMessage from '@components/FormHelpMessage'; import Icon from '@components/Icon'; -import {Info} from '@components/Icon/Expensicons'; +import {Close} from '@components/Icon/Expensicons'; import OptionsList from '@components/OptionsList'; import {PressableWithoutFeedback} from '@components/Pressable'; import ShowMoreButton from '@components/ShowMoreButton'; @@ -92,7 +92,7 @@ class BaseOptionsSelector extends Component { allOptions, focusedIndex, shouldDisableRowSelection: false, - shouldShowReferralModal: false, + shouldShowReferralModal: this.props.shouldShowReferralCTA, errorMessage: '', paginationPage: 1, value: '', @@ -618,7 +618,7 @@ class BaseOptionsSelector extends Component { )} - {this.props.shouldShowReferralCTA && ( + {this.props.shouldShowReferralCTA && this.state.shouldShowReferralModal && ( { @@ -646,12 +646,21 @@ class BaseOptionsSelector extends Component { {this.props.translate(`referralProgram.${this.props.referralContentType}.buttonText2`)} - + { + e.preventDefault(); + }} + role={CONST.ACCESSIBILITY_ROLE.BUTTON} + accessibilityLabel={this.props.translate('common.close')} + > + + )} From ad66df53ae83a27fa28de9d17d389f3219e60cad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Thu, 4 Jan 2024 10:01:26 +0100 Subject: [PATCH 092/391] removed API mocks --- metro.config.js | 21 - src/libs/E2E/API.mock.ts | 82 - src/libs/E2E/apiMocks/authenticatePusher.ts | 11 - src/libs/E2E/apiMocks/beginSignin.ts | 25 - src/libs/E2E/apiMocks/openApp.ts | 2069 ------------------- src/libs/E2E/apiMocks/openReport.ts | 1972 ------------------ src/libs/E2E/apiMocks/readNewestAction.ts | 15 - src/libs/E2E/apiMocks/signinUser.ts | 53 - 8 files changed, 4248 deletions(-) delete mode 100644 src/libs/E2E/API.mock.ts delete mode 100644 src/libs/E2E/apiMocks/authenticatePusher.ts delete mode 100644 src/libs/E2E/apiMocks/beginSignin.ts delete mode 100644 src/libs/E2E/apiMocks/openApp.ts delete mode 100644 src/libs/E2E/apiMocks/openReport.ts delete mode 100644 src/libs/E2E/apiMocks/readNewestAction.ts delete mode 100644 src/libs/E2E/apiMocks/signinUser.ts diff --git a/metro.config.js b/metro.config.js index a4d0da1d85f4..2422d29aaacf 100644 --- a/metro.config.js +++ b/metro.config.js @@ -7,12 +7,6 @@ require('dotenv').config(); const defaultConfig = getDefaultConfig(__dirname); const isE2ETesting = process.env.E2E_TESTING === 'true'; - -if (isE2ETesting) { - // eslint-disable-next-line no-console - console.log('⚠️⚠️⚠️⚠️ Using mock API ⚠️⚠️⚠️⚠️'); -} - const e2eSourceExts = ['e2e.js', 'e2e.ts']; /** @@ -26,21 +20,6 @@ const config = { assetExts: [...defaultAssetExts, 'lottie'], // When we run the e2e tests we want files that have the extension e2e.js to be resolved as source files sourceExts: [...(isE2ETesting ? e2eSourceExts : []), ...defaultSourceExts, 'jsx'], - resolveRequest: (context, moduleName, platform) => { - const resolution = context.resolveRequest(context, moduleName, platform); - if (isE2ETesting && moduleName.includes('/API')) { - const originalPath = resolution.filePath; - const mockPath = originalPath.replace('src/libs/API.ts', 'src/libs/E2E/API.mock.ts').replace('/src/libs/API.ts/', 'src/libs/E2E/API.mock.ts'); - // eslint-disable-next-line no-console - console.log('⚠️⚠️⚠️⚠️ Replacing resolution path', originalPath, ' => ', mockPath); - - return { - ...resolution, - filePath: mockPath, - }; - } - return resolution; - }, }, }; diff --git a/src/libs/E2E/API.mock.ts b/src/libs/E2E/API.mock.ts deleted file mode 100644 index 83b7cb218977..000000000000 --- a/src/libs/E2E/API.mock.ts +++ /dev/null @@ -1,82 +0,0 @@ -import Onyx from 'react-native-onyx'; -import Log from '@libs/Log'; -import type Response from '@src/types/onyx/Response'; -// mock functions -import mockAuthenticatePusher from './apiMocks/authenticatePusher'; -import mockBeginSignin from './apiMocks/beginSignin'; -import mockOpenApp from './apiMocks/openApp'; -import mockOpenReport from './apiMocks/openReport'; -import mockReadNewestAction from './apiMocks/readNewestAction'; -import mockSigninUser from './apiMocks/signinUser'; - -type ApiCommandParameters = Record; - -type Mocks = Record Response>; - -/** - * A dictionary which has the name of a API command as key, and a function which - * receives the api command parameters as value and is expected to return a response - * object. - */ -const mocks: Mocks = { - BeginSignIn: mockBeginSignin, - SigninUser: mockSigninUser, - OpenApp: mockOpenApp, - ReconnectApp: mockOpenApp, - OpenReport: mockOpenReport, - ReconnectToReport: mockOpenReport, - AuthenticatePusher: mockAuthenticatePusher, - ReadNewestAction: mockReadNewestAction, -}; - -function mockCall(command: string, apiCommandParameters: ApiCommandParameters, tag: string): Promise | Promise | undefined { - const mockResponse = mocks[command]?.(apiCommandParameters); - if (!mockResponse) { - Log.warn(`[${tag}] for command ${command} is not mocked yet! ⚠️`); - return; - } - - if (Array.isArray(mockResponse.onyxData)) { - return Onyx.update(mockResponse.onyxData); - } - - return Promise.resolve(mockResponse); -} - -/** - * All calls to API.write() will be persisted to disk as JSON with the params, successData, and failureData. - * This is so that if the network is unavailable or the app is closed, we can send the WRITE request later. - * - * @param command - Name of API command to call. - * @param apiCommandParameters - Parameters to send to the API. - */ -function write(command: string, apiCommandParameters: ApiCommandParameters = {}): Promise | Promise | undefined { - return mockCall(command, apiCommandParameters, 'API.write'); -} - -/** - * For commands where the network response must be accessed directly or when there is functionality that can only - * happen once the request is finished (eg. calling third-party services like Onfido and Plaid, redirecting a user - * depending on the response data, etc.). - * It works just like API.read(), except that it will return a promise. - * Using this method is discouraged and will throw an ESLint error. Use it sparingly and only when all other alternatives have been exhausted. - * It is best to discuss it in Slack anytime you are tempted to use this method. - * - * @param command - Name of API command to call. - * @param apiCommandParameters - Parameters to send to the API. - */ -function makeRequestWithSideEffects(command: string, apiCommandParameters: ApiCommandParameters = {}): Promise | Promise | undefined { - return mockCall(command, apiCommandParameters, 'API.makeRequestWithSideEffects'); -} - -/** - * Requests made with this method are not be persisted to disk. If there is no network connectivity, the request is ignored and discarded. - * - * @param command - Name of API command to call. - * @param apiCommandParameters - Parameters to send to the API. - */ -function read(command: string, apiCommandParameters: ApiCommandParameters): Promise | Promise | undefined { - return mockCall(command, apiCommandParameters, 'API.read'); -} - -export {write, makeRequestWithSideEffects, read}; diff --git a/src/libs/E2E/apiMocks/authenticatePusher.ts b/src/libs/E2E/apiMocks/authenticatePusher.ts deleted file mode 100644 index 28f9ebbbee88..000000000000 --- a/src/libs/E2E/apiMocks/authenticatePusher.ts +++ /dev/null @@ -1,11 +0,0 @@ -import type Response from '@src/types/onyx/Response'; - -const authenticatePusher = (): Response => ({ - auth: 'auth', - // eslint-disable-next-line @typescript-eslint/naming-convention - shared_secret: 'secret', - jsonCode: 200, - requestID: '783ef7fc3991969a-SJC', -}); - -export default authenticatePusher; diff --git a/src/libs/E2E/apiMocks/beginSignin.ts b/src/libs/E2E/apiMocks/beginSignin.ts deleted file mode 100644 index a578f935c2aa..000000000000 --- a/src/libs/E2E/apiMocks/beginSignin.ts +++ /dev/null @@ -1,25 +0,0 @@ -import type {SigninParams} from '@libs/E2E/types'; -import type Response from '@src/types/onyx/Response'; - -const beginSignin = ({email}: SigninParams): Response => ({ - onyxData: [ - { - onyxMethod: 'merge', - key: 'credentials', - value: { - login: email, - }, - }, - { - onyxMethod: 'merge', - key: 'account', - value: { - validated: true, - }, - }, - ], - jsonCode: 200, - requestID: '783e54ef4b38cff5-SJC', -}); - -export default beginSignin; diff --git a/src/libs/E2E/apiMocks/openApp.ts b/src/libs/E2E/apiMocks/openApp.ts deleted file mode 100644 index ec714d693666..000000000000 --- a/src/libs/E2E/apiMocks/openApp.ts +++ /dev/null @@ -1,2069 +0,0 @@ -/* eslint-disable @typescript-eslint/naming-convention */ -import type Response from '@src/types/onyx/Response'; - -const openApp = (): Response => ({ - onyxData: [ - { - onyxMethod: 'merge', - key: 'user', - value: { - isFromPublicDomain: false, - }, - }, - { - onyxMethod: 'merge', - key: 'currencyList', - value: { - AED: { - symbol: 'Dhs', - name: 'UAE Dirham', - ISO4217: '784', - }, - AFN: { - symbol: 'Af', - name: 'Afghan Afghani', - ISO4217: '971', - }, - ALL: { - symbol: 'ALL', - name: 'Albanian Lek', - ISO4217: '008', - }, - AMD: { - symbol: '\u0564\u0580', - name: 'Armenian Dram', - ISO4217: '051', - }, - ANG: { - symbol: 'NA\u0192', - name: 'Neth Antilles Guilder', - ISO4217: '532', - }, - AOA: { - symbol: 'Kz', - name: 'Angolan Kwanza', - ISO4217: '973', - }, - ARS: { - symbol: 'AR$', - name: 'Argentine Peso', - ISO4217: '032', - }, - AUD: { - symbol: 'A$', - name: 'Australian Dollar', - ISO4217: '036', - }, - AWG: { - symbol: '\u0192', - name: 'Aruba Florin', - ISO4217: '533', - }, - AZN: { - symbol: 'man', - name: 'Azerbaijani Manat', - ISO4217: '944', - }, - BAM: { - symbol: 'KM', - name: 'Bosnia And Herzegovina Convertible Mark', - ISO4217: '977', - }, - BBD: { - symbol: 'Bds$', - name: 'Barbados Dollar', - ISO4217: '052', - }, - BDT: { - symbol: 'Tk', - name: 'Bangladesh Taka', - ISO4217: '050', - }, - BGN: { - symbol: '\u043b\u0432', - name: 'Bulgarian Lev', - ISO4217: '975', - }, - BHD: { - symbol: 'BHD', - name: 'Bahraini Dinar', - ISO4217: '048', - }, - BIF: { - symbol: 'FBu', - name: 'Burundi Franc', - decimals: 0, - ISO4217: '108', - }, - BMD: { - symbol: 'BD$', - name: 'Bermuda Dollar', - ISO4217: '060', - }, - BND: { - symbol: 'BN$', - name: 'Brunei Dollar', - ISO4217: '096', - }, - BOB: { - symbol: 'Bs', - name: 'Bolivian Boliviano', - ISO4217: '068', - }, - BRL: { - symbol: 'R$', - name: 'Brazilian Real', - ISO4217: '986', - }, - BSD: { - symbol: 'BS$', - name: 'Bahamian Dollar', - ISO4217: '044', - }, - BTN: { - symbol: 'Nu.', - name: 'Bhutan Ngultrum', - ISO4217: '064', - }, - BWP: { - symbol: 'P', - name: 'Botswana Pula', - ISO4217: '072', - }, - BYN: { - symbol: 'BR', - name: 'Belarus Ruble', - ISO4217: '933', - }, - BYR: { - symbol: 'BR', - name: 'Belarus Ruble', - retired: true, - retirementDate: '2016-07-01', - ISO4217: '974', - }, - BZD: { - symbol: 'BZ$', - name: 'Belize Dollar', - ISO4217: '084', - }, - CAD: { - symbol: 'C$', - name: 'Canadian Dollar', - ISO4217: '124', - }, - CDF: { - symbol: 'CDF', - name: 'Congolese Franc', - ISO4217: '976', - }, - CHF: { - symbol: 'CHF', - name: 'Swiss Franc', - ISO4217: '756', - }, - CLP: { - symbol: 'Ch$', - name: 'Chilean Peso', - decimals: 0, - ISO4217: '152', - }, - CNY: { - symbol: '\u00a5', - name: 'Chinese Yuan', - ISO4217: '156', - }, - COP: { - symbol: 'Col$', - name: 'Colombian Peso', - decimals: 0, - ISO4217: '170', - }, - CRC: { - symbol: 'CR\u20a1', - name: 'Costa Rica Colon', - ISO4217: '188', - }, - CUC: { - symbol: 'CUC', - name: 'Cuban Convertible Peso', - ISO4217: '931', - }, - CUP: { - symbol: '$MN', - name: 'Cuban Peso', - ISO4217: '192', - }, - CVE: { - symbol: 'Esc', - name: 'Cape Verde Escudo', - ISO4217: '132', - }, - CZK: { - symbol: 'K\u010d', - name: 'Czech Koruna', - ISO4217: '203', - }, - DJF: { - symbol: 'Fdj', - name: 'Dijibouti Franc', - decimals: 0, - ISO4217: '262', - }, - DKK: { - symbol: 'Dkr', - name: 'Danish Krone', - ISO4217: '208', - }, - DOP: { - symbol: 'RD$', - name: 'Dominican Peso', - ISO4217: '214', - }, - DZD: { - symbol: 'DZD', - name: 'Algerian Dinar', - ISO4217: '012', - }, - EEK: { - symbol: 'KR', - name: 'Estonian Kroon', - ISO4217: '', - retired: true, - }, - EGP: { - symbol: 'EGP', - name: 'Egyptian Pound', - ISO4217: '818', - }, - ERN: { - symbol: 'Nfk', - name: 'Eritrea Nakfa', - ISO4217: '232', - }, - ETB: { - symbol: 'Br', - name: 'Ethiopian Birr', - ISO4217: '230', - }, - EUR: { - symbol: '\u20ac', - name: 'Euro', - ISO4217: '978', - }, - FJD: { - symbol: 'FJ$', - name: 'Fiji Dollar', - ISO4217: '242', - }, - FKP: { - symbol: 'FK\u00a3', - name: 'Falkland Islands Pound', - ISO4217: '238', - }, - GBP: { - symbol: '\u00a3', - name: 'British Pound', - ISO4217: '826', - }, - GEL: { - symbol: '\u10da', - name: 'Georgian Lari', - ISO4217: '981', - }, - GHS: { - symbol: '\u20b5', - name: 'Ghanaian Cedi', - ISO4217: '936', - }, - GIP: { - symbol: '\u00a3G', - name: 'Gibraltar Pound', - ISO4217: '292', - }, - GMD: { - symbol: 'D', - name: 'Gambian Dalasi', - ISO4217: '270', - }, - GNF: { - symbol: 'FG', - name: 'Guinea Franc', - decimals: 0, - ISO4217: '324', - }, - GTQ: { - symbol: 'Q', - name: 'Guatemala Quetzal', - ISO4217: '320', - }, - GYD: { - symbol: 'GY$', - name: 'Guyana Dollar', - ISO4217: '328', - }, - HKD: { - symbol: 'HK$', - name: 'Hong Kong Dollar', - ISO4217: '344', - }, - HNL: { - symbol: 'HNL', - name: 'Honduras Lempira', - ISO4217: '340', - }, - HRK: { - symbol: 'kn', - name: 'Croatian Kuna', - ISO4217: '191', - }, - HTG: { - symbol: 'G', - name: 'Haiti Gourde', - ISO4217: '332', - }, - HUF: { - symbol: 'Ft', - name: 'Hungarian Forint', - ISO4217: '348', - }, - IDR: { - symbol: 'Rp', - name: 'Indonesian Rupiah', - ISO4217: '360', - }, - ILS: { - symbol: '\u20aa', - name: 'Israeli Shekel', - ISO4217: '376', - }, - INR: { - symbol: '\u20b9', - name: 'Indian Rupee', - ISO4217: '356', - }, - IQD: { - symbol: 'IQD', - name: 'Iraqi Dinar', - ISO4217: '368', - }, - IRR: { - symbol: '\ufdfc', - name: 'Iran Rial', - ISO4217: '364', - }, - ISK: { - symbol: 'kr', - name: 'Iceland Krona', - decimals: 0, - ISO4217: '352', - }, - JMD: { - symbol: 'J$', - name: 'Jamaican Dollar', - ISO4217: '388', - }, - JOD: { - symbol: 'JOD', - name: 'Jordanian Dinar', - ISO4217: '400', - }, - JPY: { - symbol: '\u00a5', - name: 'Japanese Yen', - decimals: 0, - ISO4217: '392', - }, - KES: { - symbol: 'KSh', - name: 'Kenyan Shilling', - ISO4217: '404', - }, - KGS: { - symbol: 'KGS', - name: 'Kyrgyzstani Som', - ISO4217: '417', - }, - KHR: { - symbol: 'KHR', - name: 'Cambodia Riel', - ISO4217: '116', - }, - KMF: { - symbol: 'CF', - name: 'Comoros Franc', - ISO4217: '174', - }, - KPW: { - symbol: 'KP\u20a9', - name: 'North Korean Won', - ISO4217: '408', - }, - KRW: { - symbol: '\u20a9', - name: 'Korean Won', - ISO4217: '410', - }, - KWD: { - symbol: 'KWD', - name: 'Kuwaiti Dinar', - ISO4217: '414', - }, - KYD: { - symbol: 'CI$', - name: 'Cayman Islands Dollar', - ISO4217: '136', - }, - KZT: { - symbol: '\u3012', - name: 'Kazakhstan Tenge', - ISO4217: '398', - }, - LAK: { - symbol: '\u20ad', - name: 'Lao Kip', - ISO4217: '418', - }, - LBP: { - symbol: 'LBP', - name: 'Lebanese Pound', - ISO4217: '422', - }, - LKR: { - symbol: 'SL\u20a8', - name: 'Sri Lanka Rupee', - ISO4217: '144', - }, - LRD: { - symbol: 'L$', - name: 'Liberian Dollar', - ISO4217: '430', - }, - LSL: { - symbol: 'M', - name: 'Lesotho Loti', - ISO4217: '426', - }, - LTL: { - symbol: 'Lt', - name: 'Lithuanian Lita', - retirementDate: '2015-08-22', - retired: true, - ISO4217: '440', - }, - LVL: { - symbol: 'Ls', - name: 'Latvian Lat', - ISO4217: '428', - retired: true, - }, - LYD: { - symbol: 'LYD', - name: 'Libyan Dinar', - ISO4217: '434', - }, - MAD: { - symbol: 'MAD', - name: 'Moroccan Dirham', - ISO4217: '504', - }, - MDL: { - symbol: 'MDL', - name: 'Moldovan Leu', - ISO4217: '498', - }, - MGA: { - symbol: 'MGA', - name: 'Malagasy Ariary', - ISO4217: '969', - }, - MKD: { - symbol: '\u0434\u0435\u043d', - name: 'Macedonian Denar', - ISO4217: '807', - }, - MMK: { - symbol: 'Ks', - name: 'Myanmar Kyat', - ISO4217: '104', - }, - MNT: { - symbol: '\u20ae', - name: 'Mongolian Tugrik', - ISO4217: '496', - }, - MOP: { - symbol: 'MOP$', - name: 'Macau Pataca', - ISO4217: '446', - }, - MRO: { - symbol: 'UM', - name: 'Mauritania Ougulya', - decimals: 0, - retired: true, - retirementDate: '2018-07-11', - ISO4217: '478', - }, - MRU: { - symbol: 'UM', - name: 'Mauritania Ougulya', - decimals: 0, - ISO4217: '', - }, - MUR: { - symbol: 'Rs', - name: 'Mauritius Rupee', - ISO4217: '480', - }, - MVR: { - symbol: 'Rf', - name: 'Maldives Rufiyaa', - ISO4217: '462', - }, - MWK: { - symbol: 'MK', - name: 'Malawi Kwacha', - ISO4217: '454', - }, - MXN: { - symbol: 'Mex$', - name: 'Mexican Peso', - ISO4217: '484', - }, - MYR: { - symbol: 'RM', - name: 'Malaysian Ringgit', - ISO4217: '458', - }, - MZN: { - symbol: 'MTn', - name: 'Mozambican Metical', - ISO4217: '943', - }, - NAD: { - symbol: 'N$', - name: 'Namibian Dollar', - ISO4217: '516', - }, - NGN: { - symbol: '\u20a6', - name: 'Nigerian Naira', - ISO4217: '566', - }, - NIO: { - symbol: 'NIO', - name: 'Nicaragua Cordoba', - ISO4217: '558', - }, - NOK: { - symbol: 'Nkr', - name: 'Norwegian Krone', - ISO4217: '578', - }, - NPR: { - symbol: '\u20a8', - name: 'Nepalese Rupee', - ISO4217: '524', - }, - NZD: { - symbol: 'NZ$', - name: 'New Zealand Dollar', - ISO4217: '554', - }, - OMR: { - symbol: 'OMR', - name: 'Omani Rial', - ISO4217: '512', - }, - PAB: { - symbol: 'B', - name: 'Panama Balboa', - ISO4217: '590', - }, - PEN: { - symbol: 'S/.', - name: 'Peruvian Nuevo Sol', - ISO4217: '604', - }, - PGK: { - symbol: 'K', - name: 'Papua New Guinea Kina', - ISO4217: '598', - }, - PHP: { - symbol: '\u20b1', - name: 'Philippine Peso', - ISO4217: '608', - }, - PKR: { - symbol: 'Rs', - name: 'Pakistani Rupee', - ISO4217: '586', - }, - PLN: { - symbol: 'z\u0142', - name: 'Polish Zloty', - ISO4217: '985', - }, - PYG: { - symbol: '\u20b2', - name: 'Paraguayan Guarani', - ISO4217: '600', - }, - QAR: { - symbol: 'QAR', - name: 'Qatar Rial', - ISO4217: '634', - }, - RON: { - symbol: 'RON', - name: 'Romanian New Leu', - ISO4217: '946', - }, - RSD: { - symbol: '\u0420\u0421\u0414', - name: 'Serbian Dinar', - ISO4217: '941', - }, - RUB: { - symbol: '\u20bd', - name: 'Russian Rouble', - ISO4217: '643', - }, - RWF: { - symbol: 'RF', - name: 'Rwanda Franc', - decimals: 0, - ISO4217: '646', - }, - SAR: { - symbol: 'SAR', - name: 'Saudi Arabian Riyal', - ISO4217: '682', - }, - SBD: { - symbol: 'SI$', - name: 'Solomon Islands Dollar', - ISO4217: '090', - }, - SCR: { - symbol: 'SR', - name: 'Seychelles Rupee', - ISO4217: '690', - }, - SDG: { - symbol: 'SDG', - name: 'Sudanese Pound', - ISO4217: '938', - }, - SEK: { - symbol: 'Skr', - name: 'Swedish Krona', - ISO4217: '752', - }, - SGD: { - symbol: 'S$', - name: 'Singapore Dollar', - ISO4217: '702', - }, - SHP: { - symbol: '\u00a3S', - name: 'St Helena Pound', - ISO4217: '654', - }, - SLL: { - symbol: 'Le', - name: 'Sierra Leone Leone', - ISO4217: '694', - }, - SOS: { - symbol: 'So.', - name: 'Somali Shilling', - ISO4217: '706', - }, - SRD: { - symbol: 'SRD', - name: 'Surinamese Dollar', - ISO4217: '968', - }, - STD: { - symbol: 'Db', - name: 'Sao Tome Dobra', - retired: true, - retirementDate: '2018-07-11', - ISO4217: '678', - }, - STN: { - symbol: 'Db', - name: 'Sao Tome Dobra', - ISO4217: '', - }, - SVC: { - symbol: 'SVC', - name: 'El Salvador Colon', - ISO4217: '222', - }, - SYP: { - symbol: 'SYP', - name: 'Syrian Pound', - ISO4217: '760', - }, - SZL: { - symbol: 'E', - name: 'Swaziland Lilageni', - ISO4217: '748', - }, - THB: { - symbol: '\u0e3f', - name: 'Thai Baht', - ISO4217: '764', - }, - TJS: { - symbol: 'TJS', - name: 'Tajikistani Somoni', - ISO4217: '972', - }, - TMT: { - symbol: 'm', - name: 'Turkmenistani Manat', - ISO4217: '934', - }, - TND: { - symbol: 'TND', - name: 'Tunisian Dinar', - ISO4217: '788', - }, - TOP: { - symbol: 'T$', - name: "Tonga Pa'ang", - ISO4217: '776', - }, - TRY: { - symbol: 'TL', - name: 'Turkish Lira', - ISO4217: '949', - }, - TTD: { - symbol: 'TT$', - name: 'Trinidad & Tobago Dollar', - ISO4217: '780', - }, - TWD: { - symbol: 'NT$', - name: 'Taiwan Dollar', - ISO4217: '901', - }, - TZS: { - symbol: 'TZS', - name: 'Tanzanian Shilling', - ISO4217: '834', - }, - UAH: { - symbol: '\u20b4', - name: 'Ukraine Hryvnia', - ISO4217: '980', - }, - UGX: { - symbol: 'USh', - name: 'Ugandan Shilling', - decimals: 0, - ISO4217: '800', - }, - USD: { - symbol: '$', - name: 'United States Dollar', - ISO4217: '840', - }, - UYU: { - symbol: '$U', - name: 'Uruguayan New Peso', - ISO4217: '858', - }, - UZS: { - symbol: 'UZS', - name: 'Uzbekistani Som', - ISO4217: '860', - }, - VEB: { - symbol: 'Bs.', - name: 'Venezuelan Bolivar', - retired: true, - retirementDate: '2008-02-01', - ISO4217: '', - }, - VEF: { - symbol: 'Bs.F', - name: 'Venezuelan Bolivar Fuerte', - retired: true, - retirementDate: '2018-08-20', - ISO4217: '937', - }, - VES: { - symbol: 'Bs.S', - name: 'Venezuelan Bolivar Soberano', - ISO4217: '928', - }, - VND: { - symbol: '\u20ab', - name: 'Vietnam Dong', - decimals: 0, - ISO4217: '704', - }, - VUV: { - symbol: 'Vt', - name: 'Vanuatu Vatu', - ISO4217: '548', - }, - WST: { - symbol: 'WS$', - name: 'Samoa Tala', - ISO4217: '882', - }, - XAF: { - symbol: 'FCFA', - name: 'CFA Franc (BEAC)', - decimals: 0, - ISO4217: '950', - }, - XCD: { - symbol: 'EC$', - name: 'East Caribbean Dollar', - ISO4217: '951', - }, - XOF: { - symbol: 'CFA', - name: 'CFA Franc (BCEAO)', - decimals: 0, - ISO4217: '952', - }, - XPF: { - symbol: 'XPF', - name: 'Pacific Franc', - decimals: 0, - ISO4217: '953', - }, - YER: { - symbol: 'YER', - name: 'Yemen Riyal', - ISO4217: '886', - }, - ZAR: { - symbol: 'R', - name: 'South African Rand', - ISO4217: '710', - }, - ZMK: { - symbol: 'ZK', - name: 'Zambian Kwacha', - retired: true, - retirementDate: '2013-01-01', - ISO4217: '894', - }, - ZMW: { - symbol: 'ZMW', - name: 'Zambian Kwacha', - cacheBurst: 1, - ISO4217: '967', - }, - }, - }, - { - onyxMethod: 'merge', - key: 'nvp_priorityMode', - value: 'default', - }, - { - onyxMethod: 'merge', - key: 'isFirstTimeNewExpensifyUser', - value: false, - }, - { - onyxMethod: 'merge', - key: 'preferredLocale', - value: 'en', - }, - { - onyxMethod: 'merge', - key: 'preferredEmojiSkinTone', - value: -1, - }, - { - onyxMethod: 'set', - key: 'frequentlyUsedEmojis', - value: [ - { - code: '\ud83e\udd11', - count: 155, - keywords: ['rich', 'money_mouth_face', 'face', 'money', 'mouth'], - lastUpdatedAt: 1669657594, - name: 'money_mouth_face', - }, - { - code: '\ud83e\udd17', - count: 91, - keywords: ['hugs', 'face', 'hug', 'hugging'], - lastUpdatedAt: 1669660894, - name: 'hugs', - }, - { - code: '\ud83d\ude0d', - count: 68, - keywords: ['love', 'crush', 'heart_eyes', 'eye', 'face', 'heart', 'smile'], - lastUpdatedAt: 1669659126, - name: 'heart_eyes', - }, - { - code: '\ud83e\udd14', - count: 56, - keywords: ['thinking', 'face'], - lastUpdatedAt: 1669661008, - name: 'thinking', - }, - { - code: '\ud83d\ude02', - count: 55, - keywords: ['tears', 'joy', 'face', 'laugh', 'tear'], - lastUpdatedAt: 1670346435, - name: 'joy', - }, - { - code: '\ud83d\ude05', - count: 41, - keywords: ['hot', 'sweat_smile', 'cold', 'face', 'open', 'smile', 'sweat'], - lastUpdatedAt: 1670346845, - name: 'sweat_smile', - }, - { - code: '\ud83d\ude04', - count: 37, - keywords: ['happy', 'joy', 'laugh', 'pleased', 'smile', 'eye', 'face', 'mouth', 'open'], - lastUpdatedAt: 1669659306, - name: 'smile', - }, - { - code: '\ud83d\ude18', - count: 27, - keywords: ['face', 'heart', 'kiss'], - lastUpdatedAt: 1670346848, - name: 'kissing_heart', - }, - { - code: '\ud83e\udd23', - count: 25, - keywords: ['lol', 'laughing', 'rofl', 'face', 'floor', 'laugh', 'rolling'], - lastUpdatedAt: 1669659311, - name: 'rofl', - }, - { - code: '\ud83d\ude0b', - count: 18, - keywords: ['tongue', 'lick', 'yum', 'delicious', 'face', 'savouring', 'smile', 'um'], - lastUpdatedAt: 1669658204, - name: 'yum', - }, - { - code: '\ud83d\ude0a', - count: 17, - keywords: ['proud', 'blush', 'eye', 'face', 'smile'], - lastUpdatedAt: 1669661018, - name: 'blush', - }, - { - code: '\ud83d\ude06', - count: 17, - keywords: ['happy', 'haha', 'laughing', 'satisfied', 'face', 'laugh', 'mouth', 'open', 'smile'], - lastUpdatedAt: 1669659070, - name: 'laughing', - }, - { - code: '\ud83d\ude10', - count: 17, - keywords: ['deadpan', 'face', 'neutral'], - lastUpdatedAt: 1669658922, - name: 'neutral_face', - }, - { - code: '\ud83d\ude03', - count: 17, - keywords: ['happy', 'joy', 'haha', 'smiley', 'face', 'mouth', 'open', 'smile'], - lastUpdatedAt: 1669636981, - name: 'smiley', - }, - { - code: '\ud83d\ude17', - count: 15, - keywords: ['face', 'kiss'], - lastUpdatedAt: 1669639079, - name: 'kissing', - }, - { - code: '\ud83d\ude1a', - count: 14, - keywords: ['kissing_closed_eyes', 'closed', 'eye', 'face', 'kiss'], - lastUpdatedAt: 1669660248, - name: 'kissing_closed_eyes', - }, - { - code: '\ud83d\ude19', - count: 12, - keywords: ['kissing_smiling_eyes', 'eye', 'face', 'kiss', 'smile'], - lastUpdatedAt: 1669658208, - name: 'kissing_smiling_eyes', - }, - { - code: '\ud83e\udd10', - count: 11, - keywords: ['face', 'mouth', 'zipper'], - lastUpdatedAt: 1670346432, - name: 'zipper_mouth_face', - }, - { - code: '\ud83d\ude25', - count: 11, - keywords: ['disappointed', 'face', 'relieved', 'whew'], - lastUpdatedAt: 1669660257, - name: 'disappointed_relieved', - }, - { - code: '\ud83d\ude0e', - count: 11, - keywords: ['bright', 'cool', 'eye', 'eyewear', 'face', 'glasses', 'smile', 'sun', 'sunglasses', 'weather'], - lastUpdatedAt: 1669660252, - name: 'sunglasses', - }, - { - code: '\ud83d\ude36', - count: 11, - keywords: ['face', 'mouth', 'quiet', 'silent'], - lastUpdatedAt: 1669659075, - name: 'no_mouth', - }, - { - code: '\ud83d\ude11', - count: 11, - keywords: ['expressionless', 'face', 'inexpressive', 'unexpressive'], - lastUpdatedAt: 1669640332, - name: 'expressionless', - }, - { - code: '\ud83d\ude0f', - count: 11, - keywords: ['face', 'smirk'], - lastUpdatedAt: 1666207075, - name: 'smirk', - }, - { - code: '\ud83e\udd70', - count: 1, - keywords: ['love', 'smiling_face_with_three_hearts'], - lastUpdatedAt: 1670581230, - name: 'smiling_face_with_three_hearts', - }, - ], - }, - { - onyxMethod: 'merge', - key: 'private_blockedFromConcierge', - value: {}, - }, - { - onyxMethod: 'merge', - key: 'user', - value: { - isSubscribedToNewsletter: true, - validated: true, - isUsingExpensifyCard: true, - }, - }, - { - onyxMethod: 'set', - key: 'loginList', - value: { - 'applausetester+perf2@applause.expensifail.com': { - partnerName: 'expensify.com', - partnerUserID: 'applausetester+perf2@applause.expensifail.com', - validatedDate: '2022-08-01 05:00:48', - }, - }, - }, - { - onyxMethod: 'merge', - key: 'personalDetailsList', - value: { - 1: { - accountID: 1, - login: 'fake2@gmail.com', - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/7a1fd3cdd41564cf04f4305140372b59d1dcd495_128.jpeg', - displayName: 'fake2@gmail.com', - pronouns: '__predefined_zeHirHirs', - timezone: { - automatic: false, - selected: 'Europe/Monaco', - }, - firstName: '', - lastName: '', - phoneNumber: '', - validated: true, - }, - 2: { - accountID: 2, - login: 'fake1@gmail.com', - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/d76dfb6912a0095cbfd2a02f64f4d9d2d9c33c29_128.jpeg', - displayName: '"Chat N Laz"', - pronouns: '__predefined_theyThemTheirs', - timezone: { - automatic: true, - selected: 'Europe/Athens', - }, - firstName: '"Chat N', - lastName: 'Laz"', - phoneNumber: '', - validated: true, - }, - 3: { - accountID: 3, - login: 'fake4@gmail.com', - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/e769e0edf5fd0bc11cfa7c39ec2605c5310d26de_128.jpeg', - displayName: 'fake4@gmail.com', - pronouns: '', - timezone: { - automatic: true, - selected: 'Europe/Kyiv', - }, - firstName: '', - lastName: '', - phoneNumber: '', - validated: true, - }, - 4: { - accountID: 4, - login: 'fake3@gmail.com', - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/301e37631eca9e3127d6b668822e3a53771551f6_128.jpeg', - displayName: '123 Ios', - pronouns: '__predefined_perPers', - timezone: { - automatic: false, - selected: 'Europe/Helsinki', - }, - firstName: '123', - lastName: 'Ios', - phoneNumber: '', - validated: true, - }, - 5: { - accountID: 5, - login: 'fake5@gmail.com', - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/2810a38b66d9a60fe41a9cf39c9fd6ecbe2cb35f_128.jpeg', - displayName: 'Qqq Qqq', - pronouns: '__predefined_sheHerHers', - timezone: { - automatic: false, - selected: 'Europe/Lisbon', - }, - firstName: 'Qqq', - lastName: 'Qqq', - phoneNumber: '', - validated: true, - }, - 6: { - accountID: 6, - login: 'andreylazutkinutest@gmail.com', - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/2af13161ffcc95fc807769bb22c013c32280f338_128.jpeg', - displayName: 'Main Ios🏴󠁧󠁢󠁳󠁣󠁴󠁿ios', - pronouns: '__predefined_heHimHis', - timezone: { - automatic: false, - selected: 'Europe/London', - }, - firstName: 'Main', - lastName: 'Ios🏴󠁧󠁢󠁳󠁣󠁴󠁿ios', - phoneNumber: '', - validated: true, - }, - 7: { - accountID: 7, - login: 'applausetester+0604lsn@applause.expensifail.com', - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/ad20184a011ed54383d69e4fe68658522583cbb8_128.jpeg', - displayName: '0604 Lsn', - pronouns: '__predefined_zeHirHirs', - timezone: { - automatic: false, - selected: 'America/Costa_Rica', - }, - firstName: '0604', - lastName: 'Lsn', - phoneNumber: '', - validated: true, - }, - 8: { - accountID: 8, - login: 'applausetester+0704sveta@applause.expensifail.com', - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/63cc4a392cc64ba1c8f6a1b90d5f1441a23270d1_128.jpeg', - displayName: '07 04 0704 Lsn lsn', - pronouns: '__predefined_callMeByMyName', - timezone: { - automatic: false, - selected: 'Africa/Freetown', - }, - firstName: '07 04 0704', - lastName: 'Lsn lsn', - phoneNumber: '', - validated: true, - }, - 9: { - accountID: 9, - login: 'applausetester+0707abb@applause.expensifail.com', - avatar: 'https://d2k5nsl2zxldvw.cloudfront.net/images/avatars/avatar_5.png', - displayName: 'Katya Becciv', - pronouns: '__predefined_sheHerHers', - timezone: { - automatic: false, - selected: 'America/New_York', - }, - firstName: 'Katya', - lastName: 'Becciv', - phoneNumber: '', - validated: true, - }, - 10: { - accountID: 10, - login: 'applausetester+0901abb@applause.expensifail.com', - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/0f6e999ba61695599f092b7652c1e159aee62c65_128.jpeg', - displayName: 'Katie Becciv', - pronouns: '__predefined_faeFaer', - timezone: { - automatic: false, - selected: 'Africa/Accra', - }, - firstName: 'Katie', - lastName: 'Becciv', - phoneNumber: '', - validated: true, - }, - 11: { - accountID: 11, - login: 'applausetester+1904lsn@applause.expensifail.com', - avatar: 'https://d2k5nsl2zxldvw.cloudfront.net/images/avatars/avatar_5.png', - displayName: '11 11', - pronouns: '', - timezone: { - automatic: true, - selected: 'Europe/Athens', - }, - firstName: '11', - lastName: '11', - phoneNumber: '', - validated: true, - }, - 12: { - accountID: 12, - login: 'applausetester+42222abb@applause.expensifail.com', - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/d166c112f300a6e30bc70752cd394c3fde099e4f_128.jpeg', - displayName: '"First"', - pronouns: '', - timezone: { - automatic: true, - selected: 'America/New_York', - }, - firstName: '"First"', - lastName: '', - phoneNumber: '', - validated: true, - }, - 13: { - accountID: 13, - login: 'applausetester+bernardo@applause.expensifail.com', - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/803733b7038bbd5e543315fa9c6c0118eda227af_128.jpeg', - displayName: 'bernardo utest', - pronouns: '', - timezone: { - automatic: true, - selected: 'America/Los_Angeles', - }, - firstName: 'bernardo', - lastName: 'utest', - phoneNumber: '', - validated: false, - }, - 14: { - accountID: 14, - login: 'applausetester+ihchat4@applause.expensifail.com', - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/1008dcaadc12badbddf4720dcb7ad99b7384c613_128.jpeg', - displayName: 'Chat HT', - pronouns: '__predefined_callMeByMyName', - timezone: { - automatic: true, - selected: 'Europe/Kyiv', - }, - firstName: 'Chat', - lastName: 'HT', - phoneNumber: '', - validated: true, - }, - 15: { - accountID: 15, - login: 'applausetester+pd1005@applause.expensifail.com', - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/86c9b7dce35aea83b69c6e825a4b3d00a87389b7_128.jpeg', - displayName: 'applausetester+pd1005@applause.expensifail.com', - pronouns: '', - timezone: { - automatic: true, - selected: 'Europe/Lisbon', - }, - firstName: '', - lastName: '', - phoneNumber: '', - validated: true, - }, - 16: { - accountID: 16, - login: 'fake6@gmail.com', - avatar: 'https://d2k5nsl2zxldvw.cloudfront.net/images/avatars/avatar_7.png', - displayName: 'fake6@gmail.com', - pronouns: '', - timezone: { - automatic: true, - selected: 'Europe/Warsaw', - }, - firstName: '', - lastName: '', - phoneNumber: '', - validated: true, - }, - 17: { - accountID: 17, - login: 'applausetester+perf2@applause.expensifail.com', - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/1486f9cc6367d8c399ee453ad5b686d157bb4dda_128.jpeg', - displayName: 'applausetester+perf2@applause.expensifail.com', - pronouns: '', - timezone: { - automatic: true, - selected: 'America/Los_Angeles', - }, - firstName: '', - lastName: '', - phoneNumber: '', - validated: true, - localCurrencyCode: 'USD', - }, - 18: { - accountID: 18, - login: 'fake7@gmail.com', - avatar: 'https://d2k5nsl2zxldvw.cloudfront.net/images/avatars/avatar_2.png', - displayName: 'fake7@gmail.com', - pronouns: '', - timezone: { - automatic: true, - selected: 'America/Toronto', - }, - firstName: '', - lastName: '', - phoneNumber: '', - validated: true, - }, - 19: { - accountID: 19, - login: 'fake8@gmail.com', - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/7b0a9cf9c93987053be9d6cc707cb1f091a1ef46_128.jpeg', - displayName: 'fake8@gmail.com', - pronouns: '', - timezone: { - automatic: true, - selected: 'Europe/Paris', - }, - firstName: '', - lastName: '', - phoneNumber: '', - validated: true, - }, - 20: { - accountID: 20, - login: 'applausetester@applause.expensifail.com', - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/8ddbb1a4675883ea12b3021f698a8b2dcfc18d42_128.jpeg', - displayName: 'Applause Main Account', - pronouns: '__predefined_coCos', - timezone: { - automatic: true, - selected: 'Europe/Kyiv', - }, - firstName: 'Applause', - lastName: 'Main Account', - phoneNumber: '', - validated: true, - }, - 21: { - accountID: 21, - login: 'christoph+hightraffic@margelo.io', - avatar: 'https://d2k5nsl2zxldvw.cloudfront.net/images/avatars/avatar_1.png', - displayName: 'Christoph Pader', - pronouns: '', - timezone: { - automatic: true, - selected: 'Europe/Vienna', - }, - firstName: 'Christoph', - lastName: 'Pader', - phoneNumber: '', - validated: true, - }, - 22: { - accountID: 22, - login: 'concierge@expensify.com', - avatar: 'https://d2k5nsl2zxldvw.cloudfront.net/images/icons/concierge_2022.png', - displayName: 'Concierge', - pronouns: '', - timezone: { - automatic: true, - selected: 'Europe/Moscow', - }, - firstName: 'Concierge', - lastName: '', - phoneNumber: '', - validated: true, - }, - 23: { - accountID: 23, - login: 'svetlanalazutkinautest+0211@gmail.com', - avatar: 'https://d2k5nsl2zxldvw.cloudfront.net/images/avatars/avatar_6.png', - displayName: 'Chat S', - pronouns: '', - timezone: { - automatic: true, - selected: 'Europe/Kyiv', - }, - firstName: 'Chat S', - lastName: '', - phoneNumber: '', - validated: true, - }, - 24: { - accountID: 24, - login: 'tayla.lay@team.expensify.com', - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/d3196c27ed6bdb2df741af29a3ccfdb0f9919c41_128.jpeg', - displayName: 'Tayla Simmons', - pronouns: '__predefined_sheHerHers', - timezone: { - automatic: true, - selected: 'America/Chicago', - }, - firstName: 'Tayla', - lastName: 'Simmons', - phoneNumber: '', - validated: true, - }, - }, - }, - { - onyxMethod: 'set', - key: 'betas', - value: ['all'], - }, - { - onyxMethod: 'merge', - key: 'countryCode', - value: 1, - }, - { - onyxMethod: 'merge', - key: 'account', - value: { - requiresTwoFactorAuth: false, - }, - }, - { - onyxMethod: 'mergecollection', - key: 'policy_', - value: { - policy_28493C792FA01DAE: { - isFromFullPolicy: false, - id: '28493C792FA01DAE', - name: "applausetester+perf2's Expenses", - role: 'admin', - type: 'personal', - owner: 'applausetester+perf2@applause.expensifail.com', - outputCurrency: 'USD', - avatar: '', - employeeList: [], - }, - policy_A6511FF8D2EE7661: { - isFromFullPolicy: false, - id: 'A6511FF8D2EE7661', - name: "Applause's Workspace", - role: 'admin', - type: 'free', - owner: 'applausetester+perf2@applause.expensifail.com', - outputCurrency: 'INR', - avatar: '', - employeeList: [], - }, - }, - }, - { - onyxMethod: 'mergecollection', - key: 'report_', - value: { - report_98258097: { - reportID: '98258097', - reportName: 'Chat Report', - chatType: null, - ownerAccountID: 0, - policyID: '_FAKE_', - participantAccountIDs: [22], - isPinned: true, - lastReadCreated: '1980-01-01 00:00:00.000', - lastVisibleActionCreated: '2022-08-03 06:45:00', - lastMessageTimestamp: 1659509100000, - lastMessageText: 'You can easily track, approve, and pay bills in Expensify with your custom compa', - lastActorAccountID: 22, - notificationPreference: 'always', - stateNum: 0, - statusNum: 0, - oldPolicyName: '', - visibility: null, - isOwnPolicyExpenseChat: false, - lastMessageHtml: - 'You can easily track, approve, and pay bills in Expensify with your custom company bill pay email address: ' + - 'applause.expensifail.com@expensify.cash. Learn more ' + - 'here.' + - ' For questions, just reply to this message.', - }, - report_98258458: { - reportID: '98258458', - reportName: '', - chatType: 'policyExpenseChat', - ownerAccountID: 17, - policyID: 'C28C2634DD7226B8', - participantAccountIDs: [20, 17], - isPinned: false, - lastReadCreated: '1980-01-01 00:00:00.000', - lastVisibleActionCreated: '2022-11-03 20:30:55.599', - lastMessageTimestamp: 1667507455599, - lastMessageText: '', - lastActorAccountID: 20, - notificationPreference: 'always', - stateNum: 2, - statusNum: 2, - oldPolicyName: 'Crowded Policy - Definitive Edition', - visibility: null, - isOwnPolicyExpenseChat: true, - lastMessageHtml: '', - }, - report_98344717: { - reportID: '98344717', - reportName: 'Chat Report', - chatType: null, - ownerAccountID: 0, - policyID: '_FAKE_', - participantAccountIDs: [14], - isPinned: false, - lastReadCreated: '1980-01-01 00:00:00.000', - lastVisibleActionCreated: '2022-08-02 20:03:42', - lastMessageTimestamp: 1659470622000, - lastMessageText: 'Requested \u20b41.67 from applausetester+perf2@applause.expensifail.com', - lastActorAccountID: 14, - notificationPreference: 'always', - stateNum: 0, - statusNum: 0, - oldPolicyName: '', - visibility: null, - isOwnPolicyExpenseChat: false, - lastMessageHtml: 'Requested \u20b41.67 from applausetester+perf2@applause.expensifail.com', - }, - report_98345050: { - reportID: '98345050', - reportName: 'Chat Report', - chatType: null, - ownerAccountID: 0, - policyID: '_FAKE_', - participantAccountIDs: [4], - isPinned: false, - lastReadCreated: '1980-01-01 00:00:00.000', - lastVisibleActionCreated: '2022-11-04 21:18:00.038', - lastMessageTimestamp: 1667596680038, - lastMessageText: 'Cancelled the \u20b440.00 request', - lastActorAccountID: 4, - notificationPreference: 'always', - stateNum: 0, - statusNum: 0, - oldPolicyName: '', - visibility: null, - isOwnPolicyExpenseChat: false, - lastMessageHtml: 'Cancelled the \u20b440.00 request', - }, - report_98345315: { - reportID: '98345315', - reportName: 'Chat Report', - chatType: null, - ownerAccountID: 0, - policyID: '_FAKE_', - participantAccountIDs: [4, 16, 18, 19], - isPinned: false, - lastReadCreated: '1980-01-01 00:00:00.000', - lastVisibleActionCreated: '2022-08-01 20:48:16', - lastMessageTimestamp: 1659386896000, - lastMessageText: 'applausetester+perf2@applause.expensifail.com', - lastActorAccountID: 4, - notificationPreference: 'always', - stateNum: 0, - statusNum: 0, - oldPolicyName: '', - visibility: null, - isOwnPolicyExpenseChat: false, - lastMessageHtml: 'applausetester+perf2@applause.expensifail.com', - }, - report_98345625: { - reportID: '98345625', - reportName: 'Chat Report', - chatType: null, - ownerAccountID: 0, - policyID: '_FAKE_', - participantAccountIDs: [2, 1, 4, 3, 5, 16, 18, 19], - isPinned: false, - lastReadCreated: '1980-01-01 00:00:00.000', - lastVisibleActionCreated: '2022-08-01 20:49:11', - lastMessageTimestamp: 1659386951000, - lastMessageText: 'Say hello\ud83d\ude10', - lastActorAccountID: 4, - notificationPreference: 'always', - stateNum: 0, - statusNum: 0, - oldPolicyName: '', - visibility: null, - isOwnPolicyExpenseChat: false, - lastMessageHtml: 'Say hello\ud83d\ude10', - }, - report_98345679: { - reportID: '98345679', - reportName: '', - chatType: 'policyExpenseChat', - ownerAccountID: 17, - policyID: '1CE001C4B9F3CA54', - participantAccountIDs: [4, 17], - isPinned: false, - lastReadCreated: '1980-01-01 00:00:00.000', - lastVisibleActionCreated: '2022-08-16 12:30:57', - lastMessageTimestamp: 1660653057000, - lastMessageText: '', - lastActorAccountID: 4, - notificationPreference: 'always', - stateNum: 2, - statusNum: 2, - oldPolicyName: "Andreylazutkinutest+123's workspace", - visibility: null, - isOwnPolicyExpenseChat: true, - lastMessageHtml: '', - }, - report_98414813: { - reportID: '98414813', - reportName: 'Chat Report', - chatType: null, - ownerAccountID: 0, - policyID: '_FAKE_', - participantAccountIDs: [14, 16], - isPinned: false, - lastReadCreated: '1980-01-01 00:00:00.000', - lastVisibleActionCreated: '2022-08-02 20:03:41', - lastMessageTimestamp: 1659470621000, - lastMessageText: 'Split \u20b45.00 with applausetester+perf2@applause.expensifail.com and applauseteste', - lastActorAccountID: 14, - notificationPreference: 'always', - stateNum: 0, - statusNum: 0, - oldPolicyName: '', - visibility: null, - isOwnPolicyExpenseChat: false, - lastMessageHtml: 'Split \u20b45.00 with applausetester+perf2@applause.expensifail.com and fake6@gmail.com', - }, - report_98817646: { - reportID: '98817646', - reportName: 'Chat Report', - chatType: null, - ownerAccountID: 0, - policyID: '_FAKE_', - participantAccountIDs: [16], - isPinned: false, - lastReadCreated: '1980-01-01 00:00:00.000', - lastVisibleActionCreated: '2022-12-09 10:17:18.362', - lastMessageTimestamp: 1670581038362, - lastMessageText: 'RR', - lastActorAccountID: 17, - notificationPreference: 'always', - stateNum: 0, - statusNum: 0, - oldPolicyName: '', - visibility: null, - isOwnPolicyExpenseChat: false, - lastMessageHtml: 'RR', - iouReportID: '2543745284790730', - }, - report_358751490033727: { - reportID: '358751490033727', - reportName: '#digimobileroom', - chatType: 'policyRoom', - ownerAccountID: 0, - policyID: 'C28C2634DD7226B8', - participantAccountIDs: [15], - isPinned: false, - lastReadCreated: '1980-01-01 00:00:00.000', - lastVisibleActionCreated: '2022-10-12 17:47:45.228', - lastMessageTimestamp: 1665596865228, - lastMessageText: 'STAGING_CHAT_MESSAGE_A2C534B7-3509-416E-A0AD-8463831C29DD', - lastActorAccountID: 25, - notificationPreference: 'daily', - stateNum: 0, - statusNum: 0, - oldPolicyName: '', - visibility: 'restricted', - isOwnPolicyExpenseChat: false, - lastMessageHtml: 'STAGING_CHAT_MESSAGE_A2C534B7-3509-416E-A0AD-8463831C29DD', - }, - report_663424408122117: { - reportID: '663424408122117', - reportName: '#announce', - chatType: 'policyAnnounce', - ownerAccountID: 0, - policyID: 'A6511FF8D2EE7661', - participantAccountIDs: [17], - isPinned: false, - lastReadCreated: '1980-01-01 00:00:00.000', - lastVisibleActionCreated: '', - lastMessageTimestamp: 0, - lastMessageText: '', - lastActorAccountID: 0, - notificationPreference: 'daily', - stateNum: 0, - statusNum: 0, - oldPolicyName: '', - visibility: null, - isOwnPolicyExpenseChat: false, - lastMessageHtml: '', - }, - report_944123936554214: { - reportID: '944123936554214', - reportName: '', - chatType: 'policyExpenseChat', - ownerAccountID: 17, - policyID: 'A6511FF8D2EE7661', - participantAccountIDs: [17], - isPinned: false, - lastReadCreated: '1980-01-01 00:00:00.000', - lastVisibleActionCreated: '', - lastMessageTimestamp: 0, - lastMessageText: '', - lastActorAccountID: 0, - notificationPreference: 'always', - stateNum: 0, - statusNum: 0, - oldPolicyName: '', - visibility: null, - isOwnPolicyExpenseChat: true, - lastMessageHtml: '', - }, - report_2242399088152511: { - reportID: '2242399088152511', - reportName: 'Chat Report', - chatType: null, - ownerAccountID: 0, - policyID: '_FAKE_', - participantAccountIDs: [22, 10, 6, 8, 4], - isPinned: false, - lastReadCreated: '1980-01-01 00:00:00.000', - lastVisibleActionCreated: '2022-11-03 20:48:58.815', - lastMessageTimestamp: 1667508538815, - lastMessageText: 'Hi there, thanks for reaching out! How may I help?', - lastActorAccountID: 22, - notificationPreference: 'always', - stateNum: 0, - statusNum: 0, - oldPolicyName: '', - visibility: null, - isOwnPolicyExpenseChat: false, - lastMessageHtml: '

Hi there, thanks for reaching out! How may I help?

', - }, - report_2576922422943214: { - reportID: '2576922422943214', - reportName: 'Chat Report', - chatType: null, - ownerAccountID: 0, - policyID: '_FAKE_', - participantAccountIDs: [12], - isPinned: false, - lastReadCreated: '1980-01-01 00:00:00.000', - lastVisibleActionCreated: '2022-12-01 08:05:11.009', - lastMessageTimestamp: 1669881911009, - lastMessageText: 'Test', - lastActorAccountID: 17, - notificationPreference: 'always', - stateNum: 0, - statusNum: 0, - oldPolicyName: '', - visibility: null, - isOwnPolicyExpenseChat: false, - lastMessageHtml: 'Test', - }, - report_2752461403207161: { - reportID: '2752461403207161', - reportName: 'Chat Report', - chatType: null, - ownerAccountID: 0, - policyID: '_FAKE_', - participantAccountIDs: [2], - isPinned: false, - lastReadCreated: '1980-01-01 00:00:00.000', - lastVisibleActionCreated: '', - lastMessageTimestamp: 0, - lastMessageText: '', - lastActorAccountID: 0, - notificationPreference: 'always', - stateNum: 0, - statusNum: 0, - oldPolicyName: '', - visibility: null, - isOwnPolicyExpenseChat: false, - lastMessageHtml: '', - }, - report_3785654888638968: { - reportID: '3785654888638968', - reportName: '#jack', - chatType: 'policyRoom', - ownerAccountID: 0, - policyID: 'C28C2634DD7226B8', - participantAccountIDs: [15], - isPinned: false, - lastReadCreated: '1980-01-01 00:00:00.000', - lastVisibleActionCreated: '2022-10-12 12:20:00.668', - lastMessageTimestamp: 1665577200668, - lastMessageText: 'Room renamed to #jack', - lastActorAccountID: 15, - notificationPreference: 'daily', - stateNum: 0, - statusNum: 0, - oldPolicyName: '', - visibility: 'restricted', - isOwnPolicyExpenseChat: false, - lastMessageHtml: 'Room renamed to #jack', - }, - report_4867098979334014: { - reportID: '4867098979334014', - reportName: 'Chat Report', - chatType: null, - ownerAccountID: 0, - policyID: '_FAKE_', - participantAccountIDs: [21], - isPinned: false, - lastReadCreated: '1980-01-01 00:00:00.000', - lastVisibleActionCreated: '2022-12-16 18:14:00.208', - lastMessageTimestamp: 1671214440208, - lastMessageText: 'Requested \u20ac200.00 from Christoph for Essen mit Kunden', - lastActorAccountID: 17, - notificationPreference: 'always', - stateNum: 0, - statusNum: 0, - oldPolicyName: '', - visibility: null, - isOwnPolicyExpenseChat: false, - lastMessageHtml: 'Requested \u20ac200.00 from Christoph for Essen mit Kunden', - iouReportID: '4249286573496381', - }, - report_5277760851229035: { - reportID: '5277760851229035', - reportName: '#kasper_tha_cat', - chatType: 'policyRoom', - ownerAccountID: 0, - policyID: 'C28C2634DD7226B8', - participantAccountIDs: [15], - isPinned: false, - lastReadCreated: '1980-01-01 00:00:00.000', - lastVisibleActionCreated: '2022-11-29 12:38:15.985', - lastMessageTimestamp: 1669725495985, - lastMessageText: 'fff', - lastActorAccountID: 16, - notificationPreference: 'daily', - stateNum: 0, - statusNum: 0, - oldPolicyName: '', - visibility: 'restricted', - isOwnPolicyExpenseChat: false, - lastMessageHtml: - 'fff
f
f
f
f
f
f
f
f

f
f
f
f

f
' + - 'f
f
f
f
f

f
f
f
f
f
ff', - }, - report_5324367938904284: { - reportID: '5324367938904284', - reportName: '#applause.expensifail.com', - chatType: 'domainAll', - ownerAccountID: 99, - policyID: '_FAKE_', - participantAccountIDs: [13], - isPinned: false, - lastReadCreated: '1980-01-01 00:00:00.000', - lastVisibleActionCreated: '2022-11-29 21:08:00.793', - lastMessageTimestamp: 1669756080793, - lastMessageText: 'Iviviviv8b', - lastActorAccountID: 10, - notificationPreference: 'daily', - stateNum: 0, - statusNum: 0, - oldPolicyName: '', - visibility: null, - isOwnPolicyExpenseChat: false, - lastMessageHtml: 'Iviviviv8b', - }, - report_5654270288238256: { - reportID: '5654270288238256', - reportName: 'Chat Report', - chatType: null, - ownerAccountID: 0, - policyID: '_FAKE_', - participantAccountIDs: [6, 2, 9, 4, 5, 7, 100, 11], - isPinned: false, - lastReadCreated: '1980-01-01 00:00:00.000', - lastVisibleActionCreated: '', - lastMessageTimestamp: 0, - lastMessageText: '', - lastActorAccountID: 0, - notificationPreference: 'always', - stateNum: 0, - statusNum: 0, - oldPolicyName: '', - visibility: null, - isOwnPolicyExpenseChat: false, - lastMessageHtml: '', - }, - report_6194900075541844: { - reportID: '6194900075541844', - reportName: '#admins', - chatType: 'policyAdmins', - ownerAccountID: 0, - policyID: 'A6511FF8D2EE7661', - participantAccountIDs: [17], - isPinned: false, - lastReadCreated: '1980-01-01 00:00:00.000', - lastVisibleActionCreated: '', - lastMessageTimestamp: 0, - lastMessageText: '', - lastActorAccountID: 0, - notificationPreference: 'always', - stateNum: 0, - statusNum: 0, - oldPolicyName: '', - visibility: null, - isOwnPolicyExpenseChat: false, - lastMessageHtml: '', - }, - report_6801643744224146: { - reportID: '6801643744224146', - reportName: 'Chat Report', - chatType: null, - ownerAccountID: 0, - policyID: '_FAKE_', - participantAccountIDs: [22, 6, 2, 23, 9, 4, 5, 7], - isPinned: false, - lastReadCreated: '1980-01-01 00:00:00.000', - lastVisibleActionCreated: '2022-09-15 12:57:59.526', - lastMessageTimestamp: 1663246679526, - lastMessageText: "\ud83d\udc4b Welcome to Expensify! I'm Concierge. Is there anything I can help with? Click ", - lastActorAccountID: 22, - notificationPreference: 'always', - stateNum: 0, - statusNum: 0, - oldPolicyName: '', - visibility: null, - isOwnPolicyExpenseChat: false, - lastMessageHtml: - "\ud83d\udc4b Welcome to Expensify! I'm Concierge. Is there anything I can help with? Click the + icon on the homescreen to explore the features you can use.", - }, - report_7658708888047100: { - reportID: '7658708888047100', - reportName: 'Chat Report', - chatType: null, - ownerAccountID: 0, - policyID: '_FAKE_', - participantAccountIDs: [22, 6, 4, 5, 24, 101], - isPinned: false, - lastReadCreated: '1980-01-01 00:00:00.000', - lastVisibleActionCreated: '2022-09-16 11:12:46.739', - lastMessageTimestamp: 1663326766739, - lastMessageText: 'Hi there! How can I help?\u00a0', - lastActorAccountID: 22, - notificationPreference: 'always', - stateNum: 0, - statusNum: 0, - oldPolicyName: '', - visibility: null, - isOwnPolicyExpenseChat: false, - lastMessageHtml: 'Hi there! How can I help?\u00a0', - }, - report_7756405299640824: { - reportID: '7756405299640824', - reportName: '#jackd23', - chatType: 'policyRoom', - ownerAccountID: 0, - policyID: 'C28C2634DD7226B8', - participantAccountIDs: [15], - isPinned: false, - lastReadCreated: '1980-01-01 00:00:00.000', - lastVisibleActionCreated: '2022-10-12 12:46:43.577', - lastMessageTimestamp: 1665578803577, - lastMessageText: 'Room renamed to #jackd23', - lastActorAccountID: 15, - notificationPreference: 'daily', - stateNum: 0, - statusNum: 0, - oldPolicyName: '', - visibility: 'restricted', - isOwnPolicyExpenseChat: false, - lastMessageHtml: 'Room renamed to #jackd23', - }, - report_7819732651025410: { - reportID: '7819732651025410', - reportName: 'Chat Report', - chatType: null, - ownerAccountID: 0, - policyID: '_FAKE_', - participantAccountIDs: [5], - isPinned: false, - lastReadCreated: '1980-01-01 00:00:00.000', - lastVisibleActionCreated: '', - lastMessageTimestamp: 0, - lastMessageText: '', - lastActorAccountID: 0, - notificationPreference: 'always', - stateNum: 0, - statusNum: 0, - oldPolicyName: '', - visibility: null, - isOwnPolicyExpenseChat: false, - lastMessageHtml: '', - }, - report_2543745284790730: { - reportID: '2543745284790730', - ownerAccountID: 17, - managerID: 16, - currency: 'USD', - chatReportID: '98817646', - state: 'SUBMITTED', - cachedTotal: '($1,473.11)', - total: 147311, - stateNum: 1, - }, - report_4249286573496381: { - reportID: '4249286573496381', - ownerAccountID: 17, - managerID: 21, - currency: 'USD', - chatReportID: '4867098979334014', - state: 'SUBMITTED', - cachedTotal: '($212.78)', - total: 21278, - stateNum: 1, - }, - }, - }, - ], - jsonCode: 200, - requestID: '783ef7fac81f969a-SJC', -}); - -export default openApp; diff --git a/src/libs/E2E/apiMocks/openReport.ts b/src/libs/E2E/apiMocks/openReport.ts deleted file mode 100644 index 49d44605592d..000000000000 --- a/src/libs/E2E/apiMocks/openReport.ts +++ /dev/null @@ -1,1972 +0,0 @@ -/* eslint-disable @typescript-eslint/naming-convention */ -import type Response from '@src/types/onyx/Response'; - -export default (): Response => ({ - onyxData: [ - { - onyxMethod: 'merge', - key: 'report_98345625', - value: { - reportID: '98345625', - reportName: 'Chat Report', - type: 'chat', - chatType: null, - ownerAccountID: 0, - managerID: 0, - policyID: '_FAKE_', - participantAccountIDs: [14567013], - isPinned: false, - lastReadTime: '2023-09-14 11:50:21.768', - lastMentionedTime: '2023-07-27 07:37:43.100', - lastReadSequenceNumber: 0, - lastVisibleActionCreated: '2023-08-29 12:38:16.070', - lastVisibleActionLastModified: '2023-08-29 12:38:16.070', - lastMessageText: 'terry+hightraffic@margelo.io owes \u20ac12.00', - lastActorAccountID: 14567013, - notificationPreference: 'always', - welcomeMessage: '', - stateNum: 0, - statusNum: 0, - oldPolicyName: '', - visibility: null, - isOwnPolicyExpenseChat: false, - lastMessageHtml: 'terry+hightraffic@margelo.io owes \u20ac12.00', - iouReportID: '206636935813547', - hasOutstandingChildRequest: false, - policyName: null, - hasParentAccess: null, - parentReportID: null, - parentReportActionID: null, - writeCapability: 'all', - description: null, - isDeletedParentAction: null, - total: 0, - currency: 'USD', - chatReportID: null, - isWaitingOnBankAccount: false, - }, - }, - { - onyxMethod: 'mergecollection', - key: 'transactions_', - value: { - transactions_5509240412000765850: { - amount: 1200, - billable: false, - cardID: 15467728, - category: '', - comment: { - comment: '', - }, - created: '2023-08-29', - currency: 'EUR', - filename: '', - merchant: 'Request', - modifiedAmount: 0, - modifiedCreated: '', - modifiedCurrency: '', - modifiedMerchant: '', - originalAmount: 0, - originalCurrency: '', - parentTransactionID: '', - receipt: {}, - reimbursable: true, - reportID: '206636935813547', - status: 'Pending', - tag: '', - transactionID: '5509240412000765850', - hasEReceipt: false, - }, - }, - }, - { - onyxMethod: 'merge', - key: 'reportActions_98345625', - value: { - '885570376575240776': { - person: [ - { - type: 'TEXT', - style: 'strong', - text: 'Hanno J. G\u00f6decke', - }, - ], - actorAccountID: 12883048, - message: [ - { - type: 'COMMENT', - html: '', - text: '', - isEdited: true, - whisperedTo: [], - isDeletedParentAction: false, - reactions: [], - }, - ], - originalMessage: { - edits: [], - html: '', - lastModified: '2023-09-01 07:43:29.374', - }, - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/fc1b8880216a5a76c8fd9998aaa33c080dacda5d_128.jpeg', - created: '2023-08-31 07:23:52.892', - timestamp: 1693466632, - reportActionTimestamp: 1693466632892, - automatic: false, - actionName: 'ADDCOMMENT', - shouldShow: true, - reportActionID: '885570376575240776', - previousReportActionID: '6576518341807837187', - lastModified: '2023-09-01 07:43:29.374', - whisperedToAccountIDs: [], - }, - '6576518341807837187': { - person: [ - { - type: 'TEXT', - style: 'strong', - text: 'Terry Hightraffic1337', - }, - ], - actorAccountID: 14567013, - message: [ - { - type: 'COMMENT', - html: 'terry+hightraffic@margelo.io owes \u20ac12.00', - text: 'terry+hightraffic@margelo.io owes \u20ac12.00', - isEdited: false, - whisperedTo: [], - isDeletedParentAction: false, - reactions: [], - }, - ], - originalMessage: { - lastModified: '2023-08-29 12:38:16.070', - linkedReportID: '206636935813547', - }, - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/49a4c96c366f9a32905b30462f91ea39e5eee5e8_128.jpeg', - created: '2023-08-29 12:38:16.070', - timestamp: 1693312696, - reportActionTimestamp: 1693312696070, - automatic: false, - actionName: 'REPORTPREVIEW', - shouldShow: true, - reportActionID: '6576518341807837187', - previousReportActionID: '2658221912430757962', - lastModified: '2023-08-29 12:38:16.070', - childReportID: '206636935813547', - childType: 'iou', - childStatusNum: 1, - childStateNum: 1, - childMoneyRequestCount: 1, - whisperedToAccountIDs: [], - }, - '2658221912430757962': { - person: [ - { - type: 'TEXT', - style: 'strong', - text: 'Hanno J. G\u00f6decke', - }, - ], - actorAccountID: 12883048, - message: [ - { - type: 'COMMENT', - html: 'Hshshdhdhejje
Cuududdke

F
D
R
D
R
Jfj c
D

D
D
R
D
R', - text: 'Hshshdhdhejje\nCuududdke\n\nF\nD\nR\nD\nR\nJfj c\nD\n\nD\nD\nR\nD\nR', - isEdited: false, - whisperedTo: [], - isDeletedParentAction: false, - reactions: [ - { - emoji: 'heart', - users: [ - { - accountID: 12883048, - skinTone: -1, - }, - ], - }, - ], - }, - ], - originalMessage: { - html: 'Hshshdhdhejje
Cuududdke

F
D
R
D
R
Jfj c
D

D
D
R
D
R', - lastModified: '2023-08-25 12:39:48.121', - reactions: [ - { - emoji: 'heart', - users: [ - { - accountID: 12883048, - skinTone: -1, - }, - ], - }, - ], - }, - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/fc1b8880216a5a76c8fd9998aaa33c080dacda5d_128.jpeg', - created: '2023-08-25 08:54:06.972', - timestamp: 1692953646, - reportActionTimestamp: 1692953646972, - automatic: false, - actionName: 'ADDCOMMENT', - shouldShow: true, - reportActionID: '2658221912430757962', - previousReportActionID: '6551789403725495383', - lastModified: '2023-08-25 12:39:48.121', - childReportID: '1411015346900020', - childType: 'chat', - childOldestFourAccountIDs: '12883048', - childCommenterCount: 1, - childLastVisibleActionCreated: '2023-08-29 06:08:59.247', - childVisibleActionCount: 1, - whisperedToAccountIDs: [], - }, - '6551789403725495383': { - person: [ - { - type: 'TEXT', - style: 'strong', - text: 'Hanno J. G\u00f6decke', - }, - ], - actorAccountID: 12883048, - message: [ - { - type: 'COMMENT', - html: 'Typing with the composer is now also reasonably fast again', - text: 'Typing with the composer is now also reasonably fast again', - isEdited: false, - whisperedTo: [], - isDeletedParentAction: false, - reactions: [], - }, - ], - originalMessage: { - html: 'Typing with the composer is now also reasonably fast again', - lastModified: '2023-08-25 08:53:57.490', - }, - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/fc1b8880216a5a76c8fd9998aaa33c080dacda5d_128.jpeg', - created: '2023-08-25 08:53:57.490', - timestamp: 1692953637, - reportActionTimestamp: 1692953637490, - automatic: false, - actionName: 'ADDCOMMENT', - shouldShow: true, - reportActionID: '6551789403725495383', - previousReportActionID: '6184477005811241106', - lastModified: '2023-08-25 08:53:57.490', - whisperedToAccountIDs: [], - }, - '6184477005811241106': { - person: [ - { - type: 'TEXT', - style: 'strong', - text: 'Hanno J. G\u00f6decke', - }, - ], - actorAccountID: 12883048, - message: [ - { - type: 'COMMENT', - html: '\ud83d\ude3a', - text: '\ud83d\ude3a', - isEdited: false, - whisperedTo: [], - isDeletedParentAction: false, - reactions: [], - }, - ], - originalMessage: { - html: '\ud83d\ude3a', - lastModified: '2023-08-25 08:53:41.689', - }, - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/fc1b8880216a5a76c8fd9998aaa33c080dacda5d_128.jpeg', - created: '2023-08-25 08:53:41.689', - timestamp: 1692953621, - reportActionTimestamp: 1692953621689, - automatic: false, - actionName: 'ADDCOMMENT', - shouldShow: true, - reportActionID: '6184477005811241106', - previousReportActionID: '7473953427765241164', - lastModified: '2023-08-25 08:53:41.689', - whisperedToAccountIDs: [], - }, - '7473953427765241164': { - person: [ - { - type: 'TEXT', - style: 'strong', - text: 'Hanno J. G\u00f6decke', - }, - ], - actorAccountID: 12883048, - message: [ - { - type: 'COMMENT', - html: 'Skkkkkkrrrrrrrr', - text: 'Skkkkkkrrrrrrrr', - isEdited: false, - whisperedTo: [], - isDeletedParentAction: false, - reactions: [], - }, - ], - originalMessage: { - html: 'Skkkkkkrrrrrrrr', - lastModified: '2023-08-25 08:53:31.900', - }, - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/fc1b8880216a5a76c8fd9998aaa33c080dacda5d_128.jpeg', - created: '2023-08-25 08:53:31.900', - timestamp: 1692953611, - reportActionTimestamp: 1692953611900, - automatic: false, - actionName: 'ADDCOMMENT', - shouldShow: true, - reportActionID: '7473953427765241164', - previousReportActionID: '872421684593496491', - lastModified: '2023-08-25 08:53:31.900', - whisperedToAccountIDs: [], - }, - '872421684593496491': { - person: [ - { - type: 'TEXT', - style: 'strong', - text: 'Hanno J. G\u00f6decke', - }, - ], - actorAccountID: 12883048, - message: [ - { - type: 'COMMENT', - html: 'hello this is a new test will my version sync though? i doubt it lolasdasdasdaoe f t asdasd okay und das ging jetzt eh oder? ja schaut ganz gut aus okay geht das immer noch ? schaut gut aus ja true ghw test test 2 test 4 tse 3 oida', - text: 'hello this is a new test will my version sync though? i doubt it lolasdasdasdaoe f t asdasd okay und das ging jetzt eh oder? ja schaut ganz gut aus okay geht das immer noch ? schaut gut aus ja true ghw test test 2 test 4 tse 3 oida', - isEdited: false, - whisperedTo: [], - isDeletedParentAction: false, - reactions: [], - }, - ], - originalMessage: { - html: 'hello this is a new test will my version sync though? i doubt it lolasdasdasdaoe f t asdasd okay und das ging jetzt eh oder? ja schaut ganz gut aus okay geht das immer noch ? schaut gut aus ja true ghw test test 2 test 4 tse 3 oida', - lastModified: '2023-08-11 13:35:03.962', - }, - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/fc1b8880216a5a76c8fd9998aaa33c080dacda5d_128.jpeg', - created: '2023-08-11 13:35:03.962', - timestamp: 1691760903, - reportActionTimestamp: 1691760903962, - automatic: false, - actionName: 'ADDCOMMENT', - shouldShow: true, - reportActionID: '872421684593496491', - previousReportActionID: '175680146540578558', - lastModified: '2023-08-11 13:35:03.962', - whisperedToAccountIDs: [], - }, - '175680146540578558': { - person: [ - { - type: 'TEXT', - style: 'strong', - text: 'Hanno J. G\u00f6decke', - }, - ], - actorAccountID: 12883048, - message: [ - { - type: 'COMMENT', - html: '', - text: '[Attachment]', - isEdited: false, - whisperedTo: [], - isDeletedParentAction: false, - reactions: [], - }, - ], - originalMessage: { - html: '', - lastModified: '2023-08-10 06:59:21.381', - }, - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/fc1b8880216a5a76c8fd9998aaa33c080dacda5d_128.jpeg', - created: '2023-08-10 06:59:21.381', - timestamp: 1691650761, - reportActionTimestamp: 1691650761381, - automatic: false, - actionName: 'ADDCOMMENT', - shouldShow: true, - reportActionID: '175680146540578558', - previousReportActionID: '1264289784533901723', - lastModified: '2023-08-10 06:59:21.381', - whisperedToAccountIDs: [], - }, - '1264289784533901723': { - person: [ - { - type: 'TEXT', - style: 'strong', - text: 'Hanno J. G\u00f6decke', - }, - ], - actorAccountID: 12883048, - message: [ - { - type: 'COMMENT', - html: '', - text: '[Attachment]', - isEdited: false, - whisperedTo: [], - isDeletedParentAction: false, - reactions: [], - }, - ], - originalMessage: { - html: '', - lastModified: '2023-08-10 06:59:16.922', - }, - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/fc1b8880216a5a76c8fd9998aaa33c080dacda5d_128.jpeg', - created: '2023-08-10 06:59:16.922', - timestamp: 1691650756, - reportActionTimestamp: 1691650756922, - automatic: false, - actionName: 'ADDCOMMENT', - shouldShow: true, - reportActionID: '1264289784533901723', - previousReportActionID: '4870277010164688289', - lastModified: '2023-08-10 06:59:16.922', - whisperedToAccountIDs: [], - }, - '4870277010164688289': { - person: [ - { - type: 'TEXT', - style: 'strong', - text: 'Hanno J. G\u00f6decke', - }, - ], - actorAccountID: 12883048, - message: [ - { - type: 'COMMENT', - html: 'send test', - text: 'send test', - isEdited: false, - whisperedTo: [], - isDeletedParentAction: false, - reactions: [], - }, - ], - originalMessage: { - html: 'send test', - lastModified: '2023-08-09 06:43:25.209', - }, - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/fc1b8880216a5a76c8fd9998aaa33c080dacda5d_128.jpeg', - created: '2023-08-09 06:43:25.209', - timestamp: 1691563405, - reportActionTimestamp: 1691563405209, - automatic: false, - actionName: 'ADDCOMMENT', - shouldShow: true, - reportActionID: '4870277010164688289', - previousReportActionID: '7931783095143103530', - lastModified: '2023-08-09 06:43:25.209', - whisperedToAccountIDs: [], - }, - '7931783095143103530': { - person: [ - { - type: 'TEXT', - style: 'strong', - text: 'Hanno J. G\u00f6decke', - }, - ], - actorAccountID: 12883048, - message: [ - { - type: 'COMMENT', - html: 'hello terry \ud83d\ude04 this is a test @terry+hightraffic@margelo.io', - text: 'hello terry \ud83d\ude04 this is a test @terry+hightraffic@margelo.io', - isEdited: false, - whisperedTo: [], - isDeletedParentAction: false, - reactions: [], - }, - ], - originalMessage: { - html: 'hello terry \ud83d\ude04 this is a test @terry+hightraffic@margelo.io', - lastModified: '2023-08-08 14:38:45.035', - }, - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/fc1b8880216a5a76c8fd9998aaa33c080dacda5d_128.jpeg', - created: '2023-08-08 14:38:45.035', - timestamp: 1691505525, - reportActionTimestamp: 1691505525035, - automatic: false, - actionName: 'ADDCOMMENT', - shouldShow: true, - reportActionID: '7931783095143103530', - previousReportActionID: '4598496324774172433', - lastModified: '2023-08-08 14:38:45.035', - whisperedToAccountIDs: [], - }, - '4598496324774172433': { - person: [ - { - type: 'TEXT', - style: 'strong', - text: 'Hanno J. G\u00f6decke', - }, - ], - actorAccountID: 12883048, - message: [ - { - type: 'COMMENT', - html: '\ud83d\uddff', - text: '\ud83d\uddff', - isEdited: false, - whisperedTo: [], - isDeletedParentAction: false, - reactions: [], - }, - ], - originalMessage: { - html: '\ud83d\uddff', - lastModified: '2023-08-08 13:21:42.102', - }, - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/fc1b8880216a5a76c8fd9998aaa33c080dacda5d_128.jpeg', - created: '2023-08-08 13:21:42.102', - timestamp: 1691500902, - reportActionTimestamp: 1691500902102, - automatic: false, - actionName: 'ADDCOMMENT', - shouldShow: true, - reportActionID: '4598496324774172433', - previousReportActionID: '3324110555952451144', - lastModified: '2023-08-08 13:21:42.102', - whisperedToAccountIDs: [], - }, - '3324110555952451144': { - person: [ - { - type: 'TEXT', - style: 'strong', - text: 'Hanno J. G\u00f6decke', - }, - ], - actorAccountID: 12883048, - message: [ - { - type: 'COMMENT', - html: 'test \ud83d\uddff', - text: 'test \ud83d\uddff', - isEdited: false, - whisperedTo: [], - isDeletedParentAction: false, - reactions: [], - }, - ], - originalMessage: { - html: 'test \ud83d\uddff', - lastModified: '2023-08-08 13:21:32.101', - }, - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/fc1b8880216a5a76c8fd9998aaa33c080dacda5d_128.jpeg', - created: '2023-08-08 13:21:32.101', - timestamp: 1691500892, - reportActionTimestamp: 1691500892101, - automatic: false, - actionName: 'ADDCOMMENT', - shouldShow: true, - reportActionID: '3324110555952451144', - previousReportActionID: '5389364980227777980', - lastModified: '2023-08-08 13:21:32.101', - whisperedToAccountIDs: [], - }, - '5389364980227777980': { - person: [ - { - type: 'TEXT', - style: 'strong', - text: 'Hanno J. G\u00f6decke', - }, - ], - actorAccountID: 12883048, - message: [ - { - type: 'COMMENT', - html: 'okay now it will work again y \ud83d\udc42', - text: 'okay now it will work again y \ud83d\udc42', - isEdited: false, - whisperedTo: [], - isDeletedParentAction: false, - reactions: [], - }, - ], - originalMessage: { - html: 'okay now it will work again y \ud83d\udc42', - lastModified: '2023-08-07 10:54:38.141', - }, - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/fc1b8880216a5a76c8fd9998aaa33c080dacda5d_128.jpeg', - created: '2023-08-07 10:54:38.141', - timestamp: 1691405678, - reportActionTimestamp: 1691405678141, - automatic: false, - actionName: 'ADDCOMMENT', - shouldShow: true, - reportActionID: '5389364980227777980', - previousReportActionID: '4717622390560689493', - lastModified: '2023-08-07 10:54:38.141', - whisperedToAccountIDs: [], - }, - '4717622390560689493': { - person: [ - { - type: 'TEXT', - style: 'strong', - text: 'Hanno J. G\u00f6decke', - }, - ], - actorAccountID: 12883048, - message: [ - { - type: 'COMMENT', - html: 'hmmmm', - text: 'hmmmm', - isEdited: false, - whisperedTo: [], - isDeletedParentAction: false, - reactions: [], - }, - ], - originalMessage: { - html: 'hmmmm', - lastModified: '2023-07-27 18:13:45.322', - }, - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/fc1b8880216a5a76c8fd9998aaa33c080dacda5d_128.jpeg', - created: '2023-07-27 18:13:45.322', - timestamp: 1690481625, - reportActionTimestamp: 1690481625322, - automatic: false, - actionName: 'ADDCOMMENT', - shouldShow: true, - reportActionID: '4717622390560689493', - previousReportActionID: '745721424446883075', - lastModified: '2023-07-27 18:13:45.322', - whisperedToAccountIDs: [], - }, - '745721424446883075': { - person: [ - { - type: 'TEXT', - style: 'strong', - text: 'Hanno J. G\u00f6decke', - }, - ], - actorAccountID: 12883048, - message: [ - { - type: 'COMMENT', - html: 'test', - text: 'test', - isEdited: false, - whisperedTo: [], - isDeletedParentAction: false, - reactions: [], - }, - ], - originalMessage: { - html: 'test', - lastModified: '2023-07-27 18:13:32.595', - }, - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/fc1b8880216a5a76c8fd9998aaa33c080dacda5d_128.jpeg', - created: '2023-07-27 18:13:32.595', - timestamp: 1690481612, - reportActionTimestamp: 1690481612595, - automatic: false, - actionName: 'ADDCOMMENT', - shouldShow: true, - reportActionID: '745721424446883075', - previousReportActionID: '3986429677777110818', - lastModified: '2023-07-27 18:13:32.595', - whisperedToAccountIDs: [], - }, - '3986429677777110818': { - person: [ - { - type: 'TEXT', - style: 'strong', - text: 'Terry Hightraffic1337', - }, - ], - actorAccountID: 14567013, - message: [ - { - type: 'COMMENT', - html: 'I will', - text: 'I will', - isEdited: false, - whisperedTo: [], - isDeletedParentAction: false, - reactions: [], - }, - ], - originalMessage: { - html: 'I will', - lastModified: '2023-07-27 17:03:11.250', - }, - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/49a4c96c366f9a32905b30462f91ea39e5eee5e8_128.jpeg', - created: '2023-07-27 17:03:11.250', - timestamp: 1690477391, - reportActionTimestamp: 1690477391250, - automatic: false, - actionName: 'ADDCOMMENT', - shouldShow: true, - reportActionID: '3986429677777110818', - previousReportActionID: '7317910228472011573', - lastModified: '2023-07-27 17:03:11.250', - childReportID: '3338245207149134', - childType: 'chat', - whisperedToAccountIDs: [], - }, - '7317910228472011573': { - person: [ - { - type: 'TEXT', - style: 'strong', - text: 'Hanno J. G\u00f6decke', - }, - ], - actorAccountID: 12883048, - message: [ - { - type: 'COMMENT', - html: 'will you>', - text: 'will you>', - isEdited: false, - whisperedTo: [], - isDeletedParentAction: false, - reactions: [], - }, - ], - originalMessage: { - html: 'will you>', - lastModified: '2023-07-27 16:46:58.988', - }, - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/fc1b8880216a5a76c8fd9998aaa33c080dacda5d_128.jpeg', - created: '2023-07-27 16:46:58.988', - timestamp: 1690476418, - reportActionTimestamp: 1690476418988, - automatic: false, - actionName: 'ADDCOMMENT', - shouldShow: true, - reportActionID: '7317910228472011573', - previousReportActionID: '6779343397958390319', - lastModified: '2023-07-27 16:46:58.988', - whisperedToAccountIDs: [], - }, - '6779343397958390319': { - person: [ - { - type: 'TEXT', - style: 'strong', - text: 'Terry Hightraffic1337', - }, - ], - actorAccountID: 14567013, - message: [ - { - type: 'COMMENT', - html: 'i will always send :#', - text: 'i will always send :#', - isEdited: false, - whisperedTo: [], - isDeletedParentAction: false, - reactions: [], - }, - ], - originalMessage: { - html: 'i will always send :#', - lastModified: '2023-07-27 07:55:33.468', - }, - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/49a4c96c366f9a32905b30462f91ea39e5eee5e8_128.jpeg', - created: '2023-07-27 07:55:33.468', - timestamp: 1690444533, - reportActionTimestamp: 1690444533468, - automatic: false, - actionName: 'ADDCOMMENT', - shouldShow: true, - reportActionID: '6779343397958390319', - previousReportActionID: '5084145419388195535', - lastModified: '2023-07-27 07:55:33.468', - whisperedToAccountIDs: [], - }, - '5084145419388195535': { - person: [ - { - type: 'TEXT', - style: 'strong', - text: 'Terry Hightraffic1337', - }, - ], - actorAccountID: 14567013, - message: [ - { - type: 'COMMENT', - html: 'new test', - text: 'new test', - isEdited: false, - whisperedTo: [], - isDeletedParentAction: false, - reactions: [], - }, - ], - originalMessage: { - html: 'new test', - lastModified: '2023-07-27 07:55:22.309', - }, - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/49a4c96c366f9a32905b30462f91ea39e5eee5e8_128.jpeg', - created: '2023-07-27 07:55:22.309', - timestamp: 1690444522, - reportActionTimestamp: 1690444522309, - automatic: false, - actionName: 'ADDCOMMENT', - shouldShow: true, - reportActionID: '5084145419388195535', - previousReportActionID: '6742067600980190659', - lastModified: '2023-07-27 07:55:22.309', - whisperedToAccountIDs: [], - }, - '6742067600980190659': { - person: [ - { - type: 'TEXT', - style: 'strong', - text: 'Terry Hightraffic1337', - }, - ], - actorAccountID: 14567013, - message: [ - { - type: 'COMMENT', - html: 'okay good', - text: 'okay good', - isEdited: false, - whisperedTo: [], - isDeletedParentAction: false, - reactions: [], - }, - ], - originalMessage: { - html: 'okay good', - lastModified: '2023-07-27 07:55:15.362', - }, - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/49a4c96c366f9a32905b30462f91ea39e5eee5e8_128.jpeg', - created: '2023-07-27 07:55:15.362', - timestamp: 1690444515, - reportActionTimestamp: 1690444515362, - automatic: false, - actionName: 'ADDCOMMENT', - shouldShow: true, - reportActionID: '6742067600980190659', - previousReportActionID: '7811212427986810247', - lastModified: '2023-07-27 07:55:15.362', - whisperedToAccountIDs: [], - }, - '7811212427986810247': { - person: [ - { - type: 'TEXT', - style: 'strong', - text: 'Terry Hightraffic1337', - }, - ], - actorAccountID: 14567013, - message: [ - { - type: 'COMMENT', - html: 'test 2', - text: 'test 2', - isEdited: false, - whisperedTo: [], - isDeletedParentAction: false, - reactions: [], - }, - ], - originalMessage: { - html: 'test 2', - lastModified: '2023-07-27 07:55:10.629', - }, - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/49a4c96c366f9a32905b30462f91ea39e5eee5e8_128.jpeg', - created: '2023-07-27 07:55:10.629', - timestamp: 1690444510, - reportActionTimestamp: 1690444510629, - automatic: false, - actionName: 'ADDCOMMENT', - shouldShow: true, - reportActionID: '7811212427986810247', - previousReportActionID: '4544757211729131829', - lastModified: '2023-07-27 07:55:10.629', - whisperedToAccountIDs: [], - }, - '4544757211729131829': { - person: [ - { - type: 'TEXT', - style: 'strong', - text: 'Terry Hightraffic1337', - }, - ], - actorAccountID: 14567013, - message: [ - { - type: 'COMMENT', - html: 'new test', - text: 'new test', - isEdited: false, - whisperedTo: [], - isDeletedParentAction: false, - reactions: [], - }, - ], - originalMessage: { - html: 'new test', - lastModified: '2023-07-27 07:53:41.960', - }, - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/49a4c96c366f9a32905b30462f91ea39e5eee5e8_128.jpeg', - created: '2023-07-27 07:53:41.960', - timestamp: 1690444421, - reportActionTimestamp: 1690444421960, - automatic: false, - actionName: 'ADDCOMMENT', - shouldShow: true, - reportActionID: '4544757211729131829', - previousReportActionID: '8290114634148431001', - lastModified: '2023-07-27 07:53:41.960', - whisperedToAccountIDs: [], - }, - '8290114634148431001': { - person: [ - { - type: 'TEXT', - style: 'strong', - text: 'Terry Hightraffic1337', - }, - ], - actorAccountID: 14567013, - message: [ - { - type: 'COMMENT', - html: 'something was real', - text: 'something was real', - isEdited: false, - whisperedTo: [], - isDeletedParentAction: false, - reactions: [], - }, - ], - originalMessage: { - html: 'something was real', - lastModified: '2023-07-27 07:53:27.836', - }, - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/49a4c96c366f9a32905b30462f91ea39e5eee5e8_128.jpeg', - created: '2023-07-27 07:53:27.836', - timestamp: 1690444407, - reportActionTimestamp: 1690444407836, - automatic: false, - actionName: 'ADDCOMMENT', - shouldShow: true, - reportActionID: '8290114634148431001', - previousReportActionID: '5597494166918965742', - lastModified: '2023-07-27 07:53:27.836', - whisperedToAccountIDs: [], - }, - '5597494166918965742': { - person: [ - { - type: 'TEXT', - style: 'strong', - text: 'Terry Hightraffic1337', - }, - ], - actorAccountID: 14567013, - message: [ - { - type: 'COMMENT', - html: 'oida', - text: 'oida', - isEdited: false, - whisperedTo: [], - isDeletedParentAction: false, - reactions: [], - }, - ], - originalMessage: { - html: 'oida', - lastModified: '2023-07-27 07:53:20.783', - }, - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/49a4c96c366f9a32905b30462f91ea39e5eee5e8_128.jpeg', - created: '2023-07-27 07:53:20.783', - timestamp: 1690444400, - reportActionTimestamp: 1690444400783, - automatic: false, - actionName: 'ADDCOMMENT', - shouldShow: true, - reportActionID: '5597494166918965742', - previousReportActionID: '7445709165354739065', - lastModified: '2023-07-27 07:53:20.783', - whisperedToAccountIDs: [], - }, - '7445709165354739065': { - person: [ - { - type: 'TEXT', - style: 'strong', - text: 'Terry Hightraffic1337', - }, - ], - actorAccountID: 14567013, - message: [ - { - type: 'COMMENT', - html: 'test 12', - text: 'test 12', - isEdited: false, - whisperedTo: [], - isDeletedParentAction: false, - reactions: [], - }, - ], - originalMessage: { - html: 'test 12', - lastModified: '2023-07-27 07:53:17.393', - }, - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/49a4c96c366f9a32905b30462f91ea39e5eee5e8_128.jpeg', - created: '2023-07-27 07:53:17.393', - timestamp: 1690444397, - reportActionTimestamp: 1690444397393, - automatic: false, - actionName: 'ADDCOMMENT', - shouldShow: true, - reportActionID: '7445709165354739065', - previousReportActionID: '1985264407541504554', - lastModified: '2023-07-27 07:53:17.393', - whisperedToAccountIDs: [], - }, - '1985264407541504554': { - person: [ - { - type: 'TEXT', - style: 'strong', - text: 'Terry Hightraffic1337', - }, - ], - actorAccountID: 14567013, - message: [ - { - type: 'COMMENT', - html: 'new test', - text: 'new test', - isEdited: false, - whisperedTo: [], - isDeletedParentAction: false, - reactions: [], - }, - ], - originalMessage: { - html: 'new test', - lastModified: '2023-07-27 07:53:07.894', - }, - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/49a4c96c366f9a32905b30462f91ea39e5eee5e8_128.jpeg', - created: '2023-07-27 07:53:07.894', - timestamp: 1690444387, - reportActionTimestamp: 1690444387894, - automatic: false, - actionName: 'ADDCOMMENT', - shouldShow: true, - reportActionID: '1985264407541504554', - previousReportActionID: '6101278009725036288', - lastModified: '2023-07-27 07:53:07.894', - whisperedToAccountIDs: [], - }, - '6101278009725036288': { - person: [ - { - type: 'TEXT', - style: 'strong', - text: 'Terry Hightraffic1337', - }, - ], - actorAccountID: 14567013, - message: [ - { - type: 'COMMENT', - html: 'grrr', - text: 'grrr', - isEdited: false, - whisperedTo: [], - isDeletedParentAction: false, - reactions: [], - }, - ], - originalMessage: { - html: 'grrr', - lastModified: '2023-07-27 07:52:56.421', - }, - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/49a4c96c366f9a32905b30462f91ea39e5eee5e8_128.jpeg', - created: '2023-07-27 07:52:56.421', - timestamp: 1690444376, - reportActionTimestamp: 1690444376421, - automatic: false, - actionName: 'ADDCOMMENT', - shouldShow: true, - reportActionID: '6101278009725036288', - previousReportActionID: '6913024396112106680', - lastModified: '2023-07-27 07:52:56.421', - whisperedToAccountIDs: [], - }, - '6913024396112106680': { - person: [ - { - type: 'TEXT', - style: 'strong', - text: 'Terry Hightraffic1337', - }, - ], - actorAccountID: 14567013, - message: [ - { - type: 'COMMENT', - html: 'ne w test', - text: 'ne w test', - isEdited: false, - whisperedTo: [], - isDeletedParentAction: false, - reactions: [], - }, - ], - originalMessage: { - html: 'ne w test', - lastModified: '2023-07-27 07:52:53.352', - }, - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/49a4c96c366f9a32905b30462f91ea39e5eee5e8_128.jpeg', - created: '2023-07-27 07:52:53.352', - timestamp: 1690444373, - reportActionTimestamp: 1690444373352, - automatic: false, - actionName: 'ADDCOMMENT', - shouldShow: true, - reportActionID: '6913024396112106680', - previousReportActionID: '3663318486255461038', - lastModified: '2023-07-27 07:52:53.352', - whisperedToAccountIDs: [], - }, - '3663318486255461038': { - person: [ - { - type: 'TEXT', - style: 'strong', - text: 'Terry Hightraffic1337', - }, - ], - actorAccountID: 14567013, - message: [ - { - type: 'COMMENT', - html: 'well', - text: 'well', - isEdited: false, - whisperedTo: [], - isDeletedParentAction: false, - reactions: [], - }, - ], - originalMessage: { - html: 'well', - lastModified: '2023-07-27 07:52:47.044', - }, - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/49a4c96c366f9a32905b30462f91ea39e5eee5e8_128.jpeg', - created: '2023-07-27 07:52:47.044', - timestamp: 1690444367, - reportActionTimestamp: 1690444367044, - automatic: false, - actionName: 'ADDCOMMENT', - shouldShow: true, - reportActionID: '3663318486255461038', - previousReportActionID: '6652909175804277965', - lastModified: '2023-07-27 07:52:47.044', - whisperedToAccountIDs: [], - }, - '6652909175804277965': { - person: [ - { - type: 'TEXT', - style: 'strong', - text: 'Terry Hightraffic1337', - }, - ], - actorAccountID: 14567013, - message: [ - { - type: 'COMMENT', - html: 'hu', - text: 'hu', - isEdited: false, - whisperedTo: [], - isDeletedParentAction: false, - reactions: [], - }, - ], - originalMessage: { - html: 'hu', - lastModified: '2023-07-27 07:52:43.489', - }, - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/49a4c96c366f9a32905b30462f91ea39e5eee5e8_128.jpeg', - created: '2023-07-27 07:52:43.489', - timestamp: 1690444363, - reportActionTimestamp: 1690444363489, - automatic: false, - actionName: 'ADDCOMMENT', - shouldShow: true, - reportActionID: '6652909175804277965', - previousReportActionID: '4738491624635492834', - lastModified: '2023-07-27 07:52:43.489', - whisperedToAccountIDs: [], - }, - '4738491624635492834': { - person: [ - { - type: 'TEXT', - style: 'strong', - text: 'Terry Hightraffic1337', - }, - ], - actorAccountID: 14567013, - message: [ - { - type: 'COMMENT', - html: 'test', - text: 'test', - isEdited: false, - whisperedTo: [], - isDeletedParentAction: false, - reactions: [], - }, - ], - originalMessage: { - html: 'test', - lastModified: '2023-07-27 07:52:40.145', - }, - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/49a4c96c366f9a32905b30462f91ea39e5eee5e8_128.jpeg', - created: '2023-07-27 07:52:40.145', - timestamp: 1690444360, - reportActionTimestamp: 1690444360145, - automatic: false, - actionName: 'ADDCOMMENT', - shouldShow: true, - reportActionID: '4738491624635492834', - previousReportActionID: '1621235410433805703', - lastModified: '2023-07-27 07:52:40.145', - whisperedToAccountIDs: [], - }, - '1621235410433805703': { - person: [ - { - type: 'TEXT', - style: 'strong', - text: 'Hanno J. G\u00f6decke', - }, - ], - actorAccountID: 12883048, - message: [ - { - type: 'COMMENT', - html: 'test 4', - text: 'test 4', - isEdited: false, - whisperedTo: [], - isDeletedParentAction: false, - reactions: [], - }, - ], - originalMessage: { - html: 'test 4', - lastModified: '2023-07-27 07:48:36.809', - }, - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/fc1b8880216a5a76c8fd9998aaa33c080dacda5d_128.jpeg', - created: '2023-07-27 07:48:36.809', - timestamp: 1690444116, - reportActionTimestamp: 1690444116809, - automatic: false, - actionName: 'ADDCOMMENT', - shouldShow: true, - reportActionID: '1621235410433805703', - previousReportActionID: '1024550225871474566', - lastModified: '2023-07-27 07:48:36.809', - whisperedToAccountIDs: [], - }, - '1024550225871474566': { - person: [ - { - type: 'TEXT', - style: 'strong', - text: 'Terry Hightraffic1337', - }, - ], - actorAccountID: 14567013, - message: [ - { - type: 'COMMENT', - html: 'test 3', - text: 'test 3', - isEdited: false, - whisperedTo: [], - isDeletedParentAction: false, - reactions: [], - }, - ], - originalMessage: { - html: 'test 3', - lastModified: '2023-07-27 07:48:24.183', - }, - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/49a4c96c366f9a32905b30462f91ea39e5eee5e8_128.jpeg', - created: '2023-07-27 07:48:24.183', - timestamp: 1690444104, - reportActionTimestamp: 1690444104183, - automatic: false, - actionName: 'ADDCOMMENT', - shouldShow: true, - reportActionID: '1024550225871474566', - previousReportActionID: '5598482410513625723', - lastModified: '2023-07-27 07:48:24.183', - whisperedToAccountIDs: [], - }, - '5598482410513625723': { - person: [ - { - type: 'TEXT', - style: 'strong', - text: 'Terry Hightraffic1337', - }, - ], - actorAccountID: 14567013, - message: [ - { - type: 'COMMENT', - html: 'test2', - text: 'test2', - isEdited: false, - whisperedTo: [], - isDeletedParentAction: false, - reactions: [], - }, - ], - originalMessage: { - html: 'test2', - lastModified: '2023-07-27 07:42:25.340', - }, - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/49a4c96c366f9a32905b30462f91ea39e5eee5e8_128.jpeg', - created: '2023-07-27 07:42:25.340', - timestamp: 1690443745, - reportActionTimestamp: 1690443745340, - automatic: false, - actionName: 'ADDCOMMENT', - shouldShow: true, - reportActionID: '5598482410513625723', - previousReportActionID: '115121137377026405', - lastModified: '2023-07-27 07:42:25.340', - whisperedToAccountIDs: [], - }, - '115121137377026405': { - person: [ - { - type: 'TEXT', - style: 'strong', - text: 'Hanno J. G\u00f6decke', - }, - ], - actorAccountID: 12883048, - message: [ - { - type: 'COMMENT', - html: 'test', - text: 'test', - isEdited: false, - whisperedTo: [], - isDeletedParentAction: false, - reactions: [], - }, - ], - originalMessage: { - html: 'test', - lastModified: '2023-07-27 07:42:22.583', - }, - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/fc1b8880216a5a76c8fd9998aaa33c080dacda5d_128.jpeg', - created: '2023-07-27 07:42:22.583', - timestamp: 1690443742, - reportActionTimestamp: 1690443742583, - automatic: false, - actionName: 'ADDCOMMENT', - shouldShow: true, - reportActionID: '115121137377026405', - previousReportActionID: '2167420855737359171', - lastModified: '2023-07-27 07:42:22.583', - whisperedToAccountIDs: [], - }, - '2167420855737359171': { - person: [ - { - type: 'TEXT', - style: 'strong', - text: 'Terry Hightraffic1337', - }, - ], - actorAccountID: 14567013, - message: [ - { - type: 'COMMENT', - html: 'new message', - text: 'new message', - isEdited: false, - whisperedTo: [], - isDeletedParentAction: false, - reactions: [], - }, - ], - originalMessage: { - html: 'new message', - lastModified: '2023-07-27 07:42:09.177', - }, - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/49a4c96c366f9a32905b30462f91ea39e5eee5e8_128.jpeg', - created: '2023-07-27 07:42:09.177', - timestamp: 1690443729, - reportActionTimestamp: 1690443729177, - automatic: false, - actionName: 'ADDCOMMENT', - shouldShow: true, - reportActionID: '2167420855737359171', - previousReportActionID: '6106926938128802897', - lastModified: '2023-07-27 07:42:09.177', - whisperedToAccountIDs: [], - }, - '6106926938128802897': { - person: [ - { - type: 'TEXT', - style: 'strong', - text: 'Terry Hightraffic1337', - }, - ], - actorAccountID: 14567013, - message: [ - { - type: 'COMMENT', - html: 'oh', - text: 'oh', - isEdited: false, - whisperedTo: [], - isDeletedParentAction: false, - reactions: [], - }, - ], - originalMessage: { - html: 'oh', - lastModified: '2023-07-27 07:42:03.902', - }, - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/49a4c96c366f9a32905b30462f91ea39e5eee5e8_128.jpeg', - created: '2023-07-27 07:42:03.902', - timestamp: 1690443723, - reportActionTimestamp: 1690443723902, - automatic: false, - actionName: 'ADDCOMMENT', - shouldShow: true, - reportActionID: '6106926938128802897', - previousReportActionID: '4366704007455141347', - lastModified: '2023-07-27 07:42:03.902', - whisperedToAccountIDs: [], - }, - '4366704007455141347': { - person: [ - { - type: 'TEXT', - style: 'strong', - text: 'Terry Hightraffic1337', - }, - ], - actorAccountID: 14567013, - message: [ - { - type: 'COMMENT', - html: 'hm lol', - text: 'hm lol', - isEdited: false, - whisperedTo: [], - isDeletedParentAction: false, - reactions: [], - }, - ], - originalMessage: { - html: 'hm lol', - lastModified: '2023-07-27 07:42:00.734', - }, - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/49a4c96c366f9a32905b30462f91ea39e5eee5e8_128.jpeg', - created: '2023-07-27 07:42:00.734', - timestamp: 1690443720, - reportActionTimestamp: 1690443720734, - automatic: false, - actionName: 'ADDCOMMENT', - shouldShow: true, - reportActionID: '4366704007455141347', - previousReportActionID: '2078794664797360607', - lastModified: '2023-07-27 07:42:00.734', - whisperedToAccountIDs: [], - }, - '2078794664797360607': { - person: [ - { - type: 'TEXT', - style: 'strong', - text: 'Terry Hightraffic1337', - }, - ], - actorAccountID: 14567013, - message: [ - { - type: 'COMMENT', - html: 'hi?', - text: 'hi?', - isEdited: false, - whisperedTo: [], - isDeletedParentAction: false, - reactions: [], - }, - ], - originalMessage: { - html: 'hi?', - lastModified: '2023-07-27 07:41:49.724', - }, - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/49a4c96c366f9a32905b30462f91ea39e5eee5e8_128.jpeg', - created: '2023-07-27 07:41:49.724', - timestamp: 1690443709, - reportActionTimestamp: 1690443709724, - automatic: false, - actionName: 'ADDCOMMENT', - shouldShow: true, - reportActionID: '2078794664797360607', - previousReportActionID: '2030060194258527427', - lastModified: '2023-07-27 07:41:49.724', - whisperedToAccountIDs: [], - }, - '2030060194258527427': { - person: [ - { - type: 'TEXT', - style: 'strong', - text: 'Hanno J. G\u00f6decke', - }, - ], - actorAccountID: 12883048, - message: [ - { - type: 'COMMENT', - html: 'lets have a thread about it, will ya?', - text: 'lets have a thread about it, will ya?', - isEdited: false, - whisperedTo: [], - isDeletedParentAction: false, - reactions: [], - }, - ], - originalMessage: { - html: 'lets have a thread about it, will ya?', - lastModified: '2023-07-27 07:40:49.146', - }, - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/fc1b8880216a5a76c8fd9998aaa33c080dacda5d_128.jpeg', - created: '2023-07-27 07:40:49.146', - timestamp: 1690443649, - reportActionTimestamp: 1690443649146, - automatic: false, - actionName: 'ADDCOMMENT', - shouldShow: true, - reportActionID: '2030060194258527427', - previousReportActionID: '5540483153987237906', - lastModified: '2023-07-27 07:40:49.146', - childReportID: '5860710623453234', - childType: 'chat', - childOldestFourAccountIDs: '14567013,12883048', - childCommenterCount: 2, - childLastVisibleActionCreated: '2023-07-27 07:41:03.550', - childVisibleActionCount: 2, - whisperedToAccountIDs: [], - }, - '5540483153987237906': { - person: [ - { - type: 'TEXT', - style: 'strong', - text: 'Terry Hightraffic1337', - }, - ], - actorAccountID: 14567013, - message: [ - { - type: 'COMMENT', - html: '@hanno@margelo.io i mention you lasagna :)', - text: '@hanno@margelo.io i mention you lasagna :)', - isEdited: false, - whisperedTo: [], - isDeletedParentAction: false, - reactions: [], - }, - ], - originalMessage: { - html: '@hanno@margelo.io i mention you lasagna :)', - lastModified: '2023-07-27 07:37:43.100', - }, - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/49a4c96c366f9a32905b30462f91ea39e5eee5e8_128.jpeg', - created: '2023-07-27 07:37:43.100', - timestamp: 1690443463, - reportActionTimestamp: 1690443463100, - automatic: false, - actionName: 'ADDCOMMENT', - shouldShow: true, - reportActionID: '5540483153987237906', - previousReportActionID: '8050559753491913991', - lastModified: '2023-07-27 07:37:43.100', - whisperedToAccountIDs: [], - }, - '8050559753491913991': { - person: [ - { - type: 'TEXT', - style: 'strong', - text: 'Terry Hightraffic1337', - }, - ], - actorAccountID: 14567013, - message: [ - { - type: 'COMMENT', - html: '@terry+hightraffic@margelo.io', - text: '@terry+hightraffic@margelo.io', - isEdited: false, - whisperedTo: [], - isDeletedParentAction: false, - reactions: [], - }, - ], - originalMessage: { - html: '@terry+hightraffic@margelo.io', - lastModified: '2023-07-27 07:36:41.708', - }, - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/49a4c96c366f9a32905b30462f91ea39e5eee5e8_128.jpeg', - created: '2023-07-27 07:36:41.708', - timestamp: 1690443401, - reportActionTimestamp: 1690443401708, - automatic: false, - actionName: 'ADDCOMMENT', - shouldShow: true, - reportActionID: '8050559753491913991', - previousReportActionID: '881015235172878574', - lastModified: '2023-07-27 07:36:41.708', - whisperedToAccountIDs: [], - }, - '881015235172878574': { - person: [ - { - type: 'TEXT', - style: 'strong', - text: 'Terry Hightraffic1337', - }, - ], - actorAccountID: 14567013, - message: [ - { - type: 'COMMENT', - html: 'yeah lets see', - text: 'yeah lets see', - isEdited: false, - whisperedTo: [], - isDeletedParentAction: false, - reactions: [], - }, - ], - originalMessage: { - html: 'yeah lets see', - lastModified: '2023-07-27 07:25:15.997', - }, - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/49a4c96c366f9a32905b30462f91ea39e5eee5e8_128.jpeg', - created: '2023-07-27 07:25:15.997', - timestamp: 1690442715, - reportActionTimestamp: 1690442715997, - automatic: false, - actionName: 'ADDCOMMENT', - shouldShow: true, - reportActionID: '881015235172878574', - previousReportActionID: '4800357767877651330', - lastModified: '2023-07-27 07:25:15.997', - whisperedToAccountIDs: [], - }, - '4800357767877651330': { - person: [ - { - type: 'TEXT', - style: 'strong', - text: 'Hanno J. G\u00f6decke', - }, - ], - actorAccountID: 12883048, - message: [ - { - type: 'COMMENT', - html: 'asdasdasd', - text: 'asdasdasd', - isEdited: false, - whisperedTo: [], - isDeletedParentAction: false, - reactions: [], - }, - ], - originalMessage: { - html: 'asdasdasd', - lastModified: '2023-07-27 07:25:03.093', - }, - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/fc1b8880216a5a76c8fd9998aaa33c080dacda5d_128.jpeg', - created: '2023-07-27 07:25:03.093', - timestamp: 1690442703, - reportActionTimestamp: 1690442703093, - automatic: false, - actionName: 'ADDCOMMENT', - shouldShow: true, - reportActionID: '4800357767877651330', - previousReportActionID: '9012557872554910346', - lastModified: '2023-07-27 07:25:03.093', - whisperedToAccountIDs: [], - }, - '9012557872554910346': { - person: [ - { - type: 'TEXT', - style: 'strong', - text: 'Terry Hightraffic1337', - }, - ], - actorAccountID: 14567013, - message: [ - { - type: 'COMMENT', - html: 'yeah', - text: 'yeah', - isEdited: false, - whisperedTo: [], - isDeletedParentAction: false, - reactions: [], - }, - ], - originalMessage: { - html: 'yeah', - lastModified: '2023-07-26 19:49:40.471', - }, - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/49a4c96c366f9a32905b30462f91ea39e5eee5e8_128.jpeg', - created: '2023-07-26 19:49:40.471', - timestamp: 1690400980, - reportActionTimestamp: 1690400980471, - automatic: false, - actionName: 'ADDCOMMENT', - shouldShow: true, - reportActionID: '9012557872554910346', - previousReportActionID: '8440677969068645500', - lastModified: '2023-07-26 19:49:40.471', - whisperedToAccountIDs: [], - }, - '8440677969068645500': { - person: [ - { - type: 'TEXT', - style: 'strong', - text: 'Hanno J. G\u00f6decke', - }, - ], - actorAccountID: 12883048, - message: [ - { - type: 'COMMENT', - html: 'hello motor', - text: 'hello motor', - isEdited: false, - whisperedTo: [], - isDeletedParentAction: false, - reactions: [], - }, - ], - originalMessage: { - html: 'hello motor', - lastModified: '2023-07-26 19:49:36.262', - }, - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/fc1b8880216a5a76c8fd9998aaa33c080dacda5d_128.jpeg', - created: '2023-07-26 19:49:36.262', - timestamp: 1690400976, - reportActionTimestamp: 1690400976262, - automatic: false, - actionName: 'ADDCOMMENT', - shouldShow: true, - reportActionID: '8440677969068645500', - previousReportActionID: '306887996337608775', - lastModified: '2023-07-26 19:49:36.262', - whisperedToAccountIDs: [], - }, - '306887996337608775': { - person: [ - { - type: 'TEXT', - style: 'strong', - text: 'Hanno J. G\u00f6decke', - }, - ], - actorAccountID: 12883048, - message: [ - { - type: 'COMMENT', - html: 'a new messagfe', - text: 'a new messagfe', - isEdited: false, - whisperedTo: [], - isDeletedParentAction: false, - reactions: [], - }, - ], - originalMessage: { - html: 'a new messagfe', - lastModified: '2023-07-26 19:49:29.512', - }, - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/fc1b8880216a5a76c8fd9998aaa33c080dacda5d_128.jpeg', - created: '2023-07-26 19:49:29.512', - timestamp: 1690400969, - reportActionTimestamp: 1690400969512, - automatic: false, - actionName: 'ADDCOMMENT', - shouldShow: true, - reportActionID: '306887996337608775', - previousReportActionID: '587892433077506227', - lastModified: '2023-07-26 19:49:29.512', - whisperedToAccountIDs: [], - }, - '587892433077506227': { - person: [ - { - type: 'TEXT', - style: 'strong', - text: 'Hanno J. G\u00f6decke', - }, - ], - actorAccountID: 12883048, - message: [ - { - type: 'COMMENT', - html: 'good', - text: 'good', - isEdited: false, - whisperedTo: [], - isDeletedParentAction: false, - reactions: [], - }, - ], - originalMessage: { - html: 'good', - lastModified: '2023-07-26 19:49:20.473', - }, - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/fc1b8880216a5a76c8fd9998aaa33c080dacda5d_128.jpeg', - created: '2023-07-26 19:49:20.473', - timestamp: 1690400960, - reportActionTimestamp: 1690400960473, - automatic: false, - actionName: 'ADDCOMMENT', - shouldShow: true, - reportActionID: '587892433077506227', - previousReportActionID: '1433103421804347060', - lastModified: '2023-07-26 19:49:20.473', - whisperedToAccountIDs: [], - }, - '1433103421804347060': { - person: [ - { - type: 'TEXT', - style: 'strong', - text: 'Terry Hightraffic1337', - }, - ], - actorAccountID: 14567013, - message: [ - { - type: 'COMMENT', - html: 'ah', - text: 'ah', - isEdited: false, - whisperedTo: [], - isDeletedParentAction: false, - reactions: [], - }, - ], - originalMessage: { - html: 'ah', - lastModified: '2023-07-26 19:49:12.762', - }, - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/49a4c96c366f9a32905b30462f91ea39e5eee5e8_128.jpeg', - created: '2023-07-26 19:49:12.762', - timestamp: 1690400952, - reportActionTimestamp: 1690400952762, - automatic: false, - actionName: 'ADDCOMMENT', - shouldShow: true, - reportActionID: '1433103421804347060', - previousReportActionID: '8774157052628183778', - lastModified: '2023-07-26 19:49:12.762', - whisperedToAccountIDs: [], - }, - }, - }, - { - onyxMethod: 'mergecollection', - key: 'reportActionsReactions_', - value: { - reportActionsReactions_2658221912430757962: { - heart: { - createdAt: '2023-08-25 12:37:45', - users: { - 12883048: { - skinTones: { - '-1': '2023-08-25 12:37:45', - }, - }, - }, - }, - }, - }, - }, - { - onyxMethod: 'merge', - key: 'personalDetailsList', - value: { - 14567013: { - accountID: 14567013, - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/49a4c96c366f9a32905b30462f91ea39e5eee5e8_128.jpeg', - displayName: 'Terry Hightraffic1337', - firstName: 'Terry', - lastName: 'Hightraffic1337', - status: null, - login: 'terry+hightraffic@margelo.io', - pronouns: '', - timezone: { - automatic: true, - selected: 'Europe/Kyiv', - }, - phoneNumber: '', - validated: true, - }, - }, - }, - ], - jsonCode: 200, - requestID: '81b8b8509a7f5b54-VIE', -}); diff --git a/src/libs/E2E/apiMocks/readNewestAction.ts b/src/libs/E2E/apiMocks/readNewestAction.ts deleted file mode 100644 index eb3800a98b81..000000000000 --- a/src/libs/E2E/apiMocks/readNewestAction.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type Response from '@src/types/onyx/Response'; - -export default (): Response => ({ - jsonCode: 200, - requestID: '81b8c48e3bfe5a84-VIE', - onyxData: [ - { - onyxMethod: 'merge', - key: 'report_98345625', - value: { - lastReadTime: '2023-10-25 07:32:48.915', - }, - }, - ], -}); diff --git a/src/libs/E2E/apiMocks/signinUser.ts b/src/libs/E2E/apiMocks/signinUser.ts deleted file mode 100644 index 7063e56f94be..000000000000 --- a/src/libs/E2E/apiMocks/signinUser.ts +++ /dev/null @@ -1,53 +0,0 @@ -import type {SigninParams} from '@libs/E2E/types'; -import type Response from '@src/types/onyx/Response'; - -const signinUser = ({email}: SigninParams): Response => ({ - onyxData: [ - { - onyxMethod: 'merge', - key: 'session', - value: { - authToken: 'fakeAuthToken', - accountID: 12313081, - email, - encryptedAuthToken: 'fakeEncryptedAuthToken', - }, - }, - { - onyxMethod: 'set', - key: 'shouldShowComposeInput', - value: true, - }, - { - onyxMethod: 'merge', - key: 'credentials', - value: { - autoGeneratedLogin: 'fake', - autoGeneratedPassword: 'fake', - }, - }, - { - onyxMethod: 'merge', - key: 'user', - value: { - isUsingExpensifyCard: false, - }, - }, - { - onyxMethod: 'set', - key: 'betas', - value: ['all'], - }, - { - onyxMethod: 'merge', - key: 'account', - value: { - requiresTwoFactorAuth: false, - }, - }, - ], - jsonCode: 200, - requestID: '783e5f3cadfbcfc0-SJC', -}); - -export default signinUser; From 43a77afd47466250a642b479358a2622e5266a69 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Thu, 4 Jan 2024 10:17:52 +0100 Subject: [PATCH 093/391] Fix lint errors --- src/components/StatePicker/StateSelectorModal.tsx | 4 ++-- src/components/StatePicker/index.tsx | 6 ++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/components/StatePicker/StateSelectorModal.tsx b/src/components/StatePicker/StateSelectorModal.tsx index 2871c2ebdaf5..5be88a77f887 100644 --- a/src/components/StatePicker/StateSelectorModal.tsx +++ b/src/components/StatePicker/StateSelectorModal.tsx @@ -6,7 +6,8 @@ import ScreenWrapper from '@components/ScreenWrapper'; import SelectionList from '@components/SelectionList'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; -import searchCountryOptions, {type CountryData} from '@libs/searchCountryOptions'; +import searchCountryOptions from '@libs/searchCountryOptions'; +import type {CountryData} from '@libs/searchCountryOptions'; import StringUtils from '@libs/StringUtils'; import CONST from '@src/CONST'; @@ -74,7 +75,6 @@ function StateSelectorModal({currentState, isVisible, onClose = () => {}, onStat hideModalContentWhileAnimating useNativeDriver > - {/* @ts-expect-error TODO: Remove this once ScreenWrapper (https://github.com/Expensify/App/issues/25128) is migrated to TypeScript. */} Date: Thu, 4 Jan 2024 12:02:25 +0100 Subject: [PATCH 094/391] Fix type imports --- src/components/Form/FormContext.tsx | 2 +- src/components/Form/FormProvider.tsx | 12 +++++++----- src/components/Form/FormWrapper.tsx | 21 ++++++++++++--------- src/components/Form/InputWrapper.tsx | 2 +- src/components/Form/types.ts | 8 ++++---- 5 files changed, 25 insertions(+), 20 deletions(-) diff --git a/src/components/Form/FormContext.tsx b/src/components/Form/FormContext.tsx index dcc8f3b516de..47e0de8b497c 100644 --- a/src/components/Form/FormContext.tsx +++ b/src/components/Form/FormContext.tsx @@ -1,5 +1,5 @@ import {createContext} from 'react'; -import {RegisterInput} from './types'; +import type {RegisterInput} from './types'; type FormContext = { registerInput: RegisterInput; diff --git a/src/components/Form/FormProvider.tsx b/src/components/Form/FormProvider.tsx index f0789ef6429e..23f24abc59f0 100644 --- a/src/components/Form/FormProvider.tsx +++ b/src/components/Form/FormProvider.tsx @@ -1,17 +1,19 @@ import lodashIsEqual from 'lodash/isEqual'; -import React, {createRef, ForwardedRef, forwardRef, ReactNode, useCallback, useImperativeHandle, useMemo, useRef, useState} from 'react'; -import {OnyxEntry, withOnyx} from 'react-native-onyx'; +import type {ForwardedRef, ReactNode} from 'react'; +import React, {createRef, forwardRef, useCallback, useImperativeHandle, useMemo, useRef, useState} from 'react'; +import type {OnyxEntry} from 'react-native-onyx'; +import {withOnyx} from 'react-native-onyx'; import * as ValidationUtils from '@libs/ValidationUtils'; import Visibility from '@libs/Visibility'; import * as FormActions from '@userActions/FormActions'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import {Form, Network} from '@src/types/onyx'; -import {Errors} from '@src/types/onyx/OnyxCommon'; +import type {Form, Network} from '@src/types/onyx'; +import type {Errors} from '@src/types/onyx/OnyxCommon'; import {isEmptyObject, isNotEmptyObject} from '@src/types/utils/EmptyObject'; import FormContext from './FormContext'; import FormWrapper from './FormWrapper'; -import {FormProps, FormValuesFields, InputRef, InputRefs, OnyxFormKeyWithoutDraft, RegisterInput, ValueType} from './types'; +import type {FormProps, FormValuesFields, InputRef, InputRefs, OnyxFormKeyWithoutDraft, RegisterInput, ValueType} from './types'; // In order to prevent Checkbox focus loss when the user are focusing a TextInput and proceeds to toggle a CheckBox in web and mobile web. // 200ms delay was chosen as a result of empirical testing. diff --git a/src/components/Form/FormWrapper.tsx b/src/components/Form/FormWrapper.tsx index f1071bf8d759..306afc10836f 100644 --- a/src/components/Form/FormWrapper.tsx +++ b/src/components/Form/FormWrapper.tsx @@ -1,19 +1,22 @@ -import React, {MutableRefObject, useCallback, useMemo, useRef} from 'react'; -import {Keyboard, ScrollView, StyleProp, View, ViewStyle} from 'react-native'; -import {OnyxEntry, withOnyx} from 'react-native-onyx'; +import type {MutableRefObject} from 'react'; +import React, {useCallback, useMemo, useRef} from 'react'; +import type {StyleProp, View, ViewStyle} from 'react-native'; +import {Keyboard, ScrollView} from 'react-native'; +import type {OnyxEntry} from 'react-native-onyx'; +import {withOnyx} from 'react-native-onyx'; import FormAlertWithSubmitButton from '@components/FormAlertWithSubmitButton'; import FormSubmit from '@components/FormSubmit'; import SafeAreaConsumer from '@components/SafeAreaConsumer'; -import {SafeAreaChildrenProps} from '@components/SafeAreaConsumer/types'; +import type {SafeAreaChildrenProps} from '@components/SafeAreaConsumer/types'; import ScrollViewWithContext from '@components/ScrollViewWithContext'; import useThemeStyles from '@hooks/useThemeStyles'; import * as ErrorUtils from '@libs/ErrorUtils'; -import ONYXKEYS from '@src/ONYXKEYS'; -import {Form} from '@src/types/onyx'; -import {Errors} from '@src/types/onyx/OnyxCommon'; -import ChildrenProps from '@src/types/utils/ChildrenProps'; +import type ONYXKEYS from '@src/ONYXKEYS'; +import type {Form} from '@src/types/onyx'; +import type {Errors} from '@src/types/onyx/OnyxCommon'; +import type ChildrenProps from '@src/types/utils/ChildrenProps'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; -import {FormProps, InputRefs} from './types'; +import type {FormProps, InputRefs} from './types'; type FormWrapperOnyxProps = { /** Contains the form state that must be accessed outside the component */ diff --git a/src/components/Form/InputWrapper.tsx b/src/components/Form/InputWrapper.tsx index 579dd553afaa..9a29c1aa8762 100644 --- a/src/components/Form/InputWrapper.tsx +++ b/src/components/Form/InputWrapper.tsx @@ -1,7 +1,7 @@ import React, {forwardRef, useContext} from 'react'; import TextInput from '@components/TextInput'; import FormContext from './FormContext'; -import {InputProps, InputRef, InputWrapperProps} from './types'; +import type {InputProps, InputRef, InputWrapperProps} from './types'; function InputWrapper({InputComponent, inputID, valueType = 'string', ...rest}: InputWrapperProps, ref: InputRef) { const {registerInput} = useContext(FormContext); diff --git a/src/components/Form/types.ts b/src/components/Form/types.ts index d7662d1efc83..fc3d5c46532f 100644 --- a/src/components/Form/types.ts +++ b/src/components/Form/types.ts @@ -1,7 +1,7 @@ -import {ComponentType, ForwardedRef, ForwardRefExoticComponent, ReactNode, SyntheticEvent} from 'react'; -import {GestureResponderEvent, StyleProp, TextInput, ViewStyle} from 'react-native'; -import {OnyxFormKey} from '@src/ONYXKEYS'; -import {Form} from '@src/types/onyx'; +import type {ComponentType, ForwardedRef, ForwardRefExoticComponent, ReactNode, SyntheticEvent} from 'react'; +import type {GestureResponderEvent, StyleProp, TextInput, ViewStyle} from 'react-native'; +import type {OnyxFormKey} from '@src/ONYXKEYS'; +import type {Form} from '@src/types/onyx'; type ValueType = 'string' | 'boolean' | 'date'; From c09b5d8f79092068b92740c10083d086eba38577 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Thu, 4 Jan 2024 14:02:34 +0100 Subject: [PATCH 095/391] WIP: Improve Form types --- src/ONYXKEYS.ts | 10 +++--- src/components/Form/FormProvider.tsx | 53 ++++++++++++++------------- src/components/Form/InputWrapper.tsx | 6 ++-- src/components/Form/types.ts | 54 ++++++++++------------------ 4 files changed, 54 insertions(+), 69 deletions(-) diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index e7de2039c8f1..c29a2a74a37a 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -408,8 +408,8 @@ type OnyxValues = { [ONYXKEYS.CARD_LIST]: Record; [ONYXKEYS.WALLET_STATEMENT]: OnyxTypes.WalletStatement; [ONYXKEYS.PERSONAL_BANK_ACCOUNT]: OnyxTypes.PersonalBankAccount; - [ONYXKEYS.REIMBURSEMENT_ACCOUNT]: OnyxTypes.ReimbursementAccount; - [ONYXKEYS.REIMBURSEMENT_ACCOUNT_DRAFT]: OnyxTypes.ReimbursementAccountDraft; + [ONYXKEYS.REIMBURSEMENT_ACCOUNT]: OnyxTypes.ReimbursementAccount & OnyxTypes.Form; + [ONYXKEYS.REIMBURSEMENT_ACCOUNT_DRAFT]: OnyxTypes.ReimbursementAccountDraft & OnyxTypes.Form; [ONYXKEYS.PREFERRED_EMOJI_SKIN_TONE]: string | number; [ONYXKEYS.FREQUENTLY_USED_EMOJIS]: OnyxTypes.FrequentlyUsedEmoji[]; [ONYXKEYS.REIMBURSEMENT_ACCOUNT_WORKSPACE_ID]: string; @@ -474,8 +474,8 @@ type OnyxValues = { [ONYXKEYS.FORMS.CLOSE_ACCOUNT_FORM_DRAFT]: OnyxTypes.Form; [ONYXKEYS.FORMS.PROFILE_SETTINGS_FORM]: OnyxTypes.Form; [ONYXKEYS.FORMS.PROFILE_SETTINGS_FORM_DRAFT]: OnyxTypes.Form; - [ONYXKEYS.FORMS.DISPLAY_NAME_FORM]: OnyxTypes.Form; - [ONYXKEYS.FORMS.DISPLAY_NAME_FORM_DRAFT]: OnyxTypes.Form; + [ONYXKEYS.FORMS.DISPLAY_NAME_FORM]: OnyxTypes.Form & {firstName: string; lastName: string}; + [ONYXKEYS.FORMS.DISPLAY_NAME_FORM_DRAFT]: OnyxTypes.Form & {firstName: string; lastName: string}; [ONYXKEYS.FORMS.ROOM_NAME_FORM]: OnyxTypes.Form; [ONYXKEYS.FORMS.ROOM_NAME_FORM_DRAFT]: OnyxTypes.Form; [ONYXKEYS.FORMS.WELCOME_MESSAGE_FORM]: OnyxTypes.Form; @@ -531,4 +531,4 @@ type OnyxValues = { type OnyxKeyValue = OnyxEntry; export default ONYXKEYS; -export type {OnyxKey, OnyxCollectionKey, OnyxValues, OnyxKeyValue, OnyxFormKey, OnyxKeysMap}; +export type {OnyxKey, FormTest, OnyxCollectionKey, OnyxValues, OnyxKeyValue, OnyxFormKey, OnyxKeysMap}; diff --git a/src/components/Form/FormProvider.tsx b/src/components/Form/FormProvider.tsx index 23f24abc59f0..c4db7fcec290 100644 --- a/src/components/Form/FormProvider.tsx +++ b/src/components/Form/FormProvider.tsx @@ -1,19 +1,20 @@ import lodashIsEqual from 'lodash/isEqual'; -import type {ForwardedRef, ReactNode} from 'react'; import React, {createRef, forwardRef, useCallback, useImperativeHandle, useMemo, useRef, useState} from 'react'; +import type {ForwardedRef, ReactNode} from 'react'; import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import * as ValidationUtils from '@libs/ValidationUtils'; import Visibility from '@libs/Visibility'; import * as FormActions from '@userActions/FormActions'; import CONST from '@src/CONST'; +import type {OnyxFormKey} from '@src/ONYXKEYS'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Form, Network} from '@src/types/onyx'; import type {Errors} from '@src/types/onyx/OnyxCommon'; import {isEmptyObject, isNotEmptyObject} from '@src/types/utils/EmptyObject'; import FormContext from './FormContext'; import FormWrapper from './FormWrapper'; -import type {FormProps, FormValuesFields, InputRef, InputRefs, OnyxFormKeyWithoutDraft, RegisterInput, ValueType} from './types'; +import type {FormProps, InputRef, InputRefs, OnyxFormKeyWithoutDraft, OnyxFormValues, OnyxFormValuesFields, RegisterInput, ValueType} from './types'; // In order to prevent Checkbox focus loss when the user are focusing a TextInput and proceeds to toggle a CheckBox in web and mobile web. // 200ms delay was chosen as a result of empirical testing. @@ -35,24 +36,26 @@ function getInitialValueByType(valueType?: ValueType): InitialDefaultValue { } } -type FormProviderOnyxProps = { +type GenericFormValues = Form & Record; + +type FormProviderOnyxProps = { /** Contains the form state that must be accessed outside the component */ - formState: OnyxEntry; + formState: OnyxEntry; /** Contains draft values for each input in the form */ - draftValues: OnyxEntry; + draftValues: OnyxEntry; /** Information about the network */ network: OnyxEntry; }; -type FormProviderProps = FormProviderOnyxProps & - FormProps & { +type FormProviderProps = FormProviderOnyxProps & + FormProps & { /** Children to render. */ - children: ((props: {inputValues: TForm}) => ReactNode) | ReactNode; + children: ((props: {inputValues: OnyxFormValues}) => ReactNode) | ReactNode; /** Callback to validate the form */ - validate?: (values: FormValuesFields) => Errors; + validate?: (values: OnyxFormValuesFields) => Errors; /** Should validate function be called when input loose focus */ shouldValidateOnBlur?: boolean; @@ -61,11 +64,11 @@ type FormProviderProps = FormProviderOnyxProps & shouldValidateOnChange?: boolean; }; -type FormRef = { - resetForm: (optionalValue: TForm) => void; +type FormRef = { + resetForm: (optionalValue: OnyxFormValues) => void; }; -function FormProvider( +function FormProvider( { formID, validate, @@ -78,18 +81,18 @@ function FormProvider( draftValues, onSubmit, ...rest - }: FormProviderProps>, - forwardedRef: ForwardedRef>, + }: FormProviderProps, + forwardedRef: ForwardedRef, ) { const inputRefs = useRef({}); const touchedInputs = useRef>({}); - const [inputValues, setInputValues] = useState>(() => ({...draftValues})); + const [inputValues, setInputValues] = useState(() => ({...draftValues})); const [errors, setErrors] = useState({}); const hasServerError = useMemo(() => !!formState && !isEmptyObject(formState?.errors), [formState]); const onValidate = useCallback( - (values: FormValuesFields>, shouldClearServerError = true) => { - const trimmedStringValues = ValidationUtils.prepareValues(values) as FormValuesFields>; + (values: OnyxFormValuesFields, shouldClearServerError = true) => { + const trimmedStringValues = ValidationUtils.prepareValues(values) as OnyxFormValuesFields; if (shouldClearServerError) { FormActions.setErrors(formID, null); @@ -163,7 +166,7 @@ function FormProvider( } // Prepare values before submitting - const trimmedStringValues = ValidationUtils.prepareValues(inputValues) as FormValuesFields; + const trimmedStringValues = ValidationUtils.prepareValues(inputValues); // Touches all form inputs, so we can validate the entire form Object.keys(inputRefs.current).forEach((inputID) => (touchedInputs.current[inputID] = true)); @@ -182,7 +185,7 @@ function FormProvider( }, [enabledWhenOffline, formState?.isLoading, inputValues, network?.isOffline, onSubmit, onValidate]); const resetForm = useCallback( - (optionalValue: FormValuesFields>) => { + (optionalValue: GenericFormValues) => { Object.keys(inputValues).forEach((inputID) => { setInputValues((prevState) => { const copyPrevState = {...prevState}; @@ -342,16 +345,16 @@ function FormProvider( FormProvider.displayName = 'Form'; -export default withOnyx, FormProviderOnyxProps>({ +export default withOnyx({ network: { key: ONYXKEYS.NETWORK, }, formState: { - key: ({formID}) => formID as typeof ONYXKEYS.FORMS.EDIT_TASK_FORM, + // @ts-expect-error TODO: fix this + key: ({formID}) => formID, }, draftValues: { - key: (props) => `${props.formID}Draft` as typeof ONYXKEYS.FORMS.EDIT_TASK_FORM_DRAFT, + // @ts-expect-error TODO: fix this + key: (props) => `${props.formID}Draft` as const, }, -})(forwardRef(FormProvider)) as unknown as ( - component: React.ComponentType>, -) => React.ComponentType, keyof FormProviderOnyxProps>>; +})(forwardRef(FormProvider)) as (props: Omit, keyof FormProviderOnyxProps>) => ReactNode; diff --git a/src/components/Form/InputWrapper.tsx b/src/components/Form/InputWrapper.tsx index 9a29c1aa8762..dd8014d28564 100644 --- a/src/components/Form/InputWrapper.tsx +++ b/src/components/Form/InputWrapper.tsx @@ -1,9 +1,9 @@ import React, {forwardRef, useContext} from 'react'; import TextInput from '@components/TextInput'; import FormContext from './FormContext'; -import type {InputProps, InputRef, InputWrapperProps} from './types'; +import type {InputProps, InputRef, InputWrapperProps, ValidInput} from './types'; -function InputWrapper({InputComponent, inputID, valueType = 'string', ...rest}: InputWrapperProps, ref: InputRef) { +function InputWrapper({InputComponent, inputID, valueType = 'string', ...rest}: InputWrapperProps, ref: InputRef) { const {registerInput} = useContext(FormContext); // There are inputs that don't have onBlur methods, to simulate the behavior of onBlur in e.g. checkbox, we had to @@ -13,7 +13,7 @@ function InputWrapper({InputComponent, inputID, const shouldSetTouchedOnBlurOnly = InputComponent === TextInput; // eslint-disable-next-line react/jsx-props-no-spreading - return ; + return ; } InputWrapper.displayName = 'InputWrapper'; diff --git a/src/components/Form/types.ts b/src/components/Form/types.ts index fc3d5c46532f..6c1fcdf0c524 100644 --- a/src/components/Form/types.ts +++ b/src/components/Form/types.ts @@ -1,13 +1,17 @@ -import type {ComponentType, ForwardedRef, ForwardRefExoticComponent, ReactNode, SyntheticEvent} from 'react'; -import type {GestureResponderEvent, StyleProp, TextInput, ViewStyle} from 'react-native'; -import type {OnyxFormKey} from '@src/ONYXKEYS'; +import type {ForwardedRef, ReactNode} from 'react'; +import type {StyleProp, ViewStyle} from 'react-native'; +import type TextInput from '@components/TextInput'; +import type {OnyxFormKey, OnyxValues} from '@src/ONYXKEYS'; import type {Form} from '@src/types/onyx'; type ValueType = 'string' | 'boolean' | 'date'; -type InputWrapperProps = { - // TODO: refactor it as soon as TextInput will be written in typescript - InputComponent: ComponentType | ForwardRefExoticComponent; +type ValidInput = typeof TextInput; + +type InputProps = Parameters[0]; + +type InputWrapperProps = InputProps & { + InputComponent: TInput; inputID: string; valueType?: ValueType; }; @@ -15,9 +19,12 @@ type InputWrapperProps = { type ExcludeDraft = T extends `${string}Draft` ? never : T; type OnyxFormKeyWithoutDraft = ExcludeDraft; -type FormProps = { +type OnyxFormValues = OnyxValues[TOnyxKey]; +type OnyxFormValuesFields = Omit; + +type FormProps = { /** A unique Onyx key identifying the form */ - formID: OnyxFormKey; + formID: TFormID; /** Text to be displayed in the submit button */ submitButtonText: string; @@ -44,34 +51,9 @@ type FormProps = { footerContent?: ReactNode; }; -type FormValuesFields = Omit; - -type InputRef = ForwardedRef; +type InputRef = ForwardedRef; type InputRefs = Record; -type InputPropsToPass = { - ref?: InputRef; - key?: string; - value?: unknown; - defaultValue?: unknown; - shouldSaveDraft?: boolean; - shouldUseDefaultValue?: boolean; - valueType?: ValueType; - shouldSetTouchedOnBlurOnly?: boolean; - - onValueChange?: (value: unknown, key?: string) => void; - onTouched?: (event: GestureResponderEvent | KeyboardEvent) => void; - onPress?: (event: GestureResponderEvent | KeyboardEvent) => void; - onPressOut?: (event: GestureResponderEvent | KeyboardEvent) => void; - onBlur?: (event: SyntheticEvent | FocusEvent) => void; - onInputChange?: (value: unknown, key?: string) => void; -}; - -type InputProps = InputPropsToPass & { - inputID: string; - errorText: string; -}; - -type RegisterInput = (inputID: string, props: InputPropsToPass) => InputProps; +type RegisterInput = (inputID: string, props: InputProps) => InputProps; -export type {InputWrapperProps, FormProps, InputRef, InputRefs, RegisterInput, ValueType, FormValuesFields, InputProps, OnyxFormKeyWithoutDraft}; +export type {InputWrapperProps, ValidInput, FormProps, InputRef, InputRefs, RegisterInput, ValueType, OnyxFormValues, OnyxFormValuesFields, InputProps, OnyxFormKeyWithoutDraft}; From 2478377f9840c4d64efceecf2857973ea4d7dc86 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Thu, 4 Jan 2024 14:38:39 +0100 Subject: [PATCH 096/391] fix: types for taxes feature --- src/libs/OptionsListUtils.ts | 97 +++++++++++--------------------- src/types/onyx/PolicyTaxRates.ts | 16 ++++-- 2 files changed, 43 insertions(+), 70 deletions(-) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 655b5e4d8291..6114f9f8e970 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -14,6 +14,7 @@ import ONYXKEYS from '@src/ONYXKEYS'; import type {Beta, Login, PersonalDetails, Policy, PolicyCategories, Report, ReportAction, ReportActions, Transaction} from '@src/types/onyx'; import type {Participant} from '@src/types/onyx/IOU'; import type * as OnyxCommon from '@src/types/onyx/OnyxCommon'; +import type {PolicyTaxRate, PolicyTaxRates} from '@src/types/onyx/PolicyTaxRates'; import type DeepValueOf from '@src/types/utils/DeepValueOf'; import type {EmptyObject} from '@src/types/utils/EmptyObject'; import {isEmptyObject, isNotEmptyObject} from '@src/types/utils/EmptyObject'; @@ -41,11 +42,11 @@ type Tag = { }; type Option = { - text: string | null; - keyForList: string; - searchText: string; - tooltipText: string; - isDisabled: boolean; + text?: string | null; + keyForList?: string; + searchText?: string; + tooltipText?: string; + isDisabled?: boolean; }; type PayeePersonalDetails = { @@ -100,7 +101,7 @@ type GetOptionsConfig = { canInviteUser?: boolean; includeSelectedOptions?: boolean; includePolicyTaxRates?: boolean; - policyTaxRates?: any; + policyTaxRates?: PolicyTaxRateWithDefault; }; type MemberForList = { @@ -128,7 +129,7 @@ type GetOptions = { currentUserOption: ReportUtils.OptionData | null | undefined; categoryOptions: CategorySection[]; tagOptions: CategorySection[]; - policyTaxRatesOptions: any[]; + policyTaxRatesOptions: CategorySection[]; }; type PreviewConfig = {showChatPreviewLine?: boolean; forcePolicyNamePreview?: boolean}; @@ -838,10 +839,9 @@ function sortTags(tags: Record | Tag[]) { * @param options[].name - a name of an option * @param [isOneLine] - a flag to determine if text should be one line */ -function getCategoryOptionTree(options: Category[], isOneLine = false): Option[] { +function getCategoryOptionTree(options: Record | Category[], isOneLine = false): Option[] { const optionCollection = new Map(); - - options.forEach((option) => { + Object.values(options).forEach((option) => { if (isOneLine) { if (optionCollection.has(option.name)) { return; @@ -1114,68 +1114,41 @@ function getTagListSections(rawTags: Tag[], recentlyUsedTags: string[], selected return tagSections; } -type TaxRate = { - /** The name of the tax rate. */ - name: string; - - /** The value of the tax rate. */ - value: string; - /** The code associated with the tax rate. */ - code: string; - - /** This contains the tax name and tax value as one name */ - modifiedName: string; - - /** Indicates if the tax rate is disabled. */ - isDisabled?: boolean; +type PolicyTaxRateWithDefault = { + name: string; + defaultExternalID: string; + defaultValue: string; + foreignTaxDefault: string; + taxes: PolicyTaxRates; }; -/** - * Represents the data for a single tax rate. - * - * @property {string} name - The name of the tax rate. - * @property {string} value - The value of the tax rate. - * @property {string} code - The code associated with the tax rate. - * @property {string} modifiedName - This contains the tax name and tax value as one name - * @property {boolean} [isDisabled] - Indicates if the tax rate is disabled. - */ /** * Transforms tax rates to a new object format - to add codes and new name with concatenated name and value. * - * @param {Object} policyTaxRates - The original tax rates object. - * @returns {Object.>} The transformed tax rates object. + * @param policyTaxRates - The original tax rates object. + * @returns The transformed tax rates object.g */ -function transformedTaxRates(policyTaxRates) { - const defaultTaxKey = policyTaxRates.defaultExternalID; - const getModifiedName = (data, code) => `${data.name} (${data.value})${defaultTaxKey === code ? ` • ${Localize.translateLocal('common.default')}` : ''}`; - const taxes = Object.fromEntries(_.map(Object.entries(policyTaxRates.taxes), ([code, data]) => [code, {...data, code, modifiedName: getModifiedName(data, code), name: data.name}])); +function transformedTaxRates(policyTaxRates: PolicyTaxRateWithDefault | undefined): Record { + const defaultTaxKey = policyTaxRates?.defaultExternalID; + const getModifiedName = (data: PolicyTaxRate, code: string) => `${data.name} (${data.value})${defaultTaxKey === code ? ` • ${Localize.translateLocal('common.default')}` : ''}`; + const taxes = Object.fromEntries(Object.entries(policyTaxRates?.taxes ?? {}).map(([code, data]) => [code, {...data, code, modifiedName: getModifiedName(data, code), name: data.name}])); return taxes; } /** * Sorts tax rates alphabetically by name. - * - * @param {Object} taxRates - * @returns {Array} */ -function sortTaxRates(taxRates) { - const sortedtaxRates = _.chain(taxRates) - .values() - .sortBy((taxRate) => taxRate.name) - .value(); - +function sortTaxRates(taxRates: PolicyTaxRates): PolicyTaxRate[] { + const sortedtaxRates = lodashSortBy(taxRates, (taxRate) => taxRate.name); return sortedtaxRates; } /** * Builds the options for taxRates - * - * @param {Object[]} taxRates - an initial object array - * @returns {Array} */ -function getTaxRatesOptions(taxRates) { - return Object.values(taxRates).map((taxRate) => ({ +function getTaxRatesOptions(taxRates: Array>): Option[] { + return taxRates.map((taxRate) => ({ text: taxRate.modifiedName, keyForList: taxRate.code, searchText: taxRate.modifiedName, @@ -1187,13 +1160,8 @@ function getTaxRatesOptions(taxRates) { /** * Builds the section list for tax rates - * - * @param {Object} policyTaxRates - * @param {Object[]} selectedOptions - * @param {String} searchInputValue - * @returns {Array} */ -function getTaxRatesSection(policyTaxRates, selectedOptions, searchInputValue) { +function getTaxRatesSection(policyTaxRates: PolicyTaxRateWithDefault | undefined, selectedOptions: Category[], searchInputValue: string): CategorySection[] { const policyRatesSections = []; const taxes = transformedTaxRates(policyTaxRates); @@ -1212,7 +1180,7 @@ function getTaxRatesSection(policyTaxRates, selectedOptions, searchInputValue) { isDisabled: false, })); policyRatesSections.push({ - // "Selected" section + // "Selected" sectiong title: '', shouldShow: false, indexOffset, @@ -1253,11 +1221,11 @@ function getTaxRatesSection(policyTaxRates, selectedOptions, searchInputValue) { if (selectedOptions.length > 0) { const selectedTaxRatesOptions = selectedOptions.map((option) => { - const taxRateObject = taxes.find((taxRate) => taxRate.modifiedName === option.name); + const taxRateObject = Object.values(taxes).find((taxRate) => taxRate.modifiedName === option.name); return { modifiedName: option.name, - isDisabled: Boolean(taxRateObject.isDisabled), + isDisabled: Boolean(taxRateObject?.isDisabled), }; }); @@ -1351,8 +1319,7 @@ function getOptions( } if (includePolicyTaxRates) { - console.log(policyTaxRates); - const policyTaxRatesOptions = getTaxRatesSection(policyTaxRates, selectedOptions, searchInputValue); + const policyTaxRatesOptions = getTaxRatesSection(policyTaxRates, selectedOptions as Category[], searchInputValue); return { recentReports: [], @@ -1717,7 +1684,7 @@ function getFilteredOptions( canInviteUser = true, includeSelectedOptions = false, includePolicyTaxRates = false, - policyTaxRates = {}, + policyTaxRates = {} as PolicyTaxRateWithDefault, ) { return getOptions(reports, personalDetails, { betas, diff --git a/src/types/onyx/PolicyTaxRates.ts b/src/types/onyx/PolicyTaxRates.ts index d549b620f51f..e2bea4a3fa44 100644 --- a/src/types/onyx/PolicyTaxRates.ts +++ b/src/types/onyx/PolicyTaxRates.ts @@ -1,14 +1,20 @@ type PolicyTaxRate = { - /** Name of a tax */ + /** The name of the tax rate. */ name: string; - /** The value of a tax */ + /** The value of the tax rate. */ value: string; - /** Whether the tax is disabled */ + /** The code associated with the tax rate. */ + code: string; + + /** This contains the tax name and tax value as one name */ + modifiedName: string; + + /** Indicates if the tax rate is disabled. */ isDisabled?: boolean; }; type PolicyTaxRates = Record; -export default PolicyTaxRate; -export type {PolicyTaxRates}; + +export type {PolicyTaxRates, PolicyTaxRate}; From 7b139520adc0ba999e43f6957468fe69c7e8f185 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Thu, 4 Jan 2024 15:00:54 +0100 Subject: [PATCH 097/391] temp: e2eLogin with getting otp code from server --- src/libs/E2E/actions/e2eLogin.ts | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/libs/E2E/actions/e2eLogin.ts b/src/libs/E2E/actions/e2eLogin.ts index 6a25705df755..41f9de6c6501 100644 --- a/src/libs/E2E/actions/e2eLogin.ts +++ b/src/libs/E2E/actions/e2eLogin.ts @@ -1,5 +1,6 @@ /* eslint-disable rulesdir/prefer-onyx-connect-in-libs */ import Onyx from 'react-native-onyx'; +import E2EClient from '@libs/E2E/client'; import * as Session from '@userActions/Session'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -10,7 +11,7 @@ import ONYXKEYS from '@src/ONYXKEYS'; * * @return Resolved true when the user was actually signed in. Returns false if the user was already logged in. */ -export default function (email = 'fake@email.com', password = 'Password123'): Promise { +export default function (email = 'expensify.testuser@trashmail.de'): Promise { const waitForBeginSignInToFinish = (): Promise => new Promise((resolve) => { const id = Onyx.connect({ @@ -38,9 +39,17 @@ export default function (email = 'fake@email.com', password = 'Password123'): Pr neededLogin = true; // authenticate with a predefined user + console.debug('[E2E] Signing in…'); Session.beginSignIn(email); + console.debug('[E2E] Waiting for sign in to finish…'); waitForBeginSignInToFinish().then(() => { - Session.signIn(password); + // Get OTP code + console.debug('[E2E] Waiting for OTP…'); + E2EClient.getOTPCode().then((otp) => { + // Complete sign in + console.debug('[E2E] Completing sign in with otp code', otp); + Session.signIn(otp); + }); }); } else { // signal that auth was completed From a799cf3514bddc80db14b5edca2896b46bedd95f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Thu, 4 Jan 2024 15:01:45 +0100 Subject: [PATCH 098/391] fix timing of isSidebarLoaded --- .../LHNOptionsList/LHNOptionsList.js | 21 ++++++++++++++++++- src/components/LHNOptionsList/OptionRowLHN.js | 4 ++++ src/pages/home/sidebar/SidebarLinks.js | 2 +- 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/src/components/LHNOptionsList/LHNOptionsList.js b/src/components/LHNOptionsList/LHNOptionsList.js index 71b14b6fadcd..5f4d16174184 100644 --- a/src/components/LHNOptionsList/LHNOptionsList.js +++ b/src/components/LHNOptionsList/LHNOptionsList.js @@ -39,6 +39,9 @@ const propTypes = { /** Whether to allow option focus or not */ shouldDisableFocusOptions: PropTypes.bool, + /** Callback to fire when the list is laid out */ + onFirstItemRendered: PropTypes.func, + /** The policy which the user has access to and which the report could be tied to */ policy: PropTypes.shape({ /** The ID of the policy */ @@ -78,6 +81,7 @@ const defaultProps = { personalDetails: {}, transactions: {}, draftComments: {}, + onFirstItemRendered: () => {}, ...withCurrentReportIDDefaultProps, }; @@ -98,8 +102,22 @@ function LHNOptionsList({ transactions, draftComments, currentReportID, + onFirstItemRendered, }) { const styles = useThemeStyles(); + + // When the first item renders we want to call the onFirstItemRendered callback. + // At this point in time we know that the list is actually displaying items. + const hasCalledOnLayout = React.useRef(false); + const onLayoutItem = useCallback(() => { + if (hasCalledOnLayout.current) { + return; + } + hasCalledOnLayout.current = true; + + onFirstItemRendered(); + }, [onFirstItemRendered]); + /** * Function which renders a row in the list * @@ -137,10 +155,11 @@ function LHNOptionsList({ onSelectRow={onSelectRow} preferredLocale={preferredLocale} comment={itemComment} + onLayout={onLayoutItem} /> ); }, - [currentReportID, draftComments, onSelectRow, optionMode, personalDetails, policy, preferredLocale, reportActions, reports, shouldDisableFocusOptions, transactions], + [currentReportID, draftComments, onLayoutItem, onSelectRow, optionMode, personalDetails, policy, preferredLocale, reportActions, reports, shouldDisableFocusOptions, transactions], ); return ( diff --git a/src/components/LHNOptionsList/OptionRowLHN.js b/src/components/LHNOptionsList/OptionRowLHN.js index fc4f05eefd22..f04f4412f531 100644 --- a/src/components/LHNOptionsList/OptionRowLHN.js +++ b/src/components/LHNOptionsList/OptionRowLHN.js @@ -50,6 +50,8 @@ const propTypes = { /** The item that should be rendered */ // eslint-disable-next-line react/forbid-prop-types optionItem: PropTypes.object, + + onLayout: PropTypes.func, }; const defaultProps = { @@ -59,6 +61,7 @@ const defaultProps = { style: null, optionItem: null, isFocused: false, + onLayout: () => {}, }; function OptionRowLHN(props) { @@ -209,6 +212,7 @@ function OptionRowLHN(props) { role={CONST.ROLE.BUTTON} accessibilityLabel={translate('accessibilityHints.navigatesToChat')} needsOffscreenAlphaCompositing={props.optionItem.icons.length >= 2} + onLayout={props.onLayout} > diff --git a/src/pages/home/sidebar/SidebarLinks.js b/src/pages/home/sidebar/SidebarLinks.js index ffcba2048d18..8fee62d03edb 100644 --- a/src/pages/home/sidebar/SidebarLinks.js +++ b/src/pages/home/sidebar/SidebarLinks.js @@ -68,7 +68,6 @@ function SidebarLinks({onLinkClick, insets, optionListItems, isLoading, priority }, [isSmallScreenWidth]); useEffect(() => { - App.setSidebarLoaded(); SidebarUtils.setIsSidebarLoadedReady(); InteractionManager.runAfterInteractions(() => { @@ -192,6 +191,7 @@ function SidebarLinks({onLinkClick, insets, optionListItems, isLoading, priority onSelectRow={showReportPage} shouldDisableFocusOptions={isSmallScreenWidth} optionMode={viewMode} + onFirstItemRendered={App.setSidebarLoaded} /> {isLoading && optionListItems.length === 0 && ( From e0daefda2b0a72a7565ee353af93faf5c0488553 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Thu, 4 Jan 2024 15:02:55 +0100 Subject: [PATCH 099/391] add waitForAppLoaded in tests --- src/libs/E2E/actions/waitForAppLoaded.ts | 19 ++++++++++++++++ src/libs/E2E/tests/appStartTimeTest.e2e.ts | 7 ++++-- src/libs/E2E/tests/chatOpeningTest.e2e.ts | 23 +++++--------------- src/libs/E2E/tests/openSearchPageTest.e2e.ts | 7 ++++-- src/libs/E2E/tests/reportTypingTest.e2e.ts | 7 ++++-- 5 files changed, 40 insertions(+), 23 deletions(-) create mode 100644 src/libs/E2E/actions/waitForAppLoaded.ts diff --git a/src/libs/E2E/actions/waitForAppLoaded.ts b/src/libs/E2E/actions/waitForAppLoaded.ts new file mode 100644 index 000000000000..bea739a1b4c7 --- /dev/null +++ b/src/libs/E2E/actions/waitForAppLoaded.ts @@ -0,0 +1,19 @@ +import Onyx from 'react-native-onyx'; +import ONYXKEYS from '@src/ONYXKEYS'; + +// Once we get the sidebar loaded end mark we know that the app is ready to be used: +export default function waitForAppLoaded(): Promise { + return new Promise((resolve) => { + const connectionId = Onyx.connect({ + key: ONYXKEYS.IS_SIDEBAR_LOADED, + callback: (isSidebarLoaded) => { + if (!isSidebarLoaded) { + return; + } + + resolve(); + Onyx.disconnect(connectionId); + }, + }); + }); +} diff --git a/src/libs/E2E/tests/appStartTimeTest.e2e.ts b/src/libs/E2E/tests/appStartTimeTest.e2e.ts index 6589e594dac6..5720af8b3641 100644 --- a/src/libs/E2E/tests/appStartTimeTest.e2e.ts +++ b/src/libs/E2E/tests/appStartTimeTest.e2e.ts @@ -1,6 +1,7 @@ import Config from 'react-native-config'; import type {PerformanceEntry} from 'react-native-performance'; import E2ELogin from '@libs/E2E/actions/e2eLogin'; +import waitForAppLoaded from '@libs/E2E/actions/waitForAppLoaded'; import E2EClient from '@libs/E2E/client'; import Performance from '@libs/Performance'; @@ -8,8 +9,10 @@ const test = () => { // check for login (if already logged in the action will simply resolve) E2ELogin().then((neededLogin) => { if (neededLogin) { - // we don't want to submit the first login to the results - return E2EClient.submitTestDone(); + return waitForAppLoaded().then(() => + // we don't want to submit the first login to the results + E2EClient.submitTestDone(), + ); } console.debug('[E2E] Logged in, getting metrics and submitting them…'); diff --git a/src/libs/E2E/tests/chatOpeningTest.e2e.ts b/src/libs/E2E/tests/chatOpeningTest.e2e.ts index ff948c298b4a..5ec1d50f7cda 100644 --- a/src/libs/E2E/tests/chatOpeningTest.e2e.ts +++ b/src/libs/E2E/tests/chatOpeningTest.e2e.ts @@ -1,34 +1,23 @@ import E2ELogin from '@libs/E2E/actions/e2eLogin'; -import mockReport from '@libs/E2E/apiMocks/openReport'; +import waitForAppLoaded from '@libs/E2E/actions/waitForAppLoaded'; import E2EClient from '@libs/E2E/client'; import Navigation from '@libs/Navigation/Navigation'; import Performance from '@libs/Performance'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; -type ReportValue = { - reportID: string; -}; - -type OnyxData = { - value: ReportValue; -}; - -type MockReportResponse = { - onyxData: OnyxData[]; -}; - const test = () => { // check for login (if already logged in the action will simply resolve) console.debug('[E2E] Logging in for chat opening'); - const report = mockReport() as MockReportResponse; - const {reportID} = report.onyxData[0].value; + const reportID = ''; // report.onyxData[0].value; // TODO: get report ID! E2ELogin().then((neededLogin) => { if (neededLogin) { - // we don't want to submit the first login to the results - return E2EClient.submitTestDone(); + return waitForAppLoaded().then(() => + // we don't want to submit the first login to the results + E2EClient.submitTestDone(), + ); } console.debug('[E2E] Logged in, getting chat opening metrics and submitting them…'); diff --git a/src/libs/E2E/tests/openSearchPageTest.e2e.ts b/src/libs/E2E/tests/openSearchPageTest.e2e.ts index c68553d6de8a..86da851396f6 100644 --- a/src/libs/E2E/tests/openSearchPageTest.e2e.ts +++ b/src/libs/E2E/tests/openSearchPageTest.e2e.ts @@ -1,5 +1,6 @@ import Config from 'react-native-config'; import E2ELogin from '@libs/E2E/actions/e2eLogin'; +import waitForAppLoaded from '@libs/E2E/actions/waitForAppLoaded'; import E2EClient from '@libs/E2E/client'; import Navigation from '@libs/Navigation/Navigation'; import Performance from '@libs/Performance'; @@ -12,8 +13,10 @@ const test = () => { E2ELogin().then((neededLogin: boolean): Promise | undefined => { if (neededLogin) { - // we don't want to submit the first login to the results - return E2EClient.submitTestDone(); + return waitForAppLoaded().then(() => + // we don't want to submit the first login to the results + E2EClient.submitTestDone(), + ); } console.debug('[E2E] Logged in, getting search metrics and submitting them…'); diff --git a/src/libs/E2E/tests/reportTypingTest.e2e.ts b/src/libs/E2E/tests/reportTypingTest.e2e.ts index 90d0dc9e0bb6..d6bffa3e171a 100644 --- a/src/libs/E2E/tests/reportTypingTest.e2e.ts +++ b/src/libs/E2E/tests/reportTypingTest.e2e.ts @@ -1,5 +1,6 @@ import Config from 'react-native-config'; import E2ELogin from '@libs/E2E/actions/e2eLogin'; +import waitForAppLoaded from '@libs/E2E/actions/waitForAppLoaded'; import waitForKeyboard from '@libs/E2E/actions/waitForKeyboard'; import E2EClient from '@libs/E2E/client'; import Navigation from '@libs/Navigation/Navigation'; @@ -15,8 +16,10 @@ const test = () => { E2ELogin().then((neededLogin) => { if (neededLogin) { - // we don't want to submit the first login to the results - return E2EClient.submitTestDone(); + return waitForAppLoaded().then(() => + // we don't want to submit the first login to the results + E2EClient.submitTestDone(), + ); } console.debug('[E2E] Logged in, getting typing metrics and submitting them…'); From 19ea9355f87864b88d0111f20443fa61960383b9 Mon Sep 17 00:00:00 2001 From: Ishpaul Singh Date: Thu, 4 Jan 2024 20:33:36 +0530 Subject: [PATCH 100/391] migration v1 --- src/components/Avatar.tsx | 1 + .../CellRendererComponent.tsx | 4 +- ...agment.js => ReportActionItemFragment.tsx} | 138 ++++++++---------- .../home/report/ReportActionItemMessage.tsx | 10 +- .../home/report/ReportActionItemSingle.tsx | 2 +- ...gment.js => AttachmentCommentFragment.tsx} | 19 +-- .../home/report/comment/RenderCommentHTML.js | 23 --- .../home/report/comment/RenderCommentHTML.tsx | 18 +++ ...entFragment.js => TextCommentFragment.tsx} | 62 ++++---- src/types/onyx/OriginalMessage.ts | 1 + 10 files changed, 121 insertions(+), 157 deletions(-) rename src/pages/home/report/{ReportActionItemFragment.js => ReportActionItemFragment.tsx} (50%) rename src/pages/home/report/comment/{AttachmentCommentFragment.js => AttachmentCommentFragment.tsx} (55%) delete mode 100644 src/pages/home/report/comment/RenderCommentHTML.js create mode 100644 src/pages/home/report/comment/RenderCommentHTML.tsx rename src/pages/home/report/comment/{TextCommentFragment.js => TextCommentFragment.tsx} (67%) diff --git a/src/components/Avatar.tsx b/src/components/Avatar.tsx index 65b0b6c36061..6b0590d57e83 100644 --- a/src/components/Avatar.tsx +++ b/src/components/Avatar.tsx @@ -123,3 +123,4 @@ function Avatar({ Avatar.displayName = 'Avatar'; export default Avatar; +export {type AvatarProps}; diff --git a/src/components/InvertedFlatList/CellRendererComponent.tsx b/src/components/InvertedFlatList/CellRendererComponent.tsx index 252d47989064..60f54ead13c5 100644 --- a/src/components/InvertedFlatList/CellRendererComponent.tsx +++ b/src/components/InvertedFlatList/CellRendererComponent.tsx @@ -1,9 +1,9 @@ import React from 'react'; -import {StyleProp, View, ViewProps} from 'react-native'; +import {StyleProp, View, ViewProps, ViewStyle} from 'react-native'; type CellRendererComponentProps = ViewProps & { index: number; - style?: StyleProp; + style?: StyleProp; }; function CellRendererComponent(props: CellRendererComponentProps) { diff --git a/src/pages/home/report/ReportActionItemFragment.js b/src/pages/home/report/ReportActionItemFragment.tsx similarity index 50% rename from src/pages/home/report/ReportActionItemFragment.js rename to src/pages/home/report/ReportActionItemFragment.tsx index f05b3decc6d7..a72d91ddcbbb 100644 --- a/src/pages/home/report/ReportActionItemFragment.js +++ b/src/pages/home/report/ReportActionItemFragment.tsx @@ -1,101 +1,83 @@ -import PropTypes from 'prop-types'; import React, {memo} from 'react'; -import avatarPropTypes from '@components/avatarPropTypes'; -import {withNetwork} from '@components/OnyxProvider'; +import {StyleProp, TextStyle} from 'react-native'; +import type {AvatarProps} from '@components/Avatar'; import RenderHTML from '@components/RenderHTML'; import Text from '@components/Text'; import UserDetailsTooltip from '@components/UserDetailsTooltip'; -import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; -import withWindowDimensions, {windowDimensionsPropTypes} from '@components/withWindowDimensions'; +import useLocalize from '@hooks/useLocalize'; +import useNetwork from '@hooks/useNetwork'; import useThemeStyles from '@hooks/useThemeStyles'; -import compose from '@libs/compose'; import convertToLTR from '@libs/convertToLTR'; import * as ReportUtils from '@libs/ReportUtils'; import CONST from '@src/CONST'; +import * as OnyxCommon from '@src/types/onyx/OnyxCommon'; +import type {OriginalMessageSource} from '@src/types/onyx/OriginalMessage'; +import type {Message} from '@src/types/onyx/ReportAction'; +import {EmptyObject} from '@src/types/utils/EmptyObject'; import AttachmentCommentFragment from './comment/AttachmentCommentFragment'; import TextCommentFragment from './comment/TextCommentFragment'; -import reportActionFragmentPropTypes from './reportActionFragmentPropTypes'; -const propTypes = { +type ReportActionItemFragmentProps = { /** Users accountID */ - accountID: PropTypes.number.isRequired, + accountID: number; /** The message fragment needing to be displayed */ - fragment: reportActionFragmentPropTypes.isRequired, + fragment: Message; /** If this fragment is attachment than has info? */ - attachmentInfo: PropTypes.shape({ - /** The file name of attachment */ - name: PropTypes.string, - - /** The file size of the attachment in bytes. */ - size: PropTypes.number, - - /** The MIME type of the attachment. */ - type: PropTypes.string, - - /** Attachment's URL represents the specified File object or Blob object */ - source: PropTypes.string, - }), + attachmentInfo?: EmptyObject | File; /** Message(text) of an IOU report action */ - iouMessage: PropTypes.string, + iouMessage?: string; /** The reportAction's source */ - source: PropTypes.oneOf(['Chronos', 'email', 'ios', 'android', 'web', 'email', '']), + source: OriginalMessageSource; /** Should this fragment be contained in a single line? */ - isSingleLine: PropTypes.bool, + isSingleLine?: boolean; - // Additional styles to add after local styles - style: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.object), PropTypes.object]), + /** Additional styles to add after local styles */ + style?: StyleProp; /** The accountID of the copilot who took this action on behalf of the user */ - delegateAccountID: PropTypes.number, + delegateAccountID?: string; /** icon */ - actorIcon: avatarPropTypes, + actorIcon?: AvatarProps; /** Whether the comment is a thread parent message/the first message in a thread */ - isThreadParentMessage: PropTypes.bool, + isThreadParentMessage?: boolean; /** Should the comment have the appearance of being grouped with the previous comment? */ - displayAsGroup: PropTypes.bool, + displayAsGroup?: boolean; /** Whether the report action type is 'APPROVED' or 'SUBMITTED'. Used to style system messages from Old Dot */ - isApprovedOrSubmittedReportAction: PropTypes.bool, + isApprovedOrSubmittedReportAction?: boolean; /** Used to format RTL display names in Old Dot system messages e.g. Arabic */ - isFragmentContainingDisplayName: PropTypes.bool, - - ...windowDimensionsPropTypes, - - /** localization props */ - ...withLocalizePropTypes, -}; + isFragmentContainingDisplayName?: boolean; -const defaultProps = { - attachmentInfo: { - name: '', - size: 0, - type: '', - source: '', - }, - iouMessage: '', - isSingleLine: false, - source: '', - style: [], - delegateAccountID: 0, - actorIcon: {}, - isThreadParentMessage: false, - isApprovedOrSubmittedReportAction: false, - isFragmentContainingDisplayName: false, - displayAsGroup: false, + /** The pending action for the report action */ + pendingAction?: OnyxCommon.PendingAction; }; -function ReportActionItemFragment(props) { +function ReportActionItemFragment({ + iouMessage = '', + isSingleLine = false, + source = '', + style = [], + delegateAccountID = '', + actorIcon = {}, + isThreadParentMessage = false, + isApprovedOrSubmittedReportAction = false, + isFragmentContainingDisplayName = false, + displayAsGroup = false, + ...props +}: ReportActionItemFragmentProps) { const styles = useThemeStyles(); - const fragment = props.fragment; + const {fragment} = props; + const {isOffline} = useNetwork(); + const {translate} = useLocalize(); switch (fragment.type) { case 'COMMENT': { @@ -105,48 +87,48 @@ function ReportActionItemFragment(props) { // While offline we display the previous message with a strikethrough style. Once online we want to // immediately display "[Deleted message]" while the delete action is pending. - if ((!props.network.isOffline && props.isThreadParentMessage && isPendingDelete) || props.fragment.isDeletedParentAction) { - return ${props.translate('parentReportAction.deletedMessage')}`} />; + if ((!isOffline && isThreadParentMessage && isPendingDelete) || props.fragment.isDeletedParentAction) { + return ${translate('parentReportAction.deletedMessage')}`} />; } if (ReportUtils.isReportMessageAttachment(fragment)) { return ( ); } return ( ); } case 'TEXT': { - return props.isApprovedOrSubmittedReportAction ? ( + return isApprovedOrSubmittedReportAction ? ( - {props.isFragmentContainingDisplayName ? convertToLTR(props.fragment.text) : props.fragment.text} + {isFragmentContainingDisplayName ? convertToLTR(props.fragment.text) : props.fragment.text} ) : ( {fragment.text} @@ -172,8 +154,6 @@ function ReportActionItemFragment(props) { } } -ReportActionItemFragment.propTypes = propTypes; -ReportActionItemFragment.defaultProps = defaultProps; ReportActionItemFragment.displayName = 'ReportActionItemFragment'; -export default compose(withWindowDimensions, withLocalize, withNetwork())(memo(ReportActionItemFragment)); +export default memo(ReportActionItemFragment); diff --git a/src/pages/home/report/ReportActionItemMessage.tsx b/src/pages/home/report/ReportActionItemMessage.tsx index 89d0aaa1523b..80639fcb1123 100644 --- a/src/pages/home/report/ReportActionItemMessage.tsx +++ b/src/pages/home/report/ReportActionItemMessage.tsx @@ -1,12 +1,12 @@ import React, {ReactElement} from 'react'; -import {StyleProp, Text, View, ViewStyle} from 'react-native'; +import {StyleProp, Text, TextStyle, View, ViewStyle} from 'react-native'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import * as ReportUtils from '@libs/ReportUtils'; import CONST from '@src/CONST'; import type {ReportAction} from '@src/types/onyx'; -import type {OriginalMessageAddComment} from '@src/types/onyx/OriginalMessage'; +import type {OriginalMessageSource} from '@src/types/onyx/OriginalMessage'; import TextCommentFragment from './comment/TextCommentFragment'; import ReportActionItemFragment from './ReportActionItemFragment'; @@ -18,7 +18,7 @@ type ReportActionItemMessageProps = { displayAsGroup: boolean; /** Additional styles to add after local styles. */ - style?: StyleProp; + style?: StyleProp | StyleProp; /** Whether or not the message is hidden by moderation */ isHidden?: boolean; @@ -74,8 +74,8 @@ function ReportActionItemMessage({action, displayAsGroup, reportID, style, isHid isThreadParentMessage={ReportActionsUtils.isThreadParentMessage(action, reportID)} attachmentInfo={action.attachmentInfo} pendingAction={action.pendingAction} - source={(action.originalMessage as OriginalMessageAddComment['originalMessage'])?.source} - accountID={action.actorAccountID} + source={action.originalMessage as OriginalMessageSource} + accountID={action.actorAccountID ?? 0} style={style} displayAsGroup={displayAsGroup} isApprovedOrSubmittedReportAction={isApprovedOrSubmittedReportAction} diff --git a/src/pages/home/report/ReportActionItemSingle.tsx b/src/pages/home/report/ReportActionItemSingle.tsx index 69bbd924caef..340767441e97 100644 --- a/src/pages/home/report/ReportActionItemSingle.tsx +++ b/src/pages/home/report/ReportActionItemSingle.tsx @@ -256,7 +256,7 @@ function ReportActionItemSingle({ @@ -28,7 +22,6 @@ function AttachmentCommentFragment({addExtraMargin, html, source}) { ); } -AttachmentCommentFragment.propTypes = propTypes; AttachmentCommentFragment.displayName = 'AttachmentCommentFragment'; export default AttachmentCommentFragment; diff --git a/src/pages/home/report/comment/RenderCommentHTML.js b/src/pages/home/report/comment/RenderCommentHTML.js deleted file mode 100644 index 14039af21189..000000000000 --- a/src/pages/home/report/comment/RenderCommentHTML.js +++ /dev/null @@ -1,23 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import RenderHTML from '@components/RenderHTML'; -import reportActionSourcePropType from '@pages/home/report/reportActionSourcePropType'; - -const propTypes = { - /** The reportAction's source */ - source: reportActionSourcePropType.isRequired, - - /** The comment's HTML */ - html: PropTypes.string.isRequired, -}; - -function RenderCommentHTML({html, source}) { - const commentHtml = source === 'email' ? `${html}` : `${html}`; - - return ; -} - -RenderCommentHTML.propTypes = propTypes; -RenderCommentHTML.displayName = 'RenderCommentHTML'; - -export default RenderCommentHTML; diff --git a/src/pages/home/report/comment/RenderCommentHTML.tsx b/src/pages/home/report/comment/RenderCommentHTML.tsx new file mode 100644 index 000000000000..e730ae061519 --- /dev/null +++ b/src/pages/home/report/comment/RenderCommentHTML.tsx @@ -0,0 +1,18 @@ +import React from 'react'; +import RenderHTML from '@components/RenderHTML'; +import type {OriginalMessageSource} from '@src/types/onyx/OriginalMessage'; + +type RenderCommentHTMLProps = { + source: OriginalMessageSource; + html: string; +}; + +function RenderCommentHTML({html, source}: RenderCommentHTMLProps) { + const commentHtml = source === 'email' ? `${html}` : `${html}`; + + return ; +} + +RenderCommentHTML.displayName = 'RenderCommentHTML'; + +export default RenderCommentHTML; diff --git a/src/pages/home/report/comment/TextCommentFragment.js b/src/pages/home/report/comment/TextCommentFragment.tsx similarity index 67% rename from src/pages/home/report/comment/TextCommentFragment.js rename to src/pages/home/report/comment/TextCommentFragment.tsx index 3d6482344450..3b92c0f6a118 100644 --- a/src/pages/home/report/comment/TextCommentFragment.js +++ b/src/pages/home/report/comment/TextCommentFragment.tsx @@ -1,56 +1,49 @@ import Str from 'expensify-common/lib/str'; -import PropTypes from 'prop-types'; +import {isEmpty} from 'lodash'; import React, {memo} from 'react'; +import {type StyleProp, type TextStyle} from 'react-native'; import Text from '@components/Text'; -import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; -import withWindowDimensions, {windowDimensionsPropTypes} from '@components/withWindowDimensions'; import ZeroWidthView from '@components/ZeroWidthView'; +import useLocalize from '@hooks/useLocalize'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; -import compose from '@libs/compose'; +import useWindowDimensions from '@hooks/useWindowDimensions'; import convertToLTR from '@libs/convertToLTR'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import * as EmojiUtils from '@libs/EmojiUtils'; -import reportActionFragmentPropTypes from '@pages/home/report/reportActionFragmentPropTypes'; -import reportActionSourcePropType from '@pages/home/report/reportActionSourcePropType'; import variables from '@styles/variables'; import CONST from '@src/CONST'; +import type {OriginalMessageSource} from '@src/types/onyx/OriginalMessage'; +import type {Message} from '@src/types/onyx/ReportAction'; import RenderCommentHTML from './RenderCommentHTML'; -const propTypes = { +type TextCommentFragmentProps = { /** The reportAction's source */ - source: reportActionSourcePropType.isRequired, + source: OriginalMessageSource; /** The message fragment needing to be displayed */ - fragment: reportActionFragmentPropTypes.isRequired, + fragment: Message; /** Should this message fragment be styled as deleted? */ - styleAsDeleted: PropTypes.bool.isRequired, - - /** Text of an IOU report action */ - iouMessage: PropTypes.string, + styleAsDeleted: boolean; /** Should the comment have the appearance of being grouped with the previous comment? */ - displayAsGroup: PropTypes.bool.isRequired, + displayAsGroup: boolean; /** Additional styles to add after local styles. */ - style: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.object), PropTypes.object]).isRequired, + style: StyleProp; - ...windowDimensionsPropTypes, - - /** localization props */ - ...withLocalizePropTypes, -}; - -const defaultProps = { - iouMessage: undefined, + /** Text of an IOU report action */ + iouMessage?: string; }; -function TextCommentFragment(props) { +function TextCommentFragment({iouMessage = '', ...props}: TextCommentFragmentProps) { const theme = useTheme(); const styles = useThemeStyles(); const {fragment, styleAsDeleted} = props; - const {html, text} = fragment; + const {html = '', text} = fragment; + const {translate} = useLocalize(); + const {isSmallScreenWidth} = useWindowDimensions(); // If the only difference between fragment.text and fragment.html is
tags // we render it as text, not as html. @@ -72,10 +65,13 @@ function TextCommentFragment(props) { ); } + const propsStyle = Array.isArray(props.style) ? props.style : [props.style]; + const containsOnlyEmojis = EmojiUtils.containsOnlyEmojis(text); + const message = isEmpty(iouMessage) ? text : iouMessage; return ( - + - {convertToLTR(props.iouMessage || text)} + {convertToLTR(message)} {Boolean(fragment.isEdited) && ( <> @@ -102,9 +98,9 @@ function TextCommentFragment(props) { - {props.translate('reportActionCompose.edited')} + {translate('reportActionCompose.edited')} )} @@ -112,8 +108,6 @@ function TextCommentFragment(props) { ); } -TextCommentFragment.propTypes = propTypes; -TextCommentFragment.defaultProps = defaultProps; TextCommentFragment.displayName = 'TextCommentFragment'; -export default compose(withWindowDimensions, withLocalize)(memo(TextCommentFragment)); +export default memo(TextCommentFragment); diff --git a/src/types/onyx/OriginalMessage.ts b/src/types/onyx/OriginalMessage.ts index 767f724dd571..f301dc619fb7 100644 --- a/src/types/onyx/OriginalMessage.ts +++ b/src/types/onyx/OriginalMessage.ts @@ -266,4 +266,5 @@ export type { OriginalMessageIOU, OriginalMessageCreated, OriginalMessageAddComment, + OriginalMessageSource, }; From 3e83b4437baaa5b88bc5e0acba6c23c71dc5178f Mon Sep 17 00:00:00 2001 From: Ishpaul Singh Date: Thu, 4 Jan 2024 20:36:25 +0530 Subject: [PATCH 101/391] clean up --- src/pages/home/report/ReportActionItemMessage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/home/report/ReportActionItemMessage.tsx b/src/pages/home/report/ReportActionItemMessage.tsx index 80639fcb1123..41b18cbba15c 100644 --- a/src/pages/home/report/ReportActionItemMessage.tsx +++ b/src/pages/home/report/ReportActionItemMessage.tsx @@ -18,7 +18,7 @@ type ReportActionItemMessageProps = { displayAsGroup: boolean; /** Additional styles to add after local styles. */ - style?: StyleProp | StyleProp; + style?: StyleProp; /** Whether or not the message is hidden by moderation */ isHidden?: boolean; From 073187e317a069e5c656a0ceb7f013be4b2a14df Mon Sep 17 00:00:00 2001 From: Ishpaul Singh Date: Thu, 4 Jan 2024 21:00:14 +0530 Subject: [PATCH 102/391] lint fix --- src/components/InvertedFlatList/CellRendererComponent.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/InvertedFlatList/CellRendererComponent.tsx b/src/components/InvertedFlatList/CellRendererComponent.tsx index 4beaa1d59129..16cb5bdfeba6 100644 --- a/src/components/InvertedFlatList/CellRendererComponent.tsx +++ b/src/components/InvertedFlatList/CellRendererComponent.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import type {StyleProp, ViewStyle} from 'react-native'; +import type {StyleProp, ViewStyle, ViewProps} from 'react-native'; import {View} from 'react-native'; From 3d74ec8c2325f469a46785948bd595cac19192bd Mon Sep 17 00:00:00 2001 From: Ishpaul Singh Date: Thu, 4 Jan 2024 21:02:48 +0530 Subject: [PATCH 103/391] lint fix --- src/components/InvertedFlatList/CellRendererComponent.tsx | 3 +-- src/pages/home/report/ReportActionItemFragment.tsx | 6 +++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/components/InvertedFlatList/CellRendererComponent.tsx b/src/components/InvertedFlatList/CellRendererComponent.tsx index 16cb5bdfeba6..1199fb2a594c 100644 --- a/src/components/InvertedFlatList/CellRendererComponent.tsx +++ b/src/components/InvertedFlatList/CellRendererComponent.tsx @@ -1,8 +1,7 @@ import React from 'react'; -import type {StyleProp, ViewStyle, ViewProps} from 'react-native'; +import type {StyleProp, ViewProps, ViewStyle} from 'react-native'; import {View} from 'react-native'; - type CellRendererComponentProps = ViewProps & { index: number; style?: StyleProp; diff --git a/src/pages/home/report/ReportActionItemFragment.tsx b/src/pages/home/report/ReportActionItemFragment.tsx index a72d91ddcbbb..a5cd8f4577e5 100644 --- a/src/pages/home/report/ReportActionItemFragment.tsx +++ b/src/pages/home/report/ReportActionItemFragment.tsx @@ -1,5 +1,5 @@ import React, {memo} from 'react'; -import {StyleProp, TextStyle} from 'react-native'; +import type {StyleProp, TextStyle} from 'react-native'; import type {AvatarProps} from '@components/Avatar'; import RenderHTML from '@components/RenderHTML'; import Text from '@components/Text'; @@ -10,10 +10,10 @@ import useThemeStyles from '@hooks/useThemeStyles'; import convertToLTR from '@libs/convertToLTR'; import * as ReportUtils from '@libs/ReportUtils'; import CONST from '@src/CONST'; -import * as OnyxCommon from '@src/types/onyx/OnyxCommon'; +import type * as OnyxCommon from '@src/types/onyx/OnyxCommon'; import type {OriginalMessageSource} from '@src/types/onyx/OriginalMessage'; import type {Message} from '@src/types/onyx/ReportAction'; -import {EmptyObject} from '@src/types/utils/EmptyObject'; +import type {EmptyObject} from '@src/types/utils/EmptyObject'; import AttachmentCommentFragment from './comment/AttachmentCommentFragment'; import TextCommentFragment from './comment/TextCommentFragment'; From ad6fe37fc7cee5eb2008f45995a4e09ac0de9bd2 Mon Sep 17 00:00:00 2001 From: Ishpaul Singh Date: Thu, 4 Jan 2024 21:08:56 +0530 Subject: [PATCH 104/391] type fix --- src/pages/home/report/ReportActionItemFragment.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/home/report/ReportActionItemFragment.tsx b/src/pages/home/report/ReportActionItemFragment.tsx index a5cd8f4577e5..6e9c6a581066 100644 --- a/src/pages/home/report/ReportActionItemFragment.tsx +++ b/src/pages/home/report/ReportActionItemFragment.tsx @@ -40,7 +40,7 @@ type ReportActionItemFragmentProps = { style?: StyleProp; /** The accountID of the copilot who took this action on behalf of the user */ - delegateAccountID?: string; + delegateAccountID?: number; /** icon */ actorIcon?: AvatarProps; @@ -66,7 +66,7 @@ function ReportActionItemFragment({ isSingleLine = false, source = '', style = [], - delegateAccountID = '', + delegateAccountID = 0, actorIcon = {}, isThreadParentMessage = false, isApprovedOrSubmittedReportAction = false, From b09535d78c0f641f388d7ce2876eebb8f50bcff4 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Thu, 4 Jan 2024 16:44:44 +0100 Subject: [PATCH 105/391] fix: resolve comments --- src/components/OptionRow.tsx | 1 - src/libs/ModifiedExpenseMessage.ts | 3 ++- src/libs/OptionsListUtils.ts | 31 ++++++++++++------------------ src/libs/PolicyUtils.ts | 2 -- src/libs/TransactionUtils.ts | 3 +-- src/types/onyx/IOU.ts | 4 ++-- 6 files changed, 17 insertions(+), 27 deletions(-) diff --git a/src/components/OptionRow.tsx b/src/components/OptionRow.tsx index 395a1fe785c6..b669ca454395 100644 --- a/src/components/OptionRow.tsx +++ b/src/components/OptionRow.tsx @@ -195,7 +195,6 @@ function OptionRow({ shouldHaveOptionSeparator && styles.borderTop, !onSelectRow && !isOptionDisabled ? styles.cursorDefault : null, ]} - accessible accessibilityLabel={option.text ?? ''} role={CONST.ROLE.BUTTON} hoverDimmingValue={1} diff --git a/src/libs/ModifiedExpenseMessage.ts b/src/libs/ModifiedExpenseMessage.ts index 27232f38dbd1..d5206961f1a5 100644 --- a/src/libs/ModifiedExpenseMessage.ts +++ b/src/libs/ModifiedExpenseMessage.ts @@ -1,5 +1,6 @@ import {format} from 'date-fns'; -import Onyx, {OnyxEntry} from 'react-native-onyx'; +import Onyx from 'react-native-onyx'; +import type {OnyxEntry} from 'react-native-onyx'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {PolicyTags, ReportAction} from '@src/types/onyx'; diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 6114f9f8e970..9f3b622ae6ee 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -11,7 +11,7 @@ import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {Beta, Login, PersonalDetails, Policy, PolicyCategories, Report, ReportAction, ReportActions, Transaction} from '@src/types/onyx'; +import type {Beta, Login, PersonalDetails, PersonalDetailsList, Policy, PolicyCategories, Report, ReportAction, ReportActions, Transaction} from '@src/types/onyx'; import type {Participant} from '@src/types/onyx/IOU'; import type * as OnyxCommon from '@src/types/onyx/OnyxCommon'; import type {PolicyTaxRate, PolicyTaxRates} from '@src/types/onyx/PolicyTaxRates'; @@ -28,7 +28,6 @@ import ModifiedExpenseMessage from './ModifiedExpenseMessage'; import Navigation from './Navigation/Navigation'; import Permissions from './Permissions'; import * as PersonalDetailsUtils from './PersonalDetailsUtils'; -import type {PersonalDetailsList} from './PolicyUtils'; import * as ReportActionUtils from './ReportActionsUtils'; import * as ReportUtils from './ReportUtils'; import * as TaskUtils from './TaskUtils'; @@ -41,13 +40,7 @@ type Tag = { accountID: number | null; }; -type Option = { - text?: string | null; - keyForList?: string; - searchText?: string; - tooltipText?: string; - isDisabled?: boolean; -}; +type Option = Partial; type PayeePersonalDetails = { text: string; @@ -62,7 +55,7 @@ type CategorySection = { title: string | undefined; shouldShow: boolean; indexOffset: number; - data: Option[] | Array; + data: Option[]; }; type Category = { @@ -75,7 +68,7 @@ type Hierarchy = Record; + selectedOptions?: Option[]; maxRecentReportsToShow?: number; excludeLogins?: string[]; includeMultipleParticipantReports?: boolean; @@ -158,7 +151,7 @@ Onyx.connect({ let allPersonalDetails: OnyxEntry; Onyx.connect({ key: ONYXKEYS.PERSONAL_DETAILS_LIST, - callback: (value) => (allPersonalDetails = Object.keys(value ?? {}).length === 0 ? {} : value), + callback: (value) => (allPersonalDetails = isEmptyObject(value) ? {} : value), }); let preferredLocale: DeepValueOf = CONST.LOCALES.DEFAULT; @@ -576,7 +569,7 @@ function createOption( }; const personalDetailMap = getPersonalDetailsForAccountIDs(accountIDs, personalDetails); - const personalDetailList = Object.values(personalDetailMap).filter(Boolean) as PersonalDetails[]; + const personalDetailList = Object.values(personalDetailMap).filter((details): details is PersonalDetails => !!details); const personalDetail = personalDetailList[0]; let hasMultipleParticipants = personalDetailList.length > 1; let subtitle; @@ -661,7 +654,7 @@ function createOption( result.text = reportName; // Disabling this line for safeness as nullish coalescing works only if the value is undefined or null // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - result.searchText = getSearchText(report, reportName, personalDetailList, !!(result.isChatRoom || result.isPolicyExpenseChat), !!result.isThread); + result.searchText = getSearchText(report, reportName, personalDetailList, !!result.isChatRoom || !!result.isPolicyExpenseChat, !!result.isThread); result.icons = ReportUtils.getIcons( report, personalDetails, @@ -953,7 +946,7 @@ function getCategoryListSections( } const filteredRecentlyUsedCategories = recentlyUsedCategories - .filter((categoryName) => !selectedOptionNames.includes(categoryName) && lodashGet(categories, [categoryName, 'enabled'], false)) + .filter((categoryName) => !selectedOptionNames.includes(categoryName) && categories[categoryName].enabled) .map((categoryName) => ({ name: categoryName, enabled: categories[categoryName].enabled ?? false, @@ -1065,7 +1058,7 @@ function getTagListSections(rawTags: Tag[], recentlyUsedTags: string[], selected const filteredRecentlyUsedTags = recentlyUsedTags .filter((recentlyUsedTag) => { const tagObject = tags.find((tag) => tag.name === recentlyUsedTag); - return Boolean(tagObject && tagObject.enabled) && !selectedOptionNames.includes(recentlyUsedTag); + return !!tagObject?.enabled && !selectedOptionNames.includes(recentlyUsedTag); }) .map((tag) => ({name: tag, enabled: true})); const filteredTags = enabledTags.filter((tag) => !selectedOptionNames.includes(tag.name)); @@ -1443,13 +1436,13 @@ function getOptions( } // Exclude the current user from the personal details list - const optionsToExclude = [{login: currentUserLogin}, {login: CONST.EMAIL.NOTIFICATIONS}]; + const optionsToExclude: Option[] = [{login: currentUserLogin}, {login: CONST.EMAIL.NOTIFICATIONS}]; // If we're including selected options from the search results, we only want to exclude them if the search input is empty // This is because on certain pages, we show the selected options at the top when the search input is empty // This prevents the issue of seeing the selected option twice if you have them as a recent chat and select them if (!includeSelectedOptions || searchInputValue === '') { - optionsToExclude.push(...(selectedOptions as Participant[])); + optionsToExclude.push(...selectedOptions); } excludeLogins.forEach((login) => { @@ -1838,7 +1831,7 @@ function getHeaderMessageForNonUserList(hasSelectableOptions: boolean, searchVal * Helper method to check whether an option can show tooltip or not */ function shouldOptionShowTooltip(option: ReportUtils.OptionData): boolean { - return Boolean((!option.isChatRoom || option.isThread) && !option.isArchivedRoom); + return (!option.isChatRoom || !!option.isThread) && !option.isArchivedRoom; } /** diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index 77f9a2914951..0cab97299324 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -224,5 +224,3 @@ export { isPolicyMember, isPaidGroupPolicy, }; - -export type {PersonalDetailsList}; diff --git a/src/libs/TransactionUtils.ts b/src/libs/TransactionUtils.ts index c34a6753c1d5..d15a287fc545 100644 --- a/src/libs/TransactionUtils.ts +++ b/src/libs/TransactionUtils.ts @@ -5,8 +5,7 @@ import type {ValueOf} from 'type-fest'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {RecentWaypoint, Report, ReportAction, Transaction} from '@src/types/onyx'; -import type {PolicyTaxRates} from '@src/types/onyx/PolicyTaxRates'; -import type PolicyTaxRate from '@src/types/onyx/PolicyTaxRates'; +import type {PolicyTaxRate, PolicyTaxRates} from '@src/types/onyx/PolicyTaxRates'; import type {Comment, Receipt, Waypoint, WaypointCollection} from '@src/types/onyx/Transaction'; import type {EmptyObject} from '@src/types/utils/EmptyObject'; import {isCorporateCard, isExpensifyCard} from './CardUtils'; diff --git a/src/types/onyx/IOU.ts b/src/types/onyx/IOU.ts index 08ca5731d48a..766249f5b456 100644 --- a/src/types/onyx/IOU.ts +++ b/src/types/onyx/IOU.ts @@ -1,8 +1,8 @@ -import {Icon} from './OnyxCommon'; +import type {Icon} from './OnyxCommon'; type Participant = { accountID: number; - login: string | undefined; + login: string; isPolicyExpenseChat?: boolean; isOwnPolicyExpenseChat?: boolean; selected?: boolean; From 1f979ee695dd528d99ce4d7e3c774f66abc02c8a Mon Sep 17 00:00:00 2001 From: Ishpaul Singh Date: Thu, 4 Jan 2024 21:21:13 +0530 Subject: [PATCH 106/391] fix lint --- src/pages/home/report/comment/TextCommentFragment.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/home/report/comment/TextCommentFragment.tsx b/src/pages/home/report/comment/TextCommentFragment.tsx index 3b92c0f6a118..790b98f574b5 100644 --- a/src/pages/home/report/comment/TextCommentFragment.tsx +++ b/src/pages/home/report/comment/TextCommentFragment.tsx @@ -1,7 +1,7 @@ import Str from 'expensify-common/lib/str'; import {isEmpty} from 'lodash'; import React, {memo} from 'react'; -import {type StyleProp, type TextStyle} from 'react-native'; +import type {StyleProp, TextStyle} from 'react-native'; import Text from '@components/Text'; import ZeroWidthView from '@components/ZeroWidthView'; import useLocalize from '@hooks/useLocalize'; From ad03aca8426f0ad13f65dc3b0fd75165a2a2a214 Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Fri, 5 Jan 2024 15:39:53 +0700 Subject: [PATCH 107/391] fix: 33073 --- src/components/ReportActionItem/MoneyRequestAction.js | 5 ----- src/pages/home/report/ReportActionItem.js | 2 +- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/src/components/ReportActionItem/MoneyRequestAction.js b/src/components/ReportActionItem/MoneyRequestAction.js index e0a3152a41b4..d159998b2d57 100644 --- a/src/components/ReportActionItem/MoneyRequestAction.js +++ b/src/components/ReportActionItem/MoneyRequestAction.js @@ -34,9 +34,6 @@ const propTypes = { /** The ID of the associated request report */ requestReportID: PropTypes.string.isRequired, - /** Is this IOUACTION the most recent? */ - isMostRecentIOUReportAction: PropTypes.bool.isRequired, - /** Popover context menu anchor, used for showing context menu */ contextMenuAnchor: refPropTypes, @@ -81,7 +78,6 @@ function MoneyRequestAction({ action, chatReportID, requestReportID, - isMostRecentIOUReportAction, contextMenuAnchor, checkIfContextMenuActive, chatReport, @@ -123,7 +119,6 @@ function MoneyRequestAction({ !_.isEmpty(iouReport) && !_.isEmpty(reportActions) && chatReport.iouReportID && - isMostRecentIOUReportAction && action.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD && network.isOffline ) { diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.js index 435c086d913f..20ddb127f48a 100644 --- a/src/pages/home/report/ReportActionItem.js +++ b/src/pages/home/report/ReportActionItem.js @@ -322,7 +322,7 @@ function ReportActionItem(props) { const iouReportID = originalMessage.IOUReportID ? originalMessage.IOUReportID.toString() : '0'; children = ( Date: Fri, 5 Jan 2024 15:51:29 +0700 Subject: [PATCH 108/391] remove most recent iou report action id --- .../ReportActionItem/MoneyRequestAction.js | 8 +------- src/pages/home/report/ReportActionItem.js | 5 ----- .../home/report/ReportActionItemParentAction.js | 1 - src/pages/home/report/ReportActionsList.js | 8 +------- .../home/report/ReportActionsListItemRenderer.js | 16 +--------------- src/pages/home/report/ReportActionsView.js | 4 ---- 6 files changed, 3 insertions(+), 39 deletions(-) diff --git a/src/components/ReportActionItem/MoneyRequestAction.js b/src/components/ReportActionItem/MoneyRequestAction.js index d159998b2d57..46226969636e 100644 --- a/src/components/ReportActionItem/MoneyRequestAction.js +++ b/src/components/ReportActionItem/MoneyRequestAction.js @@ -115,13 +115,7 @@ function MoneyRequestAction({ let shouldShowPendingConversionMessage = false; const isDeletedParentAction = ReportActionsUtils.isDeletedParentAction(action); const isReversedTransaction = ReportActionsUtils.isReversedTransaction(action); - if ( - !_.isEmpty(iouReport) && - !_.isEmpty(reportActions) && - chatReport.iouReportID && - action.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD && - network.isOffline - ) { + if (!_.isEmpty(iouReport) && !_.isEmpty(reportActions) && chatReport.iouReportID && action.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD && network.isOffline) { shouldShowPendingConversionMessage = IOUUtils.isIOUReportPendingCurrencyConversion(iouReport); } diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.js index 20ddb127f48a..4b5a43215ee5 100644 --- a/src/pages/home/report/ReportActionItem.js +++ b/src/pages/home/report/ReportActionItem.js @@ -88,9 +88,6 @@ const propTypes = { /** Should the comment have the appearance of being grouped with the previous comment? */ displayAsGroup: PropTypes.bool.isRequired, - /** Is this the most recent IOU Action? */ - isMostRecentIOUReportAction: PropTypes.bool.isRequired, - /** Should we display the new marker on top of the comment? */ shouldDisplayNewMarker: PropTypes.bool.isRequired, @@ -325,7 +322,6 @@ function ReportActionItem(props) { chatReportID={originalMessage.IOUReportID ? props.report.chatReportID : props.report.reportID} requestReportID={iouReportID} action={props.action} - isMostRecentIOUReportAction={props.isMostRecentIOUReportAction} isHovered={hovered} contextMenuAnchor={popoverAnchorRef} checkIfContextMenuActive={toggleContextMenuFromActiveReportAction} @@ -775,7 +771,6 @@ export default compose( (prevProps, nextProps) => prevProps.displayAsGroup === nextProps.displayAsGroup && prevProps.draftMessage === nextProps.draftMessage && - prevProps.isMostRecentIOUReportAction === nextProps.isMostRecentIOUReportAction && prevProps.shouldDisplayNewMarker === nextProps.shouldDisplayNewMarker && _.isEqual(prevProps.emojiReactions, nextProps.emojiReactions) && _.isEqual(prevProps.action, nextProps.action) && diff --git a/src/pages/home/report/ReportActionItemParentAction.js b/src/pages/home/report/ReportActionItemParentAction.js index c11200ccc4db..161c8048ab3d 100644 --- a/src/pages/home/report/ReportActionItemParentAction.js +++ b/src/pages/home/report/ReportActionItemParentAction.js @@ -70,7 +70,6 @@ function ReportActionItemParentAction(props) { report={props.report} action={parentReportAction} displayAsGroup={false} - isMostRecentIOUReportAction={false} shouldDisplayNewMarker={props.shouldDisplayNewMarker} index={0} /> diff --git a/src/pages/home/report/ReportActionsList.js b/src/pages/home/report/ReportActionsList.js index 2009dc9a102d..c7f48a38aeea 100644 --- a/src/pages/home/report/ReportActionsList.js +++ b/src/pages/home/report/ReportActionsList.js @@ -34,9 +34,6 @@ const propTypes = { /** 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, - /** The report metadata loading states */ isLoadingInitialReportActions: PropTypes.bool, @@ -73,7 +70,6 @@ const propTypes = { const defaultProps = { onScroll: () => {}, - mostRecentIOUReportActionID: '', isLoadingInitialReportActions: false, isLoadingOlderReportActions: false, isLoadingNewerReportActions: false, @@ -128,7 +124,6 @@ function ReportActionsList({ sortedReportActions, windowHeight, onScroll, - mostRecentIOUReportActionID, isSmallScreenWidth, personalDetailsList, currentUserPersonalDetails, @@ -398,12 +393,11 @@ function ReportActionsList({ report={report} linkedReportActionID={linkedReportActionID} displayAsGroup={ReportActionsUtils.isConsecutiveActionMadeByPreviousActor(sortedReportActions, index)} - mostRecentIOUReportActionID={mostRecentIOUReportActionID} shouldHideThreadDividerLine={shouldHideThreadDividerLine} shouldDisplayNewMarker={shouldDisplayNewMarker(reportAction, index)} /> ), - [report, linkedReportActionID, sortedReportActions, mostRecentIOUReportActionID, shouldHideThreadDividerLine, shouldDisplayNewMarker], + [report, linkedReportActionID, sortedReportActions, shouldHideThreadDividerLine, shouldDisplayNewMarker], ); // Native mobile does not render updates flatlist the changes even though component did update called. diff --git a/src/pages/home/report/ReportActionsListItemRenderer.js b/src/pages/home/report/ReportActionsListItemRenderer.js index ba47e804de06..01f4bc66d59d 100644 --- a/src/pages/home/report/ReportActionsListItemRenderer.js +++ b/src/pages/home/report/ReportActionsListItemRenderer.js @@ -22,9 +22,6 @@ const propTypes = { /** Should the comment have the appearance of being grouped with the previous comment? */ displayAsGroup: PropTypes.bool.isRequired, - /** The ID of the most recent IOU report action connected with the shown report */ - mostRecentIOUReportActionID: PropTypes.string, - /** If the thread divider line should be hidden */ shouldHideThreadDividerLine: PropTypes.bool.isRequired, @@ -36,20 +33,10 @@ const propTypes = { }; const defaultProps = { - mostRecentIOUReportActionID: '', linkedReportActionID: '', }; -function ReportActionsListItemRenderer({ - reportAction, - index, - report, - displayAsGroup, - mostRecentIOUReportActionID, - shouldHideThreadDividerLine, - shouldDisplayNewMarker, - linkedReportActionID, -}) { +function ReportActionsListItemRenderer({reportAction, index, report, displayAsGroup, shouldHideThreadDividerLine, shouldDisplayNewMarker, linkedReportActionID}) { const shouldDisplayParentAction = reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED && ReportUtils.isChatThread(report) && @@ -77,7 +64,6 @@ function ReportActionsListItemRenderer({ reportAction.actionName, ) } - isMostRecentIOUReportAction={reportAction.reportActionID === mostRecentIOUReportActionID} index={index} /> ); diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index 2758437a3962..ddcea7894251 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -14,7 +14,6 @@ import usePrevious from '@hooks/usePrevious'; import compose from '@libs/compose'; import getIsReportFullyVisible from '@libs/getIsReportFullyVisible'; import Performance from '@libs/Performance'; -import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import {isUserCreatedPolicyRoom} from '@libs/ReportUtils'; import {didUserLogInDuringSession} from '@libs/SessionUtils'; import {ReactionListContext} from '@pages/home/ReportScreenContext'; @@ -87,8 +86,6 @@ function ReportActionsView(props) { const didSubscribeToReportTypingEvents = useRef(false); const isFirstRender = useRef(true); const hasCachedActions = useInitialValue(() => _.size(props.reportActions) > 0); - const mostRecentIOUReportActionID = useInitialValue(() => ReportActionsUtils.getMostRecentIOURequestActionID(props.reportActions)); - const prevNetworkRef = useRef(props.network); const prevAuthTokenType = usePrevious(props.session.authTokenType); @@ -257,7 +254,6 @@ function ReportActionsView(props) { report={props.report} onLayout={recordTimeToMeasureItemLayout} sortedReportActions={props.reportActions} - mostRecentIOUReportActionID={mostRecentIOUReportActionID} loadOlderChats={loadOlderChats} loadNewerChats={loadNewerChats} isLoadingInitialReportActions={props.isLoadingInitialReportActions} From 5dbf2c96bebe469a6b64af701e2ef5f7738753ea Mon Sep 17 00:00:00 2001 From: tienifr Date: Fri, 5 Jan 2024 16:24:37 +0700 Subject: [PATCH 109/391] fix: Drop domain name in mentions if chat members are on the same domain --- .../HTMLRenderers/MentionUserRenderer.js | 23 ++++++++++++++++--- src/libs/LoginUtils.ts | 14 ++++++++++- 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.js b/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.js index 11ffabe4fe6a..477d6270d6bb 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.js +++ b/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.js @@ -16,6 +16,7 @@ import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; import * as ReportUtils from '@libs/ReportUtils'; import personalDetailsPropType from '@pages/personalDetailsPropType'; import CONST from '@src/CONST'; +import * as LoginUtils from '@src/libs/LoginUtils'; import ROUTES from '@src/ROUTES'; import htmlRendererPropTypes from './htmlRendererPropTypes'; @@ -37,15 +38,31 @@ function MentionUserRenderer(props) { let accountID; let displayNameOrLogin; let navigationRoute; + const tnode = props.tnode; + + const getMentionDisplayText = (displayText, accountId, userLogin = '') => { + if (accountId && userLogin !== displayText) { + return displayText; + } + if (!LoginUtils.areEmailsFromSamePrivateDomain(displayText, props.currentUserPersonalDetails.login)) { + return displayText; + } + + return displayText.split('@')[0]; + }; if (!_.isEmpty(htmlAttribAccountID)) { const user = lodashGet(personalDetails, htmlAttribAccountID); accountID = parseInt(htmlAttribAccountID, 10); displayNameOrLogin = LocalePhoneNumber.formatPhoneNumber(lodashGet(user, 'login', '')) || lodashGet(user, 'displayName', '') || translate('common.hidden'); + displayNameOrLogin = getMentionDisplayText(displayNameOrLogin, htmlAttribAccountID, lodashGet(user, 'login', '')); navigationRoute = ROUTES.PROFILE.getRoute(htmlAttribAccountID); - } else if (!_.isEmpty(props.tnode.data)) { + } else if (!_.isEmpty(tnode.data)) { + displayNameOrLogin = tnode.data; + tnode.data = tnode.data.replace(displayNameOrLogin, getMentionDisplayText(displayNameOrLogin, htmlAttribAccountID)); + // We need to remove the LTR unicode and leading @ from data as it is not part of the login - displayNameOrLogin = props.tnode.data.replace(CONST.UNICODE.LTR, '').slice(1); + displayNameOrLogin = getMentionDisplayText(displayNameOrLogin, htmlAttribAccountID); accountID = _.first(PersonalDetailsUtils.getAccountIDsByLogins([displayNameOrLogin])); navigationRoute = ROUTES.DETAILS.getRoute(displayNameOrLogin); @@ -83,7 +100,7 @@ function MentionUserRenderer(props) { // eslint-disable-next-line react/jsx-props-no-spreading {...defaultRendererProps} > - {!_.isEmpty(htmlAttribAccountID) ? `@${displayNameOrLogin}` : } + {!_.isEmpty(htmlAttribAccountID) ? `@${displayNameOrLogin}` : }
diff --git a/src/libs/LoginUtils.ts b/src/libs/LoginUtils.ts index 742f9bfe16ce..a8f73a457254 100644 --- a/src/libs/LoginUtils.ts +++ b/src/libs/LoginUtils.ts @@ -59,4 +59,16 @@ function getPhoneLogin(partnerUserID: string): string { return appendCountryCode(getPhoneNumberWithoutSpecialChars(partnerUserID)); } -export {getPhoneNumberWithoutSpecialChars, appendCountryCode, isEmailPublicDomain, validateNumber, getPhoneLogin}; +/** + * Check whether 2 emails have the same private domain + */ +function areEmailsFromSamePrivateDomain(email1: string, email2: string): boolean { + if (isEmailPublicDomain(email1) || isEmailPublicDomain(email2)) { + return false; + } + const emailDomain1 = Str.extractEmailDomain(email1).toLowerCase(); + const emailDomain2 = Str.extractEmailDomain(email2).toLowerCase(); + return emailDomain1 === emailDomain2; +} + +export {getPhoneNumberWithoutSpecialChars, appendCountryCode, isEmailPublicDomain, validateNumber, getPhoneLogin, areEmailsFromSamePrivateDomain}; From 216aad48bc422cc6599cb8e58692405855b160af Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Fri, 5 Jan 2024 13:29:59 +0100 Subject: [PATCH 110/391] Improve TextInput ref types --- src/components/Form/FormWrapper.tsx | 9 +-- src/components/Form/InputWrapper.tsx | 6 +- src/components/Form/types.ts | 5 +- src/components/RNTextInput.tsx | 10 +-- .../TextInput/BaseTextInput/index.native.tsx | 3 +- .../TextInput/BaseTextInput/index.tsx | 3 +- .../TextInput/BaseTextInput/types.ts | 5 +- src/components/TextInput/index.native.tsx | 3 +- src/components/TextInput/index.tsx | 7 +- ...DisplayNamePage.js => DisplayNamePage.tsx} | 64 +++++++------------ 10 files changed, 50 insertions(+), 65 deletions(-) rename src/pages/settings/Profile/{DisplayNamePage.js => DisplayNamePage.tsx} (67%) diff --git a/src/components/Form/FormWrapper.tsx b/src/components/Form/FormWrapper.tsx index 306afc10836f..b410a09ec6fa 100644 --- a/src/components/Form/FormWrapper.tsx +++ b/src/components/Form/FormWrapper.tsx @@ -1,4 +1,4 @@ -import type {MutableRefObject} from 'react'; +import type {RefObject} from 'react'; import React, {useCallback, useMemo, useRef} from 'react'; import type {StyleProp, View, ViewStyle} from 'react-native'; import {Keyboard, ScrollView} from 'react-native'; @@ -9,6 +9,7 @@ import FormSubmit from '@components/FormSubmit'; import SafeAreaConsumer from '@components/SafeAreaConsumer'; import type {SafeAreaChildrenProps} from '@components/SafeAreaConsumer/types'; import ScrollViewWithContext from '@components/ScrollViewWithContext'; +import type {BaseTextInputRef} from '@components/TextInput/BaseTextInput/types'; import useThemeStyles from '@hooks/useThemeStyles'; import * as ErrorUtils from '@libs/ErrorUtils'; import type ONYXKEYS from '@src/ONYXKEYS'; @@ -16,7 +17,7 @@ import type {Form} from '@src/types/onyx'; import type {Errors} from '@src/types/onyx/OnyxCommon'; import type ChildrenProps from '@src/types/utils/ChildrenProps'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; -import type {FormProps, InputRefs} from './types'; +import type {FormProps} from './types'; type FormWrapperOnyxProps = { /** Contains the form state that must be accessed outside the component */ @@ -33,7 +34,7 @@ type FormWrapperProps = ChildrenProps & errors: Errors; /** Assuming refs are React refs */ - inputRefs: MutableRefObject; + inputRefs: RefObject>>; }; function FormWrapper({ @@ -96,7 +97,7 @@ function FormWrapper({ // We measure relative to the content root, not the scroll view, as that gives // consistent results across mobile and web // eslint-disable-next-line @typescript-eslint/naming-convention - focusInput?.measureLayout?.(formContentRef.current, (_x, y) => + focusInput?.measureLayout?.(formContentRef.current, (_x: number, y: number) => formRef.current?.scrollTo({ y: y - 10, animated: false, diff --git a/src/components/Form/InputWrapper.tsx b/src/components/Form/InputWrapper.tsx index dd8014d28564..3ce37b95ee23 100644 --- a/src/components/Form/InputWrapper.tsx +++ b/src/components/Form/InputWrapper.tsx @@ -1,9 +1,11 @@ +import type {ForwardedRef} from 'react'; import React, {forwardRef, useContext} from 'react'; import TextInput from '@components/TextInput'; +import type {BaseTextInputRef} from '@components/TextInput/BaseTextInput/types'; import FormContext from './FormContext'; -import type {InputProps, InputRef, InputWrapperProps, ValidInput} from './types'; +import type {InputWrapperProps, ValidInput} from './types'; -function InputWrapper({InputComponent, inputID, valueType = 'string', ...rest}: InputWrapperProps, ref: InputRef) { +function InputWrapper({InputComponent, inputID, valueType = 'string', ...rest}: InputWrapperProps, ref: ForwardedRef) { const {registerInput} = useContext(FormContext); // There are inputs that don't have onBlur methods, to simulate the behavior of onBlur in e.g. checkbox, we had to diff --git a/src/components/Form/types.ts b/src/components/Form/types.ts index 6c1fcdf0c524..e4e4bd460e11 100644 --- a/src/components/Form/types.ts +++ b/src/components/Form/types.ts @@ -51,9 +51,6 @@ type FormProps = { footerContent?: ReactNode; }; -type InputRef = ForwardedRef; -type InputRefs = Record; - type RegisterInput = (inputID: string, props: InputProps) => InputProps; -export type {InputWrapperProps, ValidInput, FormProps, InputRef, InputRefs, RegisterInput, ValueType, OnyxFormValues, OnyxFormValuesFields, InputProps, OnyxFormKeyWithoutDraft}; +export type {InputWrapperProps, ValidInput, FormProps, RegisterInput, ValueType, OnyxFormValues, OnyxFormValuesFields, InputProps, OnyxFormKeyWithoutDraft}; diff --git a/src/components/RNTextInput.tsx b/src/components/RNTextInput.tsx index f7917a852704..526a5891df16 100644 --- a/src/components/RNTextInput.tsx +++ b/src/components/RNTextInput.tsx @@ -1,17 +1,17 @@ -import type {Component, ForwardedRef} from 'react'; +import type {ForwardedRef} from 'react'; import React from 'react'; // eslint-disable-next-line no-restricted-imports import type {TextInputProps} from 'react-native'; import {TextInput} from 'react-native'; -import type {AnimatedProps} from 'react-native-reanimated'; import Animated from 'react-native-reanimated'; import useTheme from '@hooks/useTheme'; -type AnimatedTextInputRef = Component>; // Convert the underlying TextInput into an Animated component so that we can take an animated ref and pass it to a worklet const AnimatedTextInput = Animated.createAnimatedComponent(TextInput); -function RNTextInputWithRef(props: TextInputProps, ref: ForwardedRef>>) { +type AnimatedTextInputRef = typeof AnimatedTextInput & TextInput; + +function RNTextInputWithRef(props: TextInputProps, ref: ForwardedRef) { const theme = useTheme(); return ( @@ -23,7 +23,7 @@ function RNTextInputWithRef(props: TextInputProps, ref: ForwardedRef, ) { const inputProps = {shouldSaveDraft: false, shouldUseDefaultValue: false, ...props}; const theme = useTheme(); diff --git a/src/components/TextInput/BaseTextInput/index.tsx b/src/components/TextInput/BaseTextInput/index.tsx index d548041b0cf8..7269e1c5f872 100644 --- a/src/components/TextInput/BaseTextInput/index.tsx +++ b/src/components/TextInput/BaseTextInput/index.tsx @@ -1,4 +1,5 @@ import Str from 'expensify-common/lib/str'; +import type {ForwardedRef} from 'react'; import React, {forwardRef, useCallback, useEffect, useMemo, useRef, useState} from 'react'; import {ActivityIndicator, Animated, StyleSheet, View} from 'react-native'; import type {GestureResponderEvent, LayoutChangeEvent, NativeSyntheticEvent, StyleProp, TextInput, TextInputFocusEventData, ViewStyle} from 'react-native'; @@ -57,7 +58,7 @@ function BaseTextInput( inputID, ...inputProps }: BaseTextInputProps, - ref: BaseTextInputRef, + ref: ForwardedRef, ) { const theme = useTheme(); const styles = useThemeStyles(); diff --git a/src/components/TextInput/BaseTextInput/types.ts b/src/components/TextInput/BaseTextInput/types.ts index f8376219d80f..972c8a6463e1 100644 --- a/src/components/TextInput/BaseTextInput/types.ts +++ b/src/components/TextInput/BaseTextInput/types.ts @@ -1,6 +1,5 @@ -import type {Component, ForwardedRef} from 'react'; import type {GestureResponderEvent, StyleProp, TextInputProps, TextStyle, ViewStyle} from 'react-native'; -import type {AnimatedProps} from 'react-native-reanimated'; +import type {AnimatedTextInputRef} from '@components/RNTextInput'; import type {MaybePhraseKey} from '@libs/Localize'; import type IconAsset from '@src/types/utils/IconAsset'; @@ -108,7 +107,7 @@ type CustomBaseTextInputProps = { autoCompleteType?: string; }; -type BaseTextInputRef = ForwardedRef>>; +type BaseTextInputRef = HTMLFormElement | AnimatedTextInputRef; type BaseTextInputProps = CustomBaseTextInputProps & TextInputProps; diff --git a/src/components/TextInput/index.native.tsx b/src/components/TextInput/index.native.tsx index 656f0657dd26..acc40295d575 100644 --- a/src/components/TextInput/index.native.tsx +++ b/src/components/TextInput/index.native.tsx @@ -1,10 +1,11 @@ +import type {ForwardedRef} from 'react'; import React, {forwardRef, useEffect} from 'react'; import {AppState, Keyboard} from 'react-native'; import useThemeStyles from '@hooks/useThemeStyles'; import BaseTextInput from './BaseTextInput'; import type {BaseTextInputProps, BaseTextInputRef} from './BaseTextInput/types'; -function TextInput(props: BaseTextInputProps, ref: BaseTextInputRef) { +function TextInput(props: BaseTextInputProps, ref: ForwardedRef) { const styles = useThemeStyles(); useEffect(() => { diff --git a/src/components/TextInput/index.tsx b/src/components/TextInput/index.tsx index 3043edbd26a5..75c4d52e0f86 100644 --- a/src/components/TextInput/index.tsx +++ b/src/components/TextInput/index.tsx @@ -1,3 +1,4 @@ +import type {ForwardedRef} from 'react'; import React, {useEffect, useRef} from 'react'; import type {StyleProp, ViewStyle} from 'react-native'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -10,9 +11,9 @@ import * as styleConst from './styleConst'; type RemoveVisibilityListener = () => void; -function TextInput(props: BaseTextInputProps, ref: BaseTextInputRef) { +function TextInput(props: BaseTextInputProps, ref: ForwardedRef) { const styles = useThemeStyles(); - const textInputRef = useRef(null); + const textInputRef = useRef(null); const removeVisibilityListenerRef = useRef(null); useEffect(() => { @@ -57,7 +58,7 @@ function TextInput(props: BaseTextInputProps, ref: BaseTextInputRef) { // eslint-disable-next-line react/jsx-props-no-spreading {...props} ref={(element) => { - textInputRef.current = element as HTMLElement; + textInputRef.current = element as HTMLFormElement; if (!ref) { return; diff --git a/src/pages/settings/Profile/DisplayNamePage.js b/src/pages/settings/Profile/DisplayNamePage.tsx similarity index 67% rename from src/pages/settings/Profile/DisplayNamePage.js rename to src/pages/settings/Profile/DisplayNamePage.tsx index 8ea471283004..22c1c173e637 100644 --- a/src/pages/settings/Profile/DisplayNamePage.js +++ b/src/pages/settings/Profile/DisplayNamePage.tsx @@ -1,5 +1,4 @@ -import lodashGet from 'lodash/get'; -import PropTypes from 'prop-types'; +/* eslint-disable @typescript-eslint/no-explicit-any */ import React from 'react'; import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; @@ -10,8 +9,8 @@ import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; import Text from '@components/Text'; import TextInput from '@components/TextInput'; -import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsDefaultProps, withCurrentUserPersonalDetailsPropTypes} from '@components/withCurrentUserPersonalDetails'; -import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; +import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalDetails'; +import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import compose from '@libs/compose'; import * as ErrorUtils from '@libs/ErrorUtils'; @@ -21,46 +20,32 @@ import * as PersonalDetails from '@userActions/PersonalDetails'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import { OnyxFormValuesFields } from '@components/Form/types'; -const propTypes = { - ...withLocalizePropTypes, - ...withCurrentUserPersonalDetailsPropTypes, - isLoadingApp: PropTypes.bool, -}; - -const defaultProps = { - ...withCurrentUserPersonalDetailsDefaultProps, - isLoadingApp: true, -}; - -/** - * Submit form to update user's first and last name (and display name) - * @param {Object} values - * @param {String} values.firstName - * @param {String} values.lastName - */ -const updateDisplayName = (values) => { +const updateDisplayName = (values: any) => { PersonalDetails.updateDisplayName(values.firstName.trim(), values.lastName.trim()); }; -function DisplayNamePage(props) { +function DisplayNamePage(props: any) { const styles = useThemeStyles(); + const {translate} = useLocalize(); const currentUserDetails = props.currentUserPersonalDetails || {}; /** - * @param {Object} values - * @param {String} values.firstName - * @param {String} values.lastName - * @returns {Object} - An object containing the errors for each inputID + * @param values + * @param values.firstName + * @param values.lastName + * @returns - An object containing the errors for each inputID */ - const validate = (values) => { + const validate = (values: OnyxFormValuesFields) => { + console.log(`values = `, values); const errors = {}; // First we validate the first name field if (!ValidationUtils.isValidDisplayName(values.firstName)) { ErrorUtils.addErrorMessage(errors, 'firstName', 'personalDetails.error.hasInvalidCharacter'); } - if (ValidationUtils.doesContainReservedWord(values.firstName, CONST.DISPLAY_NAME.RESERVED_FIRST_NAMES)) { + if (ValidationUtils.doesContainReservedWord(values.firstName, CONST.DISPLAY_NAME.RESERVED_FIRST_NAMES as string[])) { ErrorUtils.addErrorMessage(errors, 'firstName', 'personalDetails.error.containsReservedWord'); } @@ -78,7 +63,7 @@ function DisplayNamePage(props) { testID={DisplayNamePage.displayName} > Navigation.goBack(ROUTES.SETTINGS_PROFILE)} /> {props.isLoadingApp ? ( @@ -89,21 +74,21 @@ function DisplayNamePage(props) { formID={ONYXKEYS.FORMS.DISPLAY_NAME_FORM} validate={validate} onSubmit={updateDisplayName} - submitButtonText={props.translate('common.save')} + submitButtonText={translate('common.save')} enabledWhenOffline shouldValidateOnBlur shouldValidateOnChange > - {props.translate('displayNamePage.isShownOnProfile')} + {translate('displayNamePage.isShownOnProfile')} @@ -113,10 +98,10 @@ function DisplayNamePage(props) { InputComponent={TextInput} inputID="lastName" name="lname" - label={props.translate('common.lastName')} - aria-label={props.translate('common.lastName')} + label={translate('common.lastName')} + aria-label={translate('common.lastName')} role={CONST.ROLE.PRESENTATION} - defaultValue={lodashGet(currentUserDetails, 'lastName', '')} + defaultValue={currentUserDetails?.lastName ?? ''} maxLength={CONST.DISPLAY_NAME.MAX_LENGTH} spellCheck={false} /> @@ -127,12 +112,9 @@ function DisplayNamePage(props) { ); } -DisplayNamePage.propTypes = propTypes; -DisplayNamePage.defaultProps = defaultProps; DisplayNamePage.displayName = 'DisplayNamePage'; export default compose( - withLocalize, withCurrentUserPersonalDetails, withOnyx({ isLoadingApp: { From 77ef44201944c63e0e608ec9c0d3d491f8e7cfb0 Mon Sep 17 00:00:00 2001 From: Kamil Owczarz Date: Fri, 5 Jan 2024 19:12:56 +0100 Subject: [PATCH 111/391] Register input types tweaks --- src/ONYXKEYS.ts | 2 +- src/components/Form/FormProvider.tsx | 15 +++++++-------- src/components/Form/FormWrapper.tsx | 7 +++---- src/components/Form/InputWrapper.tsx | 1 + src/components/Form/types.ts | 16 +++++++++++++--- 5 files changed, 25 insertions(+), 16 deletions(-) diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index c29a2a74a37a..8aeaf4c22f26 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -531,4 +531,4 @@ type OnyxValues = { type OnyxKeyValue = OnyxEntry; export default ONYXKEYS; -export type {OnyxKey, FormTest, OnyxCollectionKey, OnyxValues, OnyxKeyValue, OnyxFormKey, OnyxKeysMap}; +export type {OnyxKey, OnyxCollectionKey, OnyxValues, OnyxKeyValue, OnyxFormKey, OnyxKeysMap}; diff --git a/src/components/Form/FormProvider.tsx b/src/components/Form/FormProvider.tsx index c4db7fcec290..6581cef8ac95 100644 --- a/src/components/Form/FormProvider.tsx +++ b/src/components/Form/FormProvider.tsx @@ -1,6 +1,6 @@ import lodashIsEqual from 'lodash/isEqual'; +import type {ForwardedRef, MutableRefObject, ReactNode} from 'react'; import React, {createRef, forwardRef, useCallback, useImperativeHandle, useMemo, useRef, useState} from 'react'; -import type {ForwardedRef, ReactNode} from 'react'; import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import * as ValidationUtils from '@libs/ValidationUtils'; @@ -84,7 +84,7 @@ function FormProvider( }: FormProviderProps, forwardedRef: ForwardedRef, ) { - const inputRefs = useRef({}); + const inputRefs = useRef({} as InputRefs); const touchedInputs = useRef>({}); const [inputValues, setInputValues] = useState(() => ({...draftValues})); const [errors, setErrors] = useState({}); @@ -206,11 +206,10 @@ function FormProvider( const registerInput: RegisterInput = useCallback( (inputID, inputProps) => { - const newRef: InputRef = inputRefs.current[inputID] ?? inputProps.ref ?? createRef(); + const newRef: MutableRefObject = inputRefs.current[inputID] ?? inputProps.ref ?? createRef(); if (inputRefs.current[inputID] !== newRef) { inputRefs.current[inputID] = newRef; } - if (inputProps.value !== undefined) { inputValues[inputID] = inputProps.value; } else if (inputProps.shouldSaveDraft && draftValues?.[inputID] !== undefined && inputValues[inputID] === undefined) { @@ -220,7 +219,7 @@ function FormProvider( inputValues[inputID] = inputProps.defaultValue; } else if (inputValues[inputID] === undefined) { // We want to initialize the input value if it's undefined - inputValues[inputID] = inputProps.defaultValue === undefined ? getInitialValueByType(inputProps.valueType) : inputProps.defaultValue; + inputValues[inputID] = inputProps.defaultValue ?? getInitialValueByType(inputProps.valueType); } const errorFields = formState?.errorFields?.[inputID] ?? {}; @@ -237,7 +236,7 @@ function FormProvider( typeof inputRef === 'function' ? (node) => { inputRef(node); - if (typeof newRef !== 'function') { + if (node && typeof newRef !== 'function') { newRef.current = node; } } @@ -279,7 +278,7 @@ function FormProvider( onBlur: (event) => { // Only run validation when user proactively blurs the input. if (Visibility.isVisible() && Visibility.hasFocus()) { - const relatedTarget = 'nativeEvent' in event ? event?.nativeEvent?.relatedTarget : undefined; + const relatedTarget = 'nativeEvent' in event && 'relatedTarget' in event.nativeEvent && event?.nativeEvent?.relatedTarget; const relatedTargetId = relatedTarget && 'id' in relatedTarget && typeof relatedTarget.id === 'string' && relatedTarget.id; // We delay the validation in order to prevent Checkbox loss of focus when // the user is focusing a TextInput and proceeds to toggle a CheckBox in @@ -301,7 +300,7 @@ function FormProvider( } inputProps.onBlur?.(event); }, - onInputChange: (value, key) => { + onInputChange: (value: unknown, key?: string) => { const inputKey = key ?? inputID; setInputValues((prevState) => { const newState = { diff --git a/src/components/Form/FormWrapper.tsx b/src/components/Form/FormWrapper.tsx index b410a09ec6fa..f1d32486de5e 100644 --- a/src/components/Form/FormWrapper.tsx +++ b/src/components/Form/FormWrapper.tsx @@ -1,5 +1,5 @@ -import type {RefObject} from 'react'; import React, {useCallback, useMemo, useRef} from 'react'; +import type {RefObject} from 'react'; import type {StyleProp, View, ViewStyle} from 'react-native'; import {Keyboard, ScrollView} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; @@ -9,7 +9,6 @@ import FormSubmit from '@components/FormSubmit'; import SafeAreaConsumer from '@components/SafeAreaConsumer'; import type {SafeAreaChildrenProps} from '@components/SafeAreaConsumer/types'; import ScrollViewWithContext from '@components/ScrollViewWithContext'; -import type {BaseTextInputRef} from '@components/TextInput/BaseTextInput/types'; import useThemeStyles from '@hooks/useThemeStyles'; import * as ErrorUtils from '@libs/ErrorUtils'; import type ONYXKEYS from '@src/ONYXKEYS'; @@ -17,7 +16,7 @@ import type {Form} from '@src/types/onyx'; import type {Errors} from '@src/types/onyx/OnyxCommon'; import type ChildrenProps from '@src/types/utils/ChildrenProps'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; -import type {FormProps} from './types'; +import type {FormProps, InputRefs} from './types'; type FormWrapperOnyxProps = { /** Contains the form state that must be accessed outside the component */ @@ -34,7 +33,7 @@ type FormWrapperProps = ChildrenProps & errors: Errors; /** Assuming refs are React refs */ - inputRefs: RefObject>>; + inputRefs: RefObject; }; function FormWrapper({ diff --git a/src/components/Form/InputWrapper.tsx b/src/components/Form/InputWrapper.tsx index 3ce37b95ee23..a12b181c07bd 100644 --- a/src/components/Form/InputWrapper.tsx +++ b/src/components/Form/InputWrapper.tsx @@ -14,6 +14,7 @@ function InputWrapper({InputComponent, inputID, value // For now this side effect happened only in `TextInput` components. const shouldSetTouchedOnBlurOnly = InputComponent === TextInput; + // TODO: Sometimes we return too many props with register input, so we need to consider if it's better to make the returned type more general and disregard the issue, or we would like to omit the unused props somehow. // eslint-disable-next-line react/jsx-props-no-spreading return ; } diff --git a/src/components/Form/types.ts b/src/components/Form/types.ts index e4e4bd460e11..d6a9463f188f 100644 --- a/src/components/Form/types.ts +++ b/src/components/Form/types.ts @@ -1,6 +1,7 @@ -import type {ForwardedRef, ReactNode} from 'react'; +import type {FocusEvent, MutableRefObject, ReactNode} from 'react'; import type {StyleProp, ViewStyle} from 'react-native'; import type TextInput from '@components/TextInput'; +import type {BaseTextInputRef} from '@components/TextInput/BaseTextInput/types'; import type {OnyxFormKey, OnyxValues} from '@src/ONYXKEYS'; import type {Form} from '@src/types/onyx'; @@ -8,7 +9,13 @@ type ValueType = 'string' | 'boolean' | 'date'; type ValidInput = typeof TextInput; -type InputProps = Parameters[0]; +type InputProps = Parameters[0] & { + shouldSetTouchedOnBlurOnly?: boolean; + onValueChange?: (value: unknown, key: string) => void; + onTouched?: (event: unknown) => void; + valueType?: ValueType; + onBlur: (event: FocusEvent | Parameters[0]['onBlur']>>[0]) => void; +}; type InputWrapperProps = InputProps & { InputComponent: TInput; @@ -53,4 +60,7 @@ type FormProps = { type RegisterInput = (inputID: string, props: InputProps) => InputProps; -export type {InputWrapperProps, ValidInput, FormProps, RegisterInput, ValueType, OnyxFormValues, OnyxFormValuesFields, InputProps, OnyxFormKeyWithoutDraft}; +type InputRef = BaseTextInputRef; +type InputRefs = Record>; + +export type {InputWrapperProps, ValidInput, FormProps, RegisterInput, ValueType, OnyxFormValues, OnyxFormValuesFields, InputProps, InputRef, InputRefs, OnyxFormKeyWithoutDraft}; From 2fd6c4e9d275377598c346ec5cce526da98e2c84 Mon Sep 17 00:00:00 2001 From: Ishpaul Singh Date: Sat, 6 Jan 2024 08:48:09 +0530 Subject: [PATCH 112/391] adding requested chnages --- .../home/report/ReportActionItemFragment.tsx | 21 ++++++++----------- .../home/report/ReportActionItemMessage.tsx | 5 ++--- .../home/report/ReportActionItemSingle.tsx | 2 ++ .../report/comment/TextCommentFragment.tsx | 12 +++++------ src/types/onyx/ReportAction.ts | 6 +++--- 5 files changed, 21 insertions(+), 25 deletions(-) diff --git a/src/pages/home/report/ReportActionItemFragment.tsx b/src/pages/home/report/ReportActionItemFragment.tsx index 6e9c6a581066..2b8eeccd7a0a 100644 --- a/src/pages/home/report/ReportActionItemFragment.tsx +++ b/src/pages/home/report/ReportActionItemFragment.tsx @@ -13,7 +13,6 @@ import CONST from '@src/CONST'; import type * as OnyxCommon from '@src/types/onyx/OnyxCommon'; import type {OriginalMessageSource} from '@src/types/onyx/OriginalMessage'; import type {Message} from '@src/types/onyx/ReportAction'; -import type {EmptyObject} from '@src/types/utils/EmptyObject'; import AttachmentCommentFragment from './comment/AttachmentCommentFragment'; import TextCommentFragment from './comment/TextCommentFragment'; @@ -24,9 +23,6 @@ type ReportActionItemFragmentProps = { /** The message fragment needing to be displayed */ fragment: Message; - /** If this fragment is attachment than has info? */ - attachmentInfo?: EmptyObject | File; - /** Message(text) of an IOU report action */ iouMessage?: string; @@ -62,6 +58,9 @@ type ReportActionItemFragmentProps = { }; function ReportActionItemFragment({ + pendingAction, + fragment, + accountID, iouMessage = '', isSingleLine = false, source = '', @@ -72,22 +71,20 @@ function ReportActionItemFragment({ isApprovedOrSubmittedReportAction = false, isFragmentContainingDisplayName = false, displayAsGroup = false, - ...props }: ReportActionItemFragmentProps) { const styles = useThemeStyles(); - const {fragment} = props; const {isOffline} = useNetwork(); const {translate} = useLocalize(); switch (fragment.type) { case 'COMMENT': { - const isPendingDelete = props.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE; + const isPendingDelete = pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE; // Threaded messages display "[Deleted message]" instead of being hidden altogether. // While offline we display the previous message with a strikethrough style. Once online we want to // immediately display "[Deleted message]" while the delete action is pending. - if ((!isOffline && isThreadParentMessage && isPendingDelete) || props.fragment.isDeletedParentAction) { + if ((!isOffline && isThreadParentMessage && isPendingDelete) || fragment.isDeletedParentAction) { return ${translate('parentReportAction.deletedMessage')}`} />; } @@ -105,7 +102,7 @@ function ReportActionItemFragment({ - {isFragmentContainingDisplayName ? convertToLTR(props.fragment.text) : props.fragment.text} + {isFragmentContainingDisplayName ? convertToLTR(fragment.text) : fragment.text} ) : ( @@ -150,7 +147,7 @@ function ReportActionItemFragment({ case 'OLD_MESSAGE': return OLD_MESSAGE; default: - return props.fragment.text; + return fragment.text; } } diff --git a/src/pages/home/report/ReportActionItemMessage.tsx b/src/pages/home/report/ReportActionItemMessage.tsx index 0e7a63874bf5..0a0963f3d167 100644 --- a/src/pages/home/report/ReportActionItemMessage.tsx +++ b/src/pages/home/report/ReportActionItemMessage.tsx @@ -1,6 +1,6 @@ import type {ReactElement} from 'react'; import React from 'react'; -import type {StyleProp, ViewStyle} from 'react-native'; +import type {StyleProp, ViewStyle, TextStyle} from 'react-native'; import {Text, View} from 'react-native'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -20,7 +20,7 @@ type ReportActionItemMessageProps = { displayAsGroup: boolean; /** Additional styles to add after local styles. */ - style?: StyleProp; + style?: StyleProp; /** Whether or not the message is hidden by moderation */ isHidden?: boolean; @@ -74,7 +74,6 @@ function ReportActionItemMessage({action, displayAsGroup, reportID, style, isHid fragment={fragment} iouMessage={iouMessage} isThreadParentMessage={ReportActionsUtils.isThreadParentMessage(action, reportID)} - attachmentInfo={action.attachmentInfo} pendingAction={action.pendingAction} source={action.originalMessage as OriginalMessageSource} accountID={action.actorAccountID ?? 0} diff --git a/src/pages/home/report/ReportActionItemSingle.tsx b/src/pages/home/report/ReportActionItemSingle.tsx index c8083a3316bf..a35d50082d8d 100644 --- a/src/pages/home/report/ReportActionItemSingle.tsx +++ b/src/pages/home/report/ReportActionItemSingle.tsx @@ -266,3 +266,5 @@ function ReportActionItemSingle({ ReportActionItemSingle.displayName = 'ReportActionItemSingle'; export default ReportActionItemSingle; + + diff --git a/src/pages/home/report/comment/TextCommentFragment.tsx b/src/pages/home/report/comment/TextCommentFragment.tsx index 790b98f574b5..1725c5a1cef7 100644 --- a/src/pages/home/report/comment/TextCommentFragment.tsx +++ b/src/pages/home/report/comment/TextCommentFragment.tsx @@ -65,13 +65,11 @@ function TextCommentFragment({iouMessage = '', ...props}: TextCommentFragmentPro ); } - const propsStyle = Array.isArray(props.style) ? props.style : [props.style]; - const containsOnlyEmojis = EmojiUtils.containsOnlyEmojis(text); const message = isEmpty(iouMessage) ? text : iouMessage; return ( - + {convertToLTR(message)} - {Boolean(fragment.isEdited) && ( + {!!fragment.isEdited && ( <> {' '} @@ -98,7 +96,7 @@ function TextCommentFragment({iouMessage = '', ...props}: TextCommentFragmentPro {translate('reportActionCompose.edited')} diff --git a/src/types/onyx/ReportAction.ts b/src/types/onyx/ReportAction.ts index b727bc40ce93..3144333f04ca 100644 --- a/src/types/onyx/ReportAction.ts +++ b/src/types/onyx/ReportAction.ts @@ -90,9 +90,9 @@ type LinkMetadata = { }; type Person = { - type?: string; + type: string; style?: string; - text?: string; + text: string; }; type ReportActionBase = { @@ -201,4 +201,4 @@ type ReportAction = ReportActionBase & OriginalMessage; type ReportActions = Record; export default ReportAction; -export type {ReportActions, ReportActionBase, Message, LinkMetadata}; +export type {ReportActions, ReportActionBase, Message, LinkMetadata, Person}; From 4f6b75ba4c49e12f704e8f9d3e584d9c12f196d3 Mon Sep 17 00:00:00 2001 From: Ishpaul Singh Date: Sat, 6 Jan 2024 08:48:19 +0530 Subject: [PATCH 113/391] lint fix --- src/pages/home/report/ReportActionItemMessage.tsx | 2 +- src/pages/home/report/ReportActionItemSingle.tsx | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/pages/home/report/ReportActionItemMessage.tsx b/src/pages/home/report/ReportActionItemMessage.tsx index 0a0963f3d167..3b5f53d69186 100644 --- a/src/pages/home/report/ReportActionItemMessage.tsx +++ b/src/pages/home/report/ReportActionItemMessage.tsx @@ -1,6 +1,6 @@ import type {ReactElement} from 'react'; import React from 'react'; -import type {StyleProp, ViewStyle, TextStyle} from 'react-native'; +import type {StyleProp, TextStyle, ViewStyle} from 'react-native'; import {Text, View} from 'react-native'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; diff --git a/src/pages/home/report/ReportActionItemSingle.tsx b/src/pages/home/report/ReportActionItemSingle.tsx index a35d50082d8d..c8083a3316bf 100644 --- a/src/pages/home/report/ReportActionItemSingle.tsx +++ b/src/pages/home/report/ReportActionItemSingle.tsx @@ -266,5 +266,3 @@ function ReportActionItemSingle({ ReportActionItemSingle.displayName = 'ReportActionItemSingle'; export default ReportActionItemSingle; - - From c41ebb3e182f7b1ad85ca23bbe9cbf11a22bef05 Mon Sep 17 00:00:00 2001 From: Ishpaul Singh Date: Sat, 6 Jan 2024 09:04:37 +0530 Subject: [PATCH 114/391] clean up --- .../home/report/comment/TextCommentFragment.tsx | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/pages/home/report/comment/TextCommentFragment.tsx b/src/pages/home/report/comment/TextCommentFragment.tsx index 1725c5a1cef7..7450dc14e6bf 100644 --- a/src/pages/home/report/comment/TextCommentFragment.tsx +++ b/src/pages/home/report/comment/TextCommentFragment.tsx @@ -37,10 +37,9 @@ type TextCommentFragmentProps = { iouMessage?: string; }; -function TextCommentFragment({iouMessage = '', ...props}: TextCommentFragmentProps) { +function TextCommentFragment({fragment, styleAsDeleted, source, style, displayAsGroup, iouMessage = ''}: TextCommentFragmentProps) { const theme = useTheme(); const styles = useThemeStyles(); - const {fragment, styleAsDeleted} = props; const {html = '', text} = fragment; const {translate} = useLocalize(); const {isSmallScreenWidth} = useWindowDimensions(); @@ -59,7 +58,7 @@ function TextCommentFragment({iouMessage = '', ...props}: TextCommentFragmentPro return ( ); @@ -69,16 +68,16 @@ function TextCommentFragment({iouMessage = '', ...props}: TextCommentFragmentPro const message = isEmpty(iouMessage) ? text : iouMessage; return ( - + {translate('reportActionCompose.edited')} From 9aeaff0c3add72399487fee8e72b834bb9de2170 Mon Sep 17 00:00:00 2001 From: Ishpaul Singh Date: Sat, 6 Jan 2024 09:29:31 +0530 Subject: [PATCH 115/391] fix unrelated type errors --- src/pages/home/report/ReportActionItemFragment.tsx | 2 +- src/pages/home/report/ReportActionItemSingle.tsx | 2 +- src/types/onyx/ReportAction.ts | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/pages/home/report/ReportActionItemFragment.tsx b/src/pages/home/report/ReportActionItemFragment.tsx index 2b8eeccd7a0a..7f8664fa2c25 100644 --- a/src/pages/home/report/ReportActionItemFragment.tsx +++ b/src/pages/home/report/ReportActionItemFragment.tsx @@ -21,7 +21,7 @@ type ReportActionItemFragmentProps = { accountID: number; /** The message fragment needing to be displayed */ - fragment: Message; + fragment: Message, /** Message(text) of an IOU report action */ iouMessage?: string; diff --git a/src/pages/home/report/ReportActionItemSingle.tsx b/src/pages/home/report/ReportActionItemSingle.tsx index c8083a3316bf..924eccb3eaf4 100644 --- a/src/pages/home/report/ReportActionItemSingle.tsx +++ b/src/pages/home/report/ReportActionItemSingle.tsx @@ -239,7 +239,7 @@ function ReportActionItemSingle({ // eslint-disable-next-line react/no-array-index-key key={`person-${action.reportActionID}-${index}`} accountID={actorAccountID ?? 0} - fragment={fragment} + fragment={{...fragment, type: fragment.type ?? '', text: fragment.text ?? ''}} delegateAccountID={action.delegateAccountID} isSingleLine actorIcon={icon} diff --git a/src/types/onyx/ReportAction.ts b/src/types/onyx/ReportAction.ts index 3144333f04ca..b727bc40ce93 100644 --- a/src/types/onyx/ReportAction.ts +++ b/src/types/onyx/ReportAction.ts @@ -90,9 +90,9 @@ type LinkMetadata = { }; type Person = { - type: string; + type?: string; style?: string; - text: string; + text?: string; }; type ReportActionBase = { @@ -201,4 +201,4 @@ type ReportAction = ReportActionBase & OriginalMessage; type ReportActions = Record; export default ReportAction; -export type {ReportActions, ReportActionBase, Message, LinkMetadata, Person}; +export type {ReportActions, ReportActionBase, Message, LinkMetadata}; From 0bc89aa7e76d567d24558c20bbc775028b6566c1 Mon Sep 17 00:00:00 2001 From: Ishpaul Singh Date: Sat, 6 Jan 2024 09:30:22 +0530 Subject: [PATCH 116/391] fix lint --- src/pages/home/report/ReportActionItemFragment.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/home/report/ReportActionItemFragment.tsx b/src/pages/home/report/ReportActionItemFragment.tsx index 7f8664fa2c25..2b8eeccd7a0a 100644 --- a/src/pages/home/report/ReportActionItemFragment.tsx +++ b/src/pages/home/report/ReportActionItemFragment.tsx @@ -21,7 +21,7 @@ type ReportActionItemFragmentProps = { accountID: number; /** The message fragment needing to be displayed */ - fragment: Message, + fragment: Message; /** Message(text) of an IOU report action */ iouMessage?: string; From e1a546f1d68f4258753f09351532da81787278ee Mon Sep 17 00:00:00 2001 From: Ishpaul Singh Date: Mon, 8 Jan 2024 15:53:38 +0530 Subject: [PATCH 117/391] fix type errors --- src/pages/home/report/ReportActionItemFragment.tsx | 2 +- src/pages/home/report/ReportActionItemSingle.tsx | 2 +- src/types/onyx/ReportAction.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pages/home/report/ReportActionItemFragment.tsx b/src/pages/home/report/ReportActionItemFragment.tsx index 2b8eeccd7a0a..01918b377c62 100644 --- a/src/pages/home/report/ReportActionItemFragment.tsx +++ b/src/pages/home/report/ReportActionItemFragment.tsx @@ -27,7 +27,7 @@ type ReportActionItemFragmentProps = { iouMessage?: string; /** The reportAction's source */ - source: OriginalMessageSource; + source?: OriginalMessageSource; /** Should this fragment be contained in a single line? */ isSingleLine?: boolean; diff --git a/src/pages/home/report/ReportActionItemSingle.tsx b/src/pages/home/report/ReportActionItemSingle.tsx index 924eccb3eaf4..3da16bda8331 100644 --- a/src/pages/home/report/ReportActionItemSingle.tsx +++ b/src/pages/home/report/ReportActionItemSingle.tsx @@ -155,7 +155,7 @@ function ReportActionItemSingle({ Navigation.navigate(ROUTES.REPORT_PARTICIPANTS.getRoute(iouReportID)); return; } - showUserDetails(action.delegateAccountID ? action.delegateAccountID : String(actorAccountID)); + showUserDetails(action.delegateAccountID ? String(action.delegateAccountID) : String(actorAccountID)); } }, [isWorkspaceActor, reportID, actorAccountID, action.delegateAccountID, iouReportID, displayAllActors]); diff --git a/src/types/onyx/ReportAction.ts b/src/types/onyx/ReportAction.ts index b727bc40ce93..39c6136ecea2 100644 --- a/src/types/onyx/ReportAction.ts +++ b/src/types/onyx/ReportAction.ts @@ -172,7 +172,7 @@ type ReportActionBase = { /** Is this action pending? */ pendingAction?: OnyxCommon.PendingAction; - delegateAccountID?: string; + delegateAccountID?: number; /** Server side errors keyed by microtime */ errors?: OnyxCommon.Errors; From 2e692714fd903ee985006d3476e8112d23e10ec6 Mon Sep 17 00:00:00 2001 From: Ishpaul Singh Date: Mon, 8 Jan 2024 15:56:54 +0530 Subject: [PATCH 118/391] fix broken test case --- tests/utils/collections/reportActions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/utils/collections/reportActions.ts b/tests/utils/collections/reportActions.ts index 747cbe5b6a1a..cc258e89c041 100644 --- a/tests/utils/collections/reportActions.ts +++ b/tests/utils/collections/reportActions.ts @@ -65,7 +65,7 @@ export default function createRandomReportAction(index: number): ReportAction { shouldShow: randBoolean(), lastModified: randPastDate().toISOString(), pendingAction: rand(Object.values(CONST.RED_BRICK_ROAD_PENDING_ACTION)), - delegateAccountID: index.toString(), + delegateAccountID: index, errors: {}, isAttachment: randBoolean(), }; From 15e2ecd4c705479166e7ebb74ffdc50a3bd85adb Mon Sep 17 00:00:00 2001 From: Ishpaul Singh Date: Mon, 8 Jan 2024 16:07:32 +0530 Subject: [PATCH 119/391] fix prettier diffs --- src/pages/home/report/ReportActionItemSingle.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/home/report/ReportActionItemSingle.tsx b/src/pages/home/report/ReportActionItemSingle.tsx index 3da16bda8331..ae5c3d75cfff 100644 --- a/src/pages/home/report/ReportActionItemSingle.tsx +++ b/src/pages/home/report/ReportActionItemSingle.tsx @@ -155,7 +155,7 @@ function ReportActionItemSingle({ Navigation.navigate(ROUTES.REPORT_PARTICIPANTS.getRoute(iouReportID)); return; } - showUserDetails(action.delegateAccountID ? String(action.delegateAccountID) : String(actorAccountID)); + showUserDetails(action.delegateAccountID ? String(action.delegateAccountID) : String(actorAccountID)); } }, [isWorkspaceActor, reportID, actorAccountID, action.delegateAccountID, iouReportID, displayAllActors]); From 29d4061ee30807bf605ce52d6b64f8ae4dee2179 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Mon, 8 Jan 2024 13:20:55 +0100 Subject: [PATCH 120/391] fix: resolve comments --- src/libs/OptionsListUtils.ts | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 03c9bb0c74b0..9ee33198970c 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -439,7 +439,7 @@ function getSearchText( function getAllReportErrors(report: OnyxEntry, reportActions: OnyxEntry): OnyxCommon.Errors { const reportErrors = report?.errors ?? {}; const reportErrorFields = report?.errorFields ?? {}; - let reportActionErrors = Object.values(reportActions ?? {}).reduce( + const reportActionErrors: OnyxCommon.Errors = Object.values(reportActions ?? {}).reduce( (prevReportActionErrors, action) => (!action || isEmptyObject(action.errors) ? prevReportActionErrors : {...prevReportActionErrors, ...action.errors}), {}, ); @@ -450,14 +450,14 @@ function getAllReportErrors(report: OnyxEntry, reportActions: OnyxEntry< const transactionID = parentReportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? parentReportAction?.originalMessage?.IOUTransactionID : null; const transaction = allTransactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]; if (TransactionUtils.hasMissingSmartscanFields(transaction ?? null) && !ReportUtils.isSettled(transaction?.reportID)) { - reportActionErrors = {...reportActionErrors, smartscan: ErrorUtils.getMicroSecondOnyxError('report.genericSmartscanFailureMessage')}; + reportActionErrors.smartscan = ErrorUtils.getMicroSecondOnyxError('report.genericSmartscanFailureMessage') as string; } } else if ((ReportUtils.isIOUReport(report) || ReportUtils.isExpenseReport(report)) && report?.ownerAccountID === currentUserAccountID) { if (ReportUtils.hasMissingSmartscanFields(report?.reportID ?? '') && !ReportUtils.isSettled(report?.reportID)) { - reportActionErrors = {...reportActionErrors, smartscan: ErrorUtils.getMicroSecondOnyxError('report.genericSmartscanFailureMessage')}; + reportActionErrors.smartscan = ErrorUtils.getMicroSecondOnyxError('report.genericSmartscanFailureMessage') as string; } } else if (ReportUtils.hasSmartscanError(Object.values(reportActions ?? {}))) { - reportActionErrors = {...reportActionErrors, smartscan: ErrorUtils.getMicroSecondOnyxError('report.genericSmartscanFailureMessage')}; + reportActionErrors.smartscan = ErrorUtils.getMicroSecondOnyxError('report.genericSmartscanFailureMessage') as string; } // All error objects related to the report. Each object in the sources contains error messages keyed by microtime @@ -1664,20 +1664,20 @@ function getFilteredOptions( personalDetails: OnyxEntry, betas: Beta[] = [], searchValue = '', - selectedOptions = [], - excludeLogins = [], + selectedOptions: Array> = [], + excludeLogins: string[] = [], includeOwnedWorkspaceChats = false, includeP2P = true, includeCategories = false, - categories = {}, - recentlyUsedCategories = [], + categories: PolicyCategories = {}, + recentlyUsedCategories: string[] = [], includeTags = false, - tags = {}, - recentlyUsedTags = [], + tags: Record = {}, + recentlyUsedTags: string[] = [], canInviteUser = true, includeSelectedOptions = false, includePolicyTaxRates = false, - policyTaxRates = {} as PolicyTaxRateWithDefault, + policyTaxRates: PolicyTaxRateWithDefault = {} as PolicyTaxRateWithDefault, ) { return getOptions(reports, personalDetails, { betas, @@ -1711,8 +1711,8 @@ function getShareDestinationOptions( personalDetails: OnyxEntry, betas: Beta[] = [], searchValue = '', - selectedOptions = [], - excludeLogins = [], + selectedOptions: Array> = [], + excludeLogins: string[] = [], includeOwnedWorkspaceChats = true, excludeUnknownUsers = true, ) { From 6c1f6e7f2f66f24dd83dc7b53ece52bcd969a294 Mon Sep 17 00:00:00 2001 From: Aldo Canepa Date: Mon, 8 Jan 2024 10:54:26 -0300 Subject: [PATCH 121/391] Use new command for updating waypoints (WIP) --- src/libs/actions/IOU.js | 18 +++++++++++++++++- src/pages/EditRequestDistancePage.js | 2 +- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index 21997023fbc8..9c00b5769a46 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -1055,7 +1055,7 @@ function updateMoneyRequestDate(transactionID, transactionThreadReportID, val) { } /** - * Updates the created date of a money request + * Updates the tag of a money request * * @param {String} transactionID * @param {Number} transactionThreadReportID @@ -1069,6 +1069,21 @@ function updateMoneyRequestTag(transactionID, transactionThreadReportID, tag) { API.write('UpdateMoneyRequestTag', params, onyxData); } +/** + * Updates the waypoints of a distance money request + * + * @param {String} transactionID + * @param {Number} transactionThreadReportID + * @param {Object} waypoints + */ +function updateMoneyRequestDistance(transactionID, transactionThreadReportID, waypoints) { + const transactionChanges = { + waypoints, + }; + const {params, onyxData} = getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, transactionChanges, true); + API.write('UpdateMoneyRequestDistance', params, onyxData); +} + /** * Edits an existing distance request * @@ -3509,6 +3524,7 @@ export { navigateToNextPage, updateMoneyRequestDate, updateMoneyRequestTag, + updateMoneyRequestDistance, updateMoneyRequestAmountAndCurrency, replaceReceipt, detachReceipt, diff --git a/src/pages/EditRequestDistancePage.js b/src/pages/EditRequestDistancePage.js index 0ea295c0780b..f3ea76a3390a 100644 --- a/src/pages/EditRequestDistancePage.js +++ b/src/pages/EditRequestDistancePage.js @@ -79,7 +79,7 @@ function EditRequestDistancePage({report, route, transaction, transactionBackup} return; } - IOU.editMoneyRequest(transaction, report.reportID, {waypoints}); + IOU.updateMoneyRequestDistance(transaction.transactionID, report.reportID, waypoints); // If the client is offline, then the modal can be closed as well (because there are no errors or other feedback to show them // until they come online again and sync with the server). From c4a696afd855fa91af4efcfb893c606328eca4c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Mon, 8 Jan 2024 17:10:05 +0100 Subject: [PATCH 122/391] WIP CODE, REVERT ME --- appIndex.js | 12 + index.js | 13 +- package-lock.json | 896 ++++++++++++----------- package.json | 3 +- src/libs/E2E/client.ts | 6 + src/libs/E2E/reactNativeLaunchingTest.ts | 2 +- tests/e2e/server/index.js | 54 ++ tests/e2e/server/routes.js | 2 + 8 files changed, 544 insertions(+), 444 deletions(-) create mode 100644 appIndex.js diff --git a/appIndex.js b/appIndex.js new file mode 100644 index 000000000000..2a3de088f934 --- /dev/null +++ b/appIndex.js @@ -0,0 +1,12 @@ +/** + * @format + */ +import {AppRegistry} from 'react-native'; +import {enableLegacyWebImplementation} from 'react-native-gesture-handler'; +import App from './src/App'; +import Config from './src/CONFIG'; +import additionalAppSetup from './src/setup'; + +enableLegacyWebImplementation(true); +AppRegistry.registerComponent(Config.APP_NAME, () => App); +additionalAppSetup(); diff --git a/index.js b/index.js index 2a3de088f934..f7d262e1271b 100644 --- a/index.js +++ b/index.js @@ -1,12 +1 @@ -/** - * @format - */ -import {AppRegistry} from 'react-native'; -import {enableLegacyWebImplementation} from 'react-native-gesture-handler'; -import App from './src/App'; -import Config from './src/CONFIG'; -import additionalAppSetup from './src/setup'; - -enableLegacyWebImplementation(true); -AppRegistry.registerComponent(Config.APP_NAME, () => App); -additionalAppSetup(); +require('./src/libs/E2E/reactNativeLaunchingTest'); diff --git a/package-lock.json b/package-lock.json index 15964d8c5f3e..5ce3428d2557 100644 --- a/package-lock.json +++ b/package-lock.json @@ -123,7 +123,8 @@ "save": "^2.4.0", "semver": "^7.5.2", "shim-keyboard-event-key": "^1.0.3", - "underscore": "^1.13.1" + "underscore": "^1.13.1", + "xml2js": "^0.6.2" }, "devDependencies": { "@actions/core": "1.10.0", @@ -3958,6 +3959,26 @@ "node": ">=8" } }, + "node_modules/@expo/config-plugins/node_modules/xml2js": { + "version": "0.4.23", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz", + "integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/@expo/config-plugins/node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "engines": { + "node": ">=4.0" + } + }, "node_modules/@expo/config-types": { "version": "45.0.0", "resolved": "https://registry.npmjs.org/@expo/config-types/-/config-types-45.0.0.tgz", @@ -22695,7 +22716,7 @@ "version": "2.0.6", "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==", - "license": "BSD-3-Clause" + "deprecated": "Use your platform's native atob() and btoa() methods instead" }, "node_modules/abbrev": { "version": "1.1.1", @@ -22763,6 +22784,34 @@ "node": ">=0.4.0" } }, + "node_modules/acorn-globals": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-7.0.1.tgz", + "integrity": "sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==", + "dependencies": { + "acorn": "^8.1.0", + "acorn-walk": "^8.0.2" + } + }, + "node_modules/acorn-globals/node_modules/acorn": { + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", + "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-globals/node_modules/acorn-walk": { + "version": "8.3.1", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.1.tgz", + "integrity": "sha512-TgUZgYvqZprrl7YldZNoa9OciCAyZR+Ejm9eXzKCmjsF5IKp/wgQ7Z/ZpjpGTIUPwrHQIcYeI8qDh4PsEwxMbw==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/acorn-jsx": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", @@ -28101,26 +28150,7 @@ "node_modules/cssom": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz", - "integrity": "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==", - "license": "MIT" - }, - "node_modules/cssstyle": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz", - "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==", - "license": "MIT", - "dependencies": { - "cssom": "~0.3.6" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cssstyle/node_modules/cssom": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", - "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", - "license": "MIT" + "integrity": "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==" }, "node_modules/csstype": { "version": "3.1.1", @@ -28157,20 +28187,6 @@ "dev": true, "license": "BSD-2-Clause" }, - "node_modules/data-urls": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-3.0.2.tgz", - "integrity": "sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==", - "license": "MIT", - "dependencies": { - "abab": "^2.0.6", - "whatwg-mimetype": "^3.0.0", - "whatwg-url": "^11.0.0" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/date-fns": { "version": "2.30.0", "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", @@ -28941,7 +28957,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz", "integrity": "sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==", - "license": "MIT", + "deprecated": "Use your platform's native DOMException instead", "dependencies": { "webidl-conversions": "^7.0.0" }, @@ -34586,18 +34602,6 @@ "wbuf": "^1.1.0" } }, - "node_modules/html-encoding-sniffer": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", - "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==", - "license": "MIT", - "dependencies": { - "whatwg-encoding": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/html-entities": { "version": "2.3.5", "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.3.5.tgz", @@ -37553,6 +37557,17 @@ "@types/yargs-parser": "*" } }, + "node_modules/jest-environment-jsdom/node_modules/acorn": { + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", + "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/jest-environment-jsdom/node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -37602,6 +37617,59 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "license": "MIT" }, + "node_modules/jest-environment-jsdom/node_modules/cssstyle": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz", + "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==", + "dependencies": { + "cssom": "~0.3.6" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-environment-jsdom/node_modules/cssstyle/node_modules/cssom": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", + "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==" + }, + "node_modules/jest-environment-jsdom/node_modules/data-urls": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-3.0.2.tgz", + "integrity": "sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==", + "dependencies": { + "abab": "^2.0.6", + "whatwg-mimetype": "^3.0.0", + "whatwg-url": "^11.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/jest-environment-jsdom/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/jest-environment-jsdom/node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/jest-environment-jsdom/node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -37611,6 +37679,72 @@ "node": ">=8" } }, + "node_modules/jest-environment-jsdom/node_modules/html-encoding-sniffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", + "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==", + "dependencies": { + "whatwg-encoding": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/jest-environment-jsdom/node_modules/jsdom": { + "version": "20.0.3", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-20.0.3.tgz", + "integrity": "sha512-SYhBvTh89tTfCD/CRdSOm13mOBa42iTaTyfyEWBdKcGdPxPtLFBXuHR8XHb33YNYaP+lLbmSvBTsnoesCNJEsQ==", + "dependencies": { + "abab": "^2.0.6", + "acorn": "^8.8.1", + "acorn-globals": "^7.0.0", + "cssom": "^0.5.0", + "cssstyle": "^2.3.0", + "data-urls": "^3.0.2", + "decimal.js": "^10.4.2", + "domexception": "^4.0.0", + "escodegen": "^2.0.0", + "form-data": "^4.0.0", + "html-encoding-sniffer": "^3.0.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.1", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.2", + "parse5": "^7.1.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^4.1.2", + "w3c-xmlserializer": "^4.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^2.0.0", + "whatwg-mimetype": "^3.0.0", + "whatwg-url": "^11.0.0", + "ws": "^8.11.0", + "xml-name-validator": "^4.0.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "canvas": "^2.5.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jest-environment-jsdom/node_modules/parse5": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", + "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", + "dependencies": { + "entities": "^4.4.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, "node_modules/jest-environment-jsdom/node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -37623,6 +37757,67 @@ "node": ">=8" } }, + "node_modules/jest-environment-jsdom/node_modules/tr46": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", + "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==", + "dependencies": { + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/jest-environment-jsdom/node_modules/w3c-xmlserializer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", + "integrity": "sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==", + "dependencies": { + "xml-name-validator": "^4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/jest-environment-jsdom/node_modules/whatwg-encoding": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", + "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/jest-environment-jsdom/node_modules/whatwg-mimetype": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", + "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", + "engines": { + "node": ">=12" + } + }, + "node_modules/jest-environment-jsdom/node_modules/whatwg-url": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz", + "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==", + "dependencies": { + "tr46": "^3.0.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/jest-environment-jsdom/node_modules/xml-name-validator": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", + "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", + "engines": { + "node": ">=12" + } + }, "node_modules/jest-environment-node": { "version": "29.4.1", "license": "MIT", @@ -39641,130 +39836,6 @@ "node": ">=12.0.0" } }, - "node_modules/jsdom": { - "version": "20.0.3", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-20.0.3.tgz", - "integrity": "sha512-SYhBvTh89tTfCD/CRdSOm13mOBa42iTaTyfyEWBdKcGdPxPtLFBXuHR8XHb33YNYaP+lLbmSvBTsnoesCNJEsQ==", - "license": "MIT", - "dependencies": { - "abab": "^2.0.6", - "acorn": "^8.8.1", - "acorn-globals": "^7.0.0", - "cssom": "^0.5.0", - "cssstyle": "^2.3.0", - "data-urls": "^3.0.2", - "decimal.js": "^10.4.2", - "domexception": "^4.0.0", - "escodegen": "^2.0.0", - "form-data": "^4.0.0", - "html-encoding-sniffer": "^3.0.0", - "http-proxy-agent": "^5.0.0", - "https-proxy-agent": "^5.0.1", - "is-potential-custom-element-name": "^1.0.1", - "nwsapi": "^2.2.2", - "parse5": "^7.1.1", - "saxes": "^6.0.0", - "symbol-tree": "^3.2.4", - "tough-cookie": "^4.1.2", - "w3c-xmlserializer": "^4.0.0", - "webidl-conversions": "^7.0.0", - "whatwg-encoding": "^2.0.0", - "whatwg-mimetype": "^3.0.0", - "whatwg-url": "^11.0.0", - "ws": "^8.11.0", - "xml-name-validator": "^4.0.0" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "canvas": "^2.5.0" - }, - "peerDependenciesMeta": { - "canvas": { - "optional": true - } - } - }, - "node_modules/jsdom/node_modules/acorn": { - "version": "8.10.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", - "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/jsdom/node_modules/acorn-globals": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-7.0.1.tgz", - "integrity": "sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==", - "license": "MIT", - "dependencies": { - "acorn": "^8.1.0", - "acorn-walk": "^8.0.2" - } - }, - "node_modules/jsdom/node_modules/acorn-walk": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", - "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/jsdom/node_modules/entities": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", - "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/jsdom/node_modules/form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/jsdom/node_modules/parse5": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", - "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", - "license": "MIT", - "dependencies": { - "entities": "^4.4.0" - }, - "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" - } - }, - "node_modules/jsdom/node_modules/saxes": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", - "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", - "license": "ISC", - "dependencies": { - "xmlchars": "^2.2.0" - }, - "engines": { - "node": ">=v12.22.7" - } - }, "node_modules/jsesc": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", @@ -44601,8 +44672,9 @@ } }, "node_modules/nwsapi": { - "version": "2.2.2", - "license": "MIT" + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.7.tgz", + "integrity": "sha512-ub5E4+FBPKwAZx0UwIQOjYWGHTEq5sPqHQNRN8Z9e4A7u3Tj1weLJsL59yH9vmvqEtBHaOmT6cYQKIZOxp35FQ==" }, "node_modules/ob1": { "version": "0.76.8", @@ -46526,8 +46598,7 @@ "node_modules/psl": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", - "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==", - "license": "MIT" + "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==" }, "node_modules/public-encrypt": { "version": "4.0.3", @@ -46581,9 +46652,9 @@ } }, "node_modules/punycode": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", - "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "engines": { "node": ">=6" } @@ -50236,6 +50307,17 @@ "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", "license": "ISC" }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, "node_modules/scheduler": { "version": "0.22.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.22.0.tgz", @@ -53165,8 +53247,9 @@ } }, "node_modules/tough-cookie": { - "version": "4.1.2", - "license": "BSD-3-Clause", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz", + "integrity": "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==", "dependencies": { "psl": "^1.1.33", "punycode": "^2.1.1", @@ -53181,23 +53264,10 @@ "version": "0.2.0", "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", - "license": "MIT", "engines": { "node": ">= 4.0.0" } }, - "node_modules/tr46": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", - "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==", - "license": "MIT", - "dependencies": { - "punycode": "^2.1.1" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/traverse": { "version": "0.6.7", "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.6.7.tgz", @@ -54486,18 +54556,6 @@ "pbf": "^3.2.1" } }, - "node_modules/w3c-xmlserializer": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", - "integrity": "sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==", - "license": "MIT", - "dependencies": { - "xml-name-validator": "^4.0.0" - }, - "engines": { - "node": ">=14" - } - }, "node_modules/wait-port": { "version": "0.2.14", "resolved": "https://registry.npmjs.org/wait-port/-/wait-port-0.2.14.tgz", @@ -54885,7 +54943,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", - "license": "BSD-2-Clause", "engines": { "node": ">=12" } @@ -55574,45 +55631,11 @@ "node": ">=0.8.0" } }, - "node_modules/whatwg-encoding": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", - "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==", - "license": "MIT", - "dependencies": { - "iconv-lite": "0.6.3" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/whatwg-fetch": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.2.tgz", "integrity": "sha512-bJlen0FcuU/0EMLrdbJ7zOnW6ITZLrZMIarMUVmdKtsGvZna8vxKYaexICWPfZ8qwf9fzNq+UEIZrnSaApt6RA==" }, - "node_modules/whatwg-mimetype": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", - "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", - "license": "MIT", - "engines": { - "node": ">=12" - } - }, - "node_modules/whatwg-url": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz", - "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==", - "license": "MIT", - "dependencies": { - "tr46": "^3.0.0", - "webidl-conversions": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/whatwg-url-without-unicode": { "version": "8.0.0-3", "resolved": "https://registry.npmjs.org/whatwg-url-without-unicode/-/whatwg-url-without-unicode-8.0.0-3.tgz", @@ -55950,9 +55973,9 @@ } }, "node_modules/ws": { - "version": "8.13.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz", - "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.16.0.tgz", + "integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==", "engines": { "node": ">=10.0.0" }, @@ -56004,20 +56027,10 @@ "uuid": "dist/bin/uuid" } }, - "node_modules/xml-name-validator": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", - "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", - "license": "Apache-2.0", - "engines": { - "node": ">=12" - } - }, "node_modules/xml2js": { - "version": "0.4.23", - "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz", - "integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==", - "license": "MIT", + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", + "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==", "dependencies": { "sax": ">=0.6.0", "xmlbuilder": "~11.0.0" @@ -59029,6 +59042,20 @@ "requires": { "has-flag": "^4.0.0" } + }, + "xml2js": { + "version": "0.4.23", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz", + "integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==", + "requires": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + } + }, + "xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==" } } }, @@ -72647,6 +72674,27 @@ } } }, + "acorn-globals": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-7.0.1.tgz", + "integrity": "sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==", + "requires": { + "acorn": "^8.1.0", + "acorn-walk": "^8.0.2" + }, + "dependencies": { + "acorn": { + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", + "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==" + }, + "acorn-walk": { + "version": "8.3.1", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.1.tgz", + "integrity": "sha512-TgUZgYvqZprrl7YldZNoa9OciCAyZR+Ejm9eXzKCmjsF5IKp/wgQ7Z/ZpjpGTIUPwrHQIcYeI8qDh4PsEwxMbw==" + } + } + }, "acorn-jsx": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", @@ -76538,21 +76586,6 @@ "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz", "integrity": "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==" }, - "cssstyle": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz", - "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==", - "requires": { - "cssom": "~0.3.6" - }, - "dependencies": { - "cssom": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", - "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==" - } - } - }, "csstype": { "version": "3.1.1" }, @@ -76581,16 +76614,6 @@ "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", "dev": true }, - "data-urls": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-3.0.2.tgz", - "integrity": "sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==", - "requires": { - "abab": "^2.0.6", - "whatwg-mimetype": "^3.0.0", - "whatwg-url": "^11.0.0" - } - }, "date-fns": { "version": "2.30.0", "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", @@ -81261,14 +81284,6 @@ "wbuf": "^1.1.0" } }, - "html-encoding-sniffer": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", - "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==", - "requires": { - "whatwg-encoding": "^2.0.0" - } - }, "html-entities": { "version": "2.3.5", "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.3.5.tgz", @@ -83316,6 +83331,11 @@ "@types/yargs-parser": "*" } }, + "acorn": { + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", + "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==" + }, "ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -83346,11 +83366,100 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, + "cssstyle": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz", + "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==", + "requires": { + "cssom": "~0.3.6" + }, + "dependencies": { + "cssom": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", + "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==" + } + } + }, + "data-urls": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-3.0.2.tgz", + "integrity": "sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==", + "requires": { + "abab": "^2.0.6", + "whatwg-mimetype": "^3.0.0", + "whatwg-url": "^11.0.0" + } + }, + "entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==" + }, + "form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + }, "has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" }, + "html-encoding-sniffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", + "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==", + "requires": { + "whatwg-encoding": "^2.0.0" + } + }, + "jsdom": { + "version": "20.0.3", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-20.0.3.tgz", + "integrity": "sha512-SYhBvTh89tTfCD/CRdSOm13mOBa42iTaTyfyEWBdKcGdPxPtLFBXuHR8XHb33YNYaP+lLbmSvBTsnoesCNJEsQ==", + "requires": { + "abab": "^2.0.6", + "acorn": "^8.8.1", + "acorn-globals": "^7.0.0", + "cssom": "^0.5.0", + "cssstyle": "^2.3.0", + "data-urls": "^3.0.2", + "decimal.js": "^10.4.2", + "domexception": "^4.0.0", + "escodegen": "^2.0.0", + "form-data": "^4.0.0", + "html-encoding-sniffer": "^3.0.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.1", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.2", + "parse5": "^7.1.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^4.1.2", + "w3c-xmlserializer": "^4.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^2.0.0", + "whatwg-mimetype": "^3.0.0", + "whatwg-url": "^11.0.0", + "ws": "^8.11.0", + "xml-name-validator": "^4.0.0" + } + }, + "parse5": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", + "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", + "requires": { + "entities": "^4.4.0" + } + }, "supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -83358,6 +83467,49 @@ "requires": { "has-flag": "^4.0.0" } + }, + "tr46": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", + "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==", + "requires": { + "punycode": "^2.1.1" + } + }, + "w3c-xmlserializer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", + "integrity": "sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==", + "requires": { + "xml-name-validator": "^4.0.0" + } + }, + "whatwg-encoding": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", + "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==", + "requires": { + "iconv-lite": "0.6.3" + } + }, + "whatwg-mimetype": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", + "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==" + }, + "whatwg-url": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz", + "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==", + "requires": { + "tr46": "^3.0.0", + "webidl-conversions": "^7.0.0" + } + }, + "xml-name-validator": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", + "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==" } } }, @@ -84762,91 +84914,6 @@ "integrity": "sha512-YtOli5Cmzy3q4dP26GraSOeAhqecewG04hoO8DY56CH4KJ9Fvv5qKWUCCo3HZob7esJQHCv6/+bnTy72xZZaVQ==", "dev": true }, - "jsdom": { - "version": "20.0.3", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-20.0.3.tgz", - "integrity": "sha512-SYhBvTh89tTfCD/CRdSOm13mOBa42iTaTyfyEWBdKcGdPxPtLFBXuHR8XHb33YNYaP+lLbmSvBTsnoesCNJEsQ==", - "requires": { - "abab": "^2.0.6", - "acorn": "^8.8.1", - "acorn-globals": "^7.0.0", - "cssom": "^0.5.0", - "cssstyle": "^2.3.0", - "data-urls": "^3.0.2", - "decimal.js": "^10.4.2", - "domexception": "^4.0.0", - "escodegen": "^2.0.0", - "form-data": "^4.0.0", - "html-encoding-sniffer": "^3.0.0", - "http-proxy-agent": "^5.0.0", - "https-proxy-agent": "^5.0.1", - "is-potential-custom-element-name": "^1.0.1", - "nwsapi": "^2.2.2", - "parse5": "^7.1.1", - "saxes": "^6.0.0", - "symbol-tree": "^3.2.4", - "tough-cookie": "^4.1.2", - "w3c-xmlserializer": "^4.0.0", - "webidl-conversions": "^7.0.0", - "whatwg-encoding": "^2.0.0", - "whatwg-mimetype": "^3.0.0", - "whatwg-url": "^11.0.0", - "ws": "^8.11.0", - "xml-name-validator": "^4.0.0" - }, - "dependencies": { - "acorn": { - "version": "8.10.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", - "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==" - }, - "acorn-globals": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-7.0.1.tgz", - "integrity": "sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==", - "requires": { - "acorn": "^8.1.0", - "acorn-walk": "^8.0.2" - } - }, - "acorn-walk": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", - "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==" - }, - "entities": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", - "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==" - }, - "form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - } - }, - "parse5": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", - "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", - "requires": { - "entities": "^4.4.0" - } - }, - "saxes": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", - "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", - "requires": { - "xmlchars": "^2.2.0" - } - } - } - }, "jsesc": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", @@ -88326,7 +88393,9 @@ "integrity": "sha512-4jbtZXNAsfZbAHiiqjLPBiCl16dES1zI4Hpzzxw61Tk+loF+sBDBKx1ICKKKwIqQ7M0mFn1TmkN7euSncWgHiQ==" }, "nwsapi": { - "version": "2.2.2" + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.7.tgz", + "integrity": "sha512-ub5E4+FBPKwAZx0UwIQOjYWGHTEq5sPqHQNRN8Z9e4A7u3Tj1weLJsL59yH9vmvqEtBHaOmT6cYQKIZOxp35FQ==" }, "ob1": { "version": "0.76.8", @@ -89723,9 +89792,9 @@ } }, "punycode": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", - "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==" + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==" }, "pusher-js": { "version": "8.3.0", @@ -92294,6 +92363,14 @@ "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" }, + "saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "requires": { + "xmlchars": "^2.2.0" + } + }, "scheduler": { "version": "0.22.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.22.0.tgz", @@ -94463,7 +94540,9 @@ "dev": true }, "tough-cookie": { - "version": "4.1.2", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz", + "integrity": "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==", "requires": { "psl": "^1.1.33", "punycode": "^2.1.1", @@ -94478,14 +94557,6 @@ } } }, - "tr46": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", - "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==", - "requires": { - "punycode": "^2.1.1" - } - }, "traverse": { "version": "0.6.7", "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.6.7.tgz", @@ -95390,14 +95461,6 @@ "pbf": "^3.2.1" } }, - "w3c-xmlserializer": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", - "integrity": "sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==", - "requires": { - "xml-name-validator": "^4.0.0" - } - }, "wait-port": { "version": "0.2.14", "resolved": "https://registry.npmjs.org/wait-port/-/wait-port-0.2.14.tgz", @@ -96154,33 +96217,11 @@ "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", "dev": true }, - "whatwg-encoding": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", - "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==", - "requires": { - "iconv-lite": "0.6.3" - } - }, "whatwg-fetch": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.2.tgz", "integrity": "sha512-bJlen0FcuU/0EMLrdbJ7zOnW6ITZLrZMIarMUVmdKtsGvZna8vxKYaexICWPfZ8qwf9fzNq+UEIZrnSaApt6RA==" }, - "whatwg-mimetype": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", - "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==" - }, - "whatwg-url": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz", - "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==", - "requires": { - "tr46": "^3.0.0", - "webidl-conversions": "^7.0.0" - } - }, "whatwg-url-without-unicode": { "version": "8.0.0-3", "resolved": "https://registry.npmjs.org/whatwg-url-without-unicode/-/whatwg-url-without-unicode-8.0.0-3.tgz", @@ -96432,9 +96473,9 @@ } }, "ws": { - "version": "8.13.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz", - "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.16.0.tgz", + "integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==", "requires": {} }, "x-default-browser": { @@ -96462,15 +96503,10 @@ } } }, - "xml-name-validator": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", - "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==" - }, "xml2js": { - "version": "0.4.23", - "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz", - "integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==", + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", + "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==", "requires": { "sax": ">=0.6.0", "xmlbuilder": "~11.0.0" diff --git a/package.json b/package.json index 29ade80b518d..e72c07ca5075 100644 --- a/package.json +++ b/package.json @@ -171,7 +171,8 @@ "save": "^2.4.0", "semver": "^7.5.2", "shim-keyboard-event-key": "^1.0.3", - "underscore": "^1.13.1" + "underscore": "^1.13.1", + "xml2js": "^0.6.2" }, "devDependencies": { "@actions/core": "1.10.0", diff --git a/src/libs/E2E/client.ts b/src/libs/E2E/client.ts index 472567cc6c1d..74f293be2839 100644 --- a/src/libs/E2E/client.ts +++ b/src/libs/E2E/client.ts @@ -89,10 +89,16 @@ const sendNativeCommand = (payload: NativeCommand) => }); }); +const getOTPCode = (): Promise => + fetch(`${SERVER_ADDRESS}${Routes.getOtpCode}`) + .then((res: Response): Promise => res.json()) + .then((otp: string) => otp); + export default { submitTestResults, submitTestDone, getTestConfig, getCurrentActiveTestConfig, sendNativeCommand, + getOTPCode, }; diff --git a/src/libs/E2E/reactNativeLaunchingTest.ts b/src/libs/E2E/reactNativeLaunchingTest.ts index cbd63270e736..dc687c61eb0b 100644 --- a/src/libs/E2E/reactNativeLaunchingTest.ts +++ b/src/libs/E2E/reactNativeLaunchingTest.ts @@ -69,5 +69,5 @@ E2EClient.getTestConfig() // start the usual app Performance.markStart('regularAppStart'); -import '../../../index'; +import '../../../appIndex'; Performance.markEnd('regularAppStart'); diff --git a/tests/e2e/server/index.js b/tests/e2e/server/index.js index 4c2e00126fd5..b2c2f1853320 100644 --- a/tests/e2e/server/index.js +++ b/tests/e2e/server/index.js @@ -54,6 +54,39 @@ const createListenerState = () => { return [listeners, addListener]; }; +const https = require('https'); + +function simpleHttpRequest(url, method = 'GET') { + return new Promise((resolve, reject) => { + const req = https.request(url, {method}, (res) => { + let data = ''; + res.on('data', (chunk) => { + data += chunk; + }); + res.on('end', () => { + resolve(data); + }); + }); + req.on('error', reject); + req.end(); + }); +} + +const parseString = require('xml2js').parseString; + +function simpleXMLToJSON(xml) { + // using xml2js + return new Promise((resolve, reject) => { + parseString(xml, (err, result) => { + if (err) { + reject(err); + return; + } + resolve(result); + }); + }); +} + /** * The test result object that a client might submit to the server. * @typedef TestResult @@ -146,6 +179,27 @@ const createServerInstance = () => { break; } + case Routes.getOtpCode: { + // Wait 10 seconds for the email to arrive + setTimeout(() => { + simpleHttpRequest('https://www.trashmail.de/inbox-api.php?name=expensify.testuser') + .then(simpleXMLToJSON) + .then(({feed}) => { + const firstEmailID = feed.entry[0].id; + // Get email content: + return simpleHttpRequest(`https://www.trashmail.de/mail-api.php?name=expensify.testuser&id=${firstEmailID}`).then(simpleXMLToJSON); + }) + .then(({feed}) => { + const content = feed.entry[0].content[0]; + // content is a string, find code using regex based on text "Use 259463 to sign" + const otpCode = content.match(/Use (\d+) to sign/)[1]; + console.debug('otpCode', otpCode); + res.end(otpCode); + }); + }, 10000); + break; + } + default: res.statusCode = 404; res.end('Page not found!'); diff --git a/tests/e2e/server/routes.js b/tests/e2e/server/routes.js index 84fc2f89fd9b..1128b5b0f8dc 100644 --- a/tests/e2e/server/routes.js +++ b/tests/e2e/server/routes.js @@ -10,4 +10,6 @@ module.exports = { // Commands to execute from the host machine (there are pre-defined types like scroll or type) testNativeCommand: '/test_native_command', + + getOtpCode: '/get_otp_code', }; From c5131b01eb4740ad0663ea2c28ab3929613bbba2 Mon Sep 17 00:00:00 2001 From: mkhutornyi Date: Tue, 9 Jan 2024 10:57:05 +0100 Subject: [PATCH 123/391] fix IOU - Amount is not preserved in Manual page when the amount is changed in confirmation page --- src/pages/iou/steps/MoneyRequestAmountForm.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/iou/steps/MoneyRequestAmountForm.js b/src/pages/iou/steps/MoneyRequestAmountForm.js index 536944f4a2d8..8775562d4476 100644 --- a/src/pages/iou/steps/MoneyRequestAmountForm.js +++ b/src/pages/iou/steps/MoneyRequestAmountForm.js @@ -132,9 +132,9 @@ function MoneyRequestAmountForm({amount, taxAmount, currency, isEditing, forward return; } initializeAmount(amount); - // we want to re-initialize the state only when the selected tab changes + // we want to re-initialize the state only when the selected tab or amount changes // eslint-disable-next-line react-hooks/exhaustive-deps - }, [selectedTab]); + }, [selectedTab, amount]); /** * Sets the selection and the amount accordingly to the value passed to the input From 7bf4ad6ad103f455687e420bebd1a7be4c6c2ce0 Mon Sep 17 00:00:00 2001 From: tienifr Date: Tue, 9 Jan 2024 17:14:11 +0700 Subject: [PATCH 124/391] fix remove ltf unicode --- .../HTMLRenderers/MentionUserRenderer.js | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.js b/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.js index 477d6270d6bb..7242b38a4fcb 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.js +++ b/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.js @@ -39,7 +39,7 @@ function MentionUserRenderer(props) { let displayNameOrLogin; let navigationRoute; const tnode = props.tnode; - + const getMentionDisplayText = (displayText, accountId, userLogin = '') => { if (accountId && userLogin !== displayText) { return displayText; @@ -58,11 +58,9 @@ function MentionUserRenderer(props) { displayNameOrLogin = getMentionDisplayText(displayNameOrLogin, htmlAttribAccountID, lodashGet(user, 'login', '')); navigationRoute = ROUTES.PROFILE.getRoute(htmlAttribAccountID); } else if (!_.isEmpty(tnode.data)) { - displayNameOrLogin = tnode.data; - tnode.data = tnode.data.replace(displayNameOrLogin, getMentionDisplayText(displayNameOrLogin, htmlAttribAccountID)); - // We need to remove the LTR unicode and leading @ from data as it is not part of the login - displayNameOrLogin = getMentionDisplayText(displayNameOrLogin, htmlAttribAccountID); + displayNameOrLogin = tnode.data.replace(CONST.UNICODE.LTR, '').slice(1); + tnode.data = tnode.data.replace(displayNameOrLogin, getMentionDisplayText(displayNameOrLogin, htmlAttribAccountID)); accountID = _.first(PersonalDetailsUtils.getAccountIDsByLogins([displayNameOrLogin])); navigationRoute = ROUTES.DETAILS.getRoute(displayNameOrLogin); @@ -89,7 +87,7 @@ function MentionUserRenderer(props) { Date: Tue, 9 Jan 2024 17:16:23 +0700 Subject: [PATCH 125/391] remove quote --- .../HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.js b/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.js index 7242b38a4fcb..a045118eb13f 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.js +++ b/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.js @@ -87,7 +87,7 @@ function MentionUserRenderer(props) { Date: Tue, 9 Jan 2024 17:24:22 +0700 Subject: [PATCH 126/391] lint fix --- .../HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.js b/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.js index a045118eb13f..e7712a2dcde1 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.js +++ b/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.js @@ -39,7 +39,7 @@ function MentionUserRenderer(props) { let displayNameOrLogin; let navigationRoute; const tnode = props.tnode; - + const getMentionDisplayText = (displayText, accountId, userLogin = '') => { if (accountId && userLogin !== displayText) { return displayText; From bd003728830a32ae638cdcb756813e604c7f477e Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Tue, 9 Jan 2024 14:31:32 +0100 Subject: [PATCH 127/391] remove empty line --- android/settings.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/settings.gradle b/android/settings.gradle index 40aefc6f2405..950ca5388131 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -18,4 +18,4 @@ include ':app' includeBuild('../node_modules/@react-native/gradle-plugin') apply from: new File(["node", "--print", "require.resolve('expo/package.json')"].execute(null, rootDir).text.trim(), "../scripts/autolinking.gradle") -useExpoModules() +useExpoModules() \ No newline at end of file From 0384181fd7a473e7d83efe96fce9177e660a9892 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Tue, 9 Jan 2024 14:32:18 +0100 Subject: [PATCH 128/391] add back empty lien at EOF --- android/settings.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/settings.gradle b/android/settings.gradle index 950ca5388131..40aefc6f2405 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -18,4 +18,4 @@ include ':app' includeBuild('../node_modules/@react-native/gradle-plugin') apply from: new File(["node", "--print", "require.resolve('expo/package.json')"].execute(null, rootDir).text.trim(), "../scripts/autolinking.gradle") -useExpoModules() \ No newline at end of file +useExpoModules() From 8170a79d9ebe005a6fae2afbb3ad686ecd68a984 Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Tue, 9 Jan 2024 15:15:43 +0100 Subject: [PATCH 129/391] migrate index.tsx to TypeScript --- src/components/PopoverMenu.tsx | 3 +- .../ThreeDotsMenu/{index.js => index.tsx} | 73 ++++++++----------- 2 files changed, 34 insertions(+), 42 deletions(-) rename src/components/ThreeDotsMenu/{index.js => index.tsx} (70%) diff --git a/src/components/PopoverMenu.tsx b/src/components/PopoverMenu.tsx index 502bdbf83b53..52119439de45 100644 --- a/src/components/PopoverMenu.tsx +++ b/src/components/PopoverMenu.tsx @@ -46,7 +46,7 @@ type PopoverMenuItem = { type PopoverModalProps = Pick; -type PopoverMenuProps = PopoverModalProps & { +type PopoverMenuProps = Partial & { /** Callback method fired when the user requests to close the modal */ onClose: () => void; @@ -175,3 +175,4 @@ function PopoverMenu({ PopoverMenu.displayName = 'PopoverMenu'; export default React.memo(PopoverMenu); +export type {PopoverMenuItem}; diff --git a/src/components/ThreeDotsMenu/index.js b/src/components/ThreeDotsMenu/index.tsx similarity index 70% rename from src/components/ThreeDotsMenu/index.js rename to src/components/ThreeDotsMenu/index.tsx index 150487b2aa57..a6bb0ea51858 100644 --- a/src/components/ThreeDotsMenu/index.js +++ b/src/components/ThreeDotsMenu/index.tsx @@ -1,10 +1,10 @@ -import PropTypes from 'prop-types'; import React, {useRef, useState} from 'react'; +import type {ViewStyle} from 'react-native'; import {View} from 'react-native'; -import _ from 'underscore'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; -import sourcePropTypes from '@components/Image/sourcePropTypes'; +import type {AnchorAlignment} from '@components/Popover/types'; +import type {PopoverMenuItem} from '@components/PopoverMenu'; import PopoverMenu from '@components/PopoverMenu'; import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; import Tooltip from '@components/Tooltip/PopoverAnchorTooltip'; @@ -13,68 +13,62 @@ import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import * as Browser from '@libs/Browser'; import CONST from '@src/CONST'; +import type {TranslationPaths} from '@src/languages/types'; +import type {AnchorPosition} from '@src/styles'; +import type IconAsset from '@src/types/utils/IconAsset'; import ThreeDotsMenuItemPropTypes from './ThreeDotsMenuItemPropTypes'; -const propTypes = { +type ThreeDotsMenuProps = { /** Tooltip for the popup icon */ - iconTooltip: PropTypes.string, + iconTooltip?: TranslationPaths; /** icon for the popup trigger */ - icon: PropTypes.oneOfType([PropTypes.string, sourcePropTypes]), + icon: IconAsset; /** Any additional styles to pass to the icon container. */ - // eslint-disable-next-line react/forbid-prop-types - iconStyles: PropTypes.arrayOf(PropTypes.object), + iconStyles?: ViewStyle[]; /** The fill color to pass into the icon. */ - iconFill: PropTypes.string, + iconFill?: string; /** Function to call on icon press */ - onIconPress: PropTypes.func, + onIconPress?: () => void; /** menuItems that'll show up on toggle of the popup menu */ - menuItems: ThreeDotsMenuItemPropTypes.isRequired, + menuItems: PopoverMenuItem[]; /** The anchor position of the menu */ - anchorPosition: PropTypes.shape({ - top: PropTypes.number, - right: PropTypes.number, - bottom: PropTypes.number, - left: PropTypes.number, - }).isRequired, + anchorPosition: AnchorPosition; /** The anchor alignment of the menu */ - anchorAlignment: PropTypes.shape({ - horizontal: PropTypes.oneOf(_.values(CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL)), - vertical: PropTypes.oneOf(_.values(CONST.MODAL.ANCHOR_ORIGIN_VERTICAL)), - }), + anchorAlignment?: AnchorAlignment; /** Whether the popover menu should overlay the current view */ - shouldOverlay: PropTypes.bool, + shouldOverlay?: boolean; /** Whether the menu is disabled */ - disabled: PropTypes.bool, + disabled?: boolean; /** Should we announce the Modal visibility changes? */ - shouldSetModalVisibility: PropTypes.bool, + shouldSetModalVisibility?: boolean; }; -const defaultProps = { - iconTooltip: 'common.more', - disabled: false, - iconFill: undefined, - iconStyles: [], - icon: Expensicons.ThreeDots, - onIconPress: () => {}, - anchorAlignment: { +function ThreeDotsMenu({ + iconTooltip = 'common.more', + icon = Expensicons.ThreeDots, + iconFill, + iconStyles = [], + onIconPress = () => {}, + menuItems, + anchorPosition, + anchorAlignment = { horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.LEFT, vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.TOP, // we assume that popover menu opens below the button, anchor is at TOP }, - shouldOverlay: false, - shouldSetModalVisibility: true, -}; - -function ThreeDotsMenu({iconTooltip, icon, iconFill, iconStyles, onIconPress, menuItems, anchorPosition, anchorAlignment, shouldOverlay, shouldSetModalVisibility, disabled}) { + shouldOverlay = false, + shouldSetModalVisibility = true, + disabled = false, +}: ThreeDotsMenuProps) { const theme = useTheme(); const styles = useThemeStyles(); const [isPopupMenuVisible, setPopupMenuVisible] = useState(false); @@ -119,7 +113,7 @@ function ThreeDotsMenu({iconTooltip, icon, iconFill, iconStyles, onIconPress, me > @@ -139,10 +133,7 @@ function ThreeDotsMenu({iconTooltip, icon, iconFill, iconStyles, onIconPress, me ); } -ThreeDotsMenu.propTypes = propTypes; -ThreeDotsMenu.defaultProps = defaultProps; ThreeDotsMenu.displayName = 'ThreeDotsMenu'; export default ThreeDotsMenu; - export {ThreeDotsMenuItemPropTypes}; From c5d8c4138094a39eb0ba600fdf88cfea0ff7da49 Mon Sep 17 00:00:00 2001 From: Ishpaul Singh <104348397+ishpaul777@users.noreply.github.com> Date: Tue, 9 Jan 2024 21:05:41 +0530 Subject: [PATCH 130/391] remove default prop iouMessage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Błażej Kustra <46095609+blazejkustra@users.noreply.github.com> --- src/pages/home/report/comment/TextCommentFragment.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/home/report/comment/TextCommentFragment.tsx b/src/pages/home/report/comment/TextCommentFragment.tsx index 7450dc14e6bf..763f8d2c143c 100644 --- a/src/pages/home/report/comment/TextCommentFragment.tsx +++ b/src/pages/home/report/comment/TextCommentFragment.tsx @@ -37,7 +37,7 @@ type TextCommentFragmentProps = { iouMessage?: string; }; -function TextCommentFragment({fragment, styleAsDeleted, source, style, displayAsGroup, iouMessage = ''}: TextCommentFragmentProps) { +function TextCommentFragment({fragment, styleAsDeleted, source, style, displayAsGroup, iouMessage}: TextCommentFragmentProps) { const theme = useTheme(); const styles = useThemeStyles(); const {html = '', text} = fragment; From 80f3867a83c71fd89e4b61160a6a7233bfd54291 Mon Sep 17 00:00:00 2001 From: tienifr Date: Wed, 10 Jan 2024 11:59:16 +0700 Subject: [PATCH 131/391] fix: Migrate ImageView to ts --- src/components/ImageView/index.native.js | 52 ------------ src/components/ImageView/index.native.tsx | 39 +++++++++ .../ImageView/{index.js => index.tsx} | 83 +++++++------------ src/components/ImageView/propTypes.js | 46 ---------- src/components/ImageView/types.ts | 50 +++++++++++ src/components/Lightbox.js | 2 +- src/components/MultiGestureCanvas/types.ts | 6 ++ 7 files changed, 124 insertions(+), 154 deletions(-) delete mode 100644 src/components/ImageView/index.native.js create mode 100644 src/components/ImageView/index.native.tsx rename src/components/ImageView/{index.js => index.tsx} (80%) delete mode 100644 src/components/ImageView/propTypes.js create mode 100644 src/components/ImageView/types.ts create mode 100644 src/components/MultiGestureCanvas/types.ts diff --git a/src/components/ImageView/index.native.js b/src/components/ImageView/index.native.js deleted file mode 100644 index 98349b213aa5..000000000000 --- a/src/components/ImageView/index.native.js +++ /dev/null @@ -1,52 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import Lightbox from '@components/Lightbox'; -import {zoomRangeDefaultProps, zoomRangePropTypes} from '@components/MultiGestureCanvas/propTypes'; -import {imageViewDefaultProps, imageViewPropTypes} from './propTypes'; - -/** - * On the native layer, we use a image library to handle zoom functionality - */ -const propTypes = { - ...imageViewPropTypes, - ...zoomRangePropTypes, - - /** Function for handle on press */ - onPress: PropTypes.func, - - /** Additional styles to add to the component */ - style: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.object), PropTypes.object]), -}; - -const defaultProps = { - ...imageViewDefaultProps, - ...zoomRangeDefaultProps, - - onPress: () => {}, - style: {}, -}; - -function ImageView({isAuthTokenRequired, url, onScaleChanged, onPress, style, zoomRange, onError, isUsedInCarousel, isSingleCarouselItem, carouselItemIndex, carouselActiveItemIndex}) { - const hasSiblingCarouselItems = isUsedInCarousel && !isSingleCarouselItem; - - return ( - - ); -} - -ImageView.propTypes = propTypes; -ImageView.defaultProps = defaultProps; -ImageView.displayName = 'ImageView'; - -export default ImageView; diff --git a/src/components/ImageView/index.native.tsx b/src/components/ImageView/index.native.tsx new file mode 100644 index 000000000000..74db0869f9e1 --- /dev/null +++ b/src/components/ImageView/index.native.tsx @@ -0,0 +1,39 @@ +import React from 'react'; +import Lightbox from '@components/Lightbox'; +import {zoomRangeDefaultProps} from '@components/MultiGestureCanvas/propTypes'; +import type * as ImageViewTypes from './types'; + +function ImageView({ + isAuthTokenRequired, + url, + onScaleChanged, + onPress = () => {}, + style = {}, + zoomRange = zoomRangeDefaultProps.zoomRange, + onError, + isUsedInCarousel, + isSingleCarouselItem, + carouselItemIndex, + carouselActiveItemIndex, +}: ImageViewTypes.ImageViewProps) { + const hasSiblingCarouselItems = isUsedInCarousel && !isSingleCarouselItem; + + return ( + + ); +} + +ImageView.displayName = 'ImageView'; + +export default ImageView; diff --git a/src/components/ImageView/index.js b/src/components/ImageView/index.tsx similarity index 80% rename from src/components/ImageView/index.js rename to src/components/ImageView/index.tsx index f16b37f328f5..b0bc2faed9fc 100644 --- a/src/components/ImageView/index.js +++ b/src/components/ImageView/index.tsx @@ -1,15 +1,18 @@ import React, {useCallback, useEffect, useRef, useState} from 'react'; -import {View} from 'react-native'; +import type {LayoutChangeEvent,GestureResponderEvent} from 'react-native'; +import { View} from 'react-native'; import FullscreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import Image from '@components/Image'; +import RESIZE_MODES from '@components/Image/resizeModes'; import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import CONST from '@src/CONST'; -import {imageViewDefaultProps, imageViewPropTypes} from './propTypes'; +import viewRef from '@src/types/utils/viewRef'; +import type * as ImageViewTypes from './types'; -function ImageView({isAuthTokenRequired, url, fileName, onError}) { +function ImageView({isAuthTokenRequired = false, url, fileName, onError = () => {}}: ImageViewTypes.ImageViewProps) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); const [isLoading, setIsLoading] = useState(true); @@ -27,16 +30,10 @@ function ImageView({isAuthTokenRequired, url, fileName, onError}) { const [zoomScale, setZoomScale] = useState(0); const [zoomDelta, setZoomDelta] = useState({offsetX: 0, offsetY: 0}); - const scrollableRef = useRef(null); + const scrollableRef = useRef(null); const canUseTouchScreen = DeviceCapabilities.canUseTouchScreen(); - /** - * @param {Number} newContainerWidth - * @param {Number} newContainerHeight - * @param {Number} newImageWidth - * @param {Number} newImageHeight - */ - const setScale = (newContainerWidth, newContainerHeight, newImageWidth, newImageHeight) => { + const setScale = (newContainerWidth: number, newContainerHeight: number, newImageWidth: number, newImageHeight: number) => { if (!newContainerWidth || !newImageWidth || !newContainerHeight || !newImageHeight) { return; } @@ -44,10 +41,7 @@ function ImageView({isAuthTokenRequired, url, fileName, onError}) { setZoomScale(newZoomScale); }; - /** - * @param {SyntheticEvent} e - */ - const onContainerLayoutChanged = (e) => { + const onContainerLayoutChanged = (e: LayoutChangeEvent) => { const {width, height} = e.nativeEvent.layout; setScale(width, height, imgWidth, imgHeight); @@ -55,12 +49,7 @@ function ImageView({isAuthTokenRequired, url, fileName, onError}) { setContainerWidth(width); }; - /** - * When open image, set image width, height. - * @param {Number} imageWidth - * @param {Number} imageHeight - */ - const setImageRegion = (imageWidth, imageHeight) => { + const setImageRegion = (imageWidth: number, imageHeight: number) => { if (imageHeight <= 0) { return; } @@ -78,32 +67,23 @@ function ImageView({isAuthTokenRequired, url, fileName, onError}) { setIsZoomed(false); }; - const imageLoad = ({nativeEvent}) => { + const imageLoad = ({nativeEvent}: {nativeEvent: ImageViewTypes.ImageLoadNativeEventData}) => { setImageRegion(nativeEvent.width, nativeEvent.height); setIsLoading(false); }; - /** - * @param {SyntheticEvent} e - */ - const onContainerPressIn = (e) => { + const onContainerPressIn = (e: GestureResponderEvent) => { const {pageX, pageY} = e.nativeEvent; setIsMouseDown(true); setInitialX(pageX); setInitialY(pageY); - setInitialScrollLeft(scrollableRef.current.scrollLeft); - setInitialScrollTop(scrollableRef.current.scrollTop); + setInitialScrollLeft(scrollableRef.current?.scrollLeft ?? 0); + setInitialScrollTop(scrollableRef.current?.scrollTop ?? 0); }; - /** - * Convert touch point to zoomed point - * @param {Boolean} x x point when click zoom - * @param {Boolean} y y point when click zoom - * @returns {Object} converted touch point - */ - const getScrollOffset = (x, y) => { - let offsetX; - let offsetY; + const getScrollOffset = (x: number, y: number) => { + let offsetX = 0; + let offsetY = 0; // Container size bigger than clicked position offset if (x <= containerWidth / 2) { @@ -121,10 +101,8 @@ function ImageView({isAuthTokenRequired, url, fileName, onError}) { return {offsetX, offsetY}; }; - /** - * @param {SyntheticEvent} e - */ - const onContainerPress = (e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const onContainerPress = (e: any) => { if (!isZoomed && !isDragging) { if (e.nativeEvent) { const {offsetX, offsetY} = e.nativeEvent; @@ -148,13 +126,10 @@ function ImageView({isAuthTokenRequired, url, fileName, onError}) { } }; - /** - * @param {SyntheticEvent} e - */ const trackPointerPosition = useCallback( - (e) => { + (e: MouseEvent) => { // Whether the pointer is released inside the ImageView - const isInsideImageView = scrollableRef.current.contains(e.nativeEvent.target); + const isInsideImageView = scrollableRef.current?.contains(e.target as Node); if (!isInsideImageView && isZoomed && isDragging && isMouseDown) { setIsDragging(false); @@ -165,14 +140,14 @@ function ImageView({isAuthTokenRequired, url, fileName, onError}) { ); const trackMovement = useCallback( - (e) => { + (e: MouseEvent) => { if (!isZoomed) { return; } - if (isDragging && isMouseDown) { - const x = e.nativeEvent.x; - const y = e.nativeEvent.y; + if (isDragging && isMouseDown && scrollableRef.current) { + const x = e.x; + const y = e.y; const moveX = initialX - x; const moveY = initialY - y; scrollableRef.current.scrollLeft = initialScrollLeft + moveX; @@ -218,7 +193,7 @@ function ImageView({isAuthTokenRequired, url, fileName, onError}) { style={isLoading || zoomScale === 0 ? undefined : [styles.w100, styles.h100]} // When Image dimensions are lower than the container boundary(zoomscale <= 1), use `contain` to render the image with natural dimensions. // Both `center` and `contain` keeps the image centered on both x and y axis. - resizeMode={zoomScale > 1 ? Image.resizeMode.center : Image.resizeMode.contain} + resizeMode={zoomScale > 1 ? RESIZE_MODES.center : RESIZE_MODES.contain} onLoadStart={imageLoadingStart} onLoad={imageLoad} onError={onError} @@ -229,7 +204,7 @@ function ImageView({isAuthTokenRequired, url, fileName, onError}) { } return ( @@ -249,7 +224,7 @@ function ImageView({isAuthTokenRequired, url, fileName, onError}) { source={{uri: url}} isAuthTokenRequired={isAuthTokenRequired} style={[styles.h100, styles.w100]} - resizeMode={Image.resizeMode.contain} + resizeMode={RESIZE_MODES.contain} onLoadStart={imageLoadingStart} onLoad={imageLoad} onError={onError} @@ -261,8 +236,6 @@ function ImageView({isAuthTokenRequired, url, fileName, onError}) { ); } -ImageView.propTypes = imageViewPropTypes; -ImageView.defaultProps = imageViewDefaultProps; ImageView.displayName = 'ImageView'; export default ImageView; diff --git a/src/components/ImageView/propTypes.js b/src/components/ImageView/propTypes.js deleted file mode 100644 index 3809d9aed043..000000000000 --- a/src/components/ImageView/propTypes.js +++ /dev/null @@ -1,46 +0,0 @@ -import PropTypes from 'prop-types'; - -const imageViewPropTypes = { - /** Whether source url requires authentication */ - isAuthTokenRequired: PropTypes.bool, - - /** Handles scale changed event in image zoom component. Used on native only */ - // eslint-disable-next-line react/no-unused-prop-types - onScaleChanged: PropTypes.func.isRequired, - - /** URL to full-sized image */ - url: PropTypes.string.isRequired, - - /** image file name */ - fileName: PropTypes.string.isRequired, - - /** Handles errors while displaying the image */ - onError: PropTypes.func, - - /** Whether this view is the active screen */ - isFocused: PropTypes.bool, - - /** Whether this AttachmentView is shown as part of a AttachmentCarousel */ - isUsedInCarousel: PropTypes.bool, - - /** When "isUsedInCarousel" is set to true, determines whether there is only one item in the carousel */ - isSingleCarouselItem: PropTypes.bool, - - /** The index of the carousel item */ - carouselItemIndex: PropTypes.number, - - /** The index of the currently active carousel item */ - carouselActiveItemIndex: PropTypes.number, -}; - -const imageViewDefaultProps = { - isAuthTokenRequired: false, - onError: () => {}, - isFocused: true, - isUsedInCarousel: false, - isSingleCarouselItem: false, - carouselItemIndex: 0, - carouselActiveItemIndex: 0, -}; - -export {imageViewPropTypes, imageViewDefaultProps}; diff --git a/src/components/ImageView/types.ts b/src/components/ImageView/types.ts new file mode 100644 index 000000000000..9bb3584955d4 --- /dev/null +++ b/src/components/ImageView/types.ts @@ -0,0 +1,50 @@ +import type ZoomRange from '@components/MultiGestureCanvas/types'; +import type {StyleProp, ViewStyle} from 'react-native'; + +type ImageViewProps = { + /** Whether source url requires authentication */ + isAuthTokenRequired?: boolean; + + /** Handles scale changed event in image zoom component. Used on native only */ + // eslint-disable-next-line react/no-unused-prop-types + onScaleChanged: (scale: number) => void; + + /** URL to full-sized image */ + url: string; + + /** image file name */ + fileName: string; + + /** Handles errors while displaying the image */ + onError?: () => void; + + /** Whether this view is the active screen */ + isFocused?: boolean; + + /** Whether this AttachmentView is shown as part of a AttachmentCarousel */ + isUsedInCarousel?: boolean; + + /** When "isUsedInCarousel" is set to true, determines whether there is only one item in the carousel */ + isSingleCarouselItem?: boolean; + + /** The index of the carousel item */ + carouselItemIndex?: number; + + /** The index of the currently active carousel item */ + carouselActiveItemIndex?: number; + + /** Function for handle on press */ + onPress?: () => void; + + /** Additional styles to add to the component */ + style?: StyleProp; + + /** Range of zoom that can be applied to the content by pinching or double tapping. */ + zoomRange?: ZoomRange; +}; + +type ImageLoadNativeEventData = { + width: number; + height: number; +}; +export type {ImageViewProps, ImageLoadNativeEventData}; diff --git a/src/components/Lightbox.js b/src/components/Lightbox.js index 45326edb4610..a941d23a5f16 100644 --- a/src/components/Lightbox.js +++ b/src/components/Lightbox.js @@ -44,7 +44,7 @@ const propTypes = { activeIndex: PropTypes.number, /** Additional styles to add to the component */ - style: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.object), PropTypes.object]), + style: PropTypes.oneOfType([PropTypes.bool, PropTypes.arrayOf(PropTypes.object), PropTypes.object]), }; const defaultProps = { diff --git a/src/components/MultiGestureCanvas/types.ts b/src/components/MultiGestureCanvas/types.ts new file mode 100644 index 000000000000..0242f045feef --- /dev/null +++ b/src/components/MultiGestureCanvas/types.ts @@ -0,0 +1,6 @@ +type ZoomRange = { + min: number; + max: number; +}; + +export default ZoomRange; From 5ffd8e7ad8fcd470a5820af21cae165552828af5 Mon Sep 17 00:00:00 2001 From: tienifr Date: Wed, 10 Jan 2024 12:27:15 +0700 Subject: [PATCH 132/391] lint fix --- src/components/ImageView/index.tsx | 4 ++-- src/components/ImageView/types.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/ImageView/index.tsx b/src/components/ImageView/index.tsx index b0bc2faed9fc..f0d43121f9ef 100644 --- a/src/components/ImageView/index.tsx +++ b/src/components/ImageView/index.tsx @@ -1,6 +1,6 @@ import React, {useCallback, useEffect, useRef, useState} from 'react'; -import type {LayoutChangeEvent,GestureResponderEvent} from 'react-native'; -import { View} from 'react-native'; +import type {GestureResponderEvent, LayoutChangeEvent} from 'react-native'; +import {View} from 'react-native'; import FullscreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import Image from '@components/Image'; import RESIZE_MODES from '@components/Image/resizeModes'; diff --git a/src/components/ImageView/types.ts b/src/components/ImageView/types.ts index 9bb3584955d4..80fabed0ee58 100644 --- a/src/components/ImageView/types.ts +++ b/src/components/ImageView/types.ts @@ -1,5 +1,5 @@ -import type ZoomRange from '@components/MultiGestureCanvas/types'; import type {StyleProp, ViewStyle} from 'react-native'; +import type ZoomRange from '@components/MultiGestureCanvas/types'; type ImageViewProps = { /** Whether source url requires authentication */ From cb9b1e01f49b4435765a1595a200dd9432ebabd1 Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Wed, 10 Jan 2024 08:44:56 +0100 Subject: [PATCH 133/391] use correct menuItems type --- src/components/HeaderWithBackButton/types.ts | 15 ++------------- src/components/ThreeDotsMenu/index.tsx | 2 +- src/pages/workspace/WorkspacesListRow.tsx | 4 ++-- 3 files changed, 5 insertions(+), 16 deletions(-) diff --git a/src/components/HeaderWithBackButton/types.ts b/src/components/HeaderWithBackButton/types.ts index 9ffb0b5ef2f3..0a427310e4e7 100644 --- a/src/components/HeaderWithBackButton/types.ts +++ b/src/components/HeaderWithBackButton/types.ts @@ -1,22 +1,11 @@ import type {ReactNode} from 'react'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; +import type {PopoverMenuItem} from '@components/PopoverMenu'; import type {Action} from '@hooks/useSingleExecution'; import type {StepCounterParams} from '@src/languages/types'; import type {AnchorPosition} from '@src/styles'; import type {PersonalDetails, Policy, Report} from '@src/types/onyx'; import type ChildrenProps from '@src/types/utils/ChildrenProps'; -import type IconAsset from '@src/types/utils/IconAsset'; - -type ThreeDotsMenuItems = { - /** An icon element displayed on the left side */ - icon?: IconAsset; - - /** Text label */ - text: string; - - /** A callback triggered when the item is selected */ - onSelected: () => void; -}; type HeaderWithBackButtonProps = Partial & { /** Title of the Header */ @@ -62,7 +51,7 @@ type HeaderWithBackButtonProps = Partial & { shouldDisableThreeDotsButton?: boolean; /** List of menu items for more(three dots) menu */ - threeDotsMenuItems?: ThreeDotsMenuItems[]; + threeDotsMenuItems?: PopoverMenuItem[]; /** The anchor position of the menu */ threeDotsAnchorPosition?: AnchorPosition; diff --git a/src/components/ThreeDotsMenu/index.tsx b/src/components/ThreeDotsMenu/index.tsx index a6bb0ea51858..ced33c6b2ef9 100644 --- a/src/components/ThreeDotsMenu/index.tsx +++ b/src/components/ThreeDotsMenu/index.tsx @@ -23,7 +23,7 @@ type ThreeDotsMenuProps = { iconTooltip?: TranslationPaths; /** icon for the popup trigger */ - icon: IconAsset; + icon?: IconAsset; /** Any additional styles to pass to the icon container. */ iconStyles?: ViewStyle[]; diff --git a/src/pages/workspace/WorkspacesListRow.tsx b/src/pages/workspace/WorkspacesListRow.tsx index d6bb3fb05385..5346ae6f5107 100755 --- a/src/pages/workspace/WorkspacesListRow.tsx +++ b/src/pages/workspace/WorkspacesListRow.tsx @@ -4,7 +4,7 @@ import type {ValueOf} from 'type-fest'; import Avatar from '@components/Avatar'; import Icon from '@components/Icon'; import * as Illustrations from '@components/Icon/Illustrations'; -import type {MenuItemProps} from '@components/MenuItem'; +import type {PopoverMenuItem} from '@components/PopoverMenu'; import Text from '@components/Text'; import ThreeDotsMenu from '@components/ThreeDotsMenu'; import type {WithCurrentUserPersonalDetailsProps} from '@components/withCurrentUserPersonalDetails'; @@ -34,7 +34,7 @@ type WorkspacesListRowProps = WithCurrentUserPersonalDetailsProps & { fallbackWorkspaceIcon?: AvatarSource; /** Items for the three dots menu */ - menuItems: MenuItemProps[]; + menuItems: PopoverMenuItem[]; /** Renders the component using big screen layout or small screen layout. When layoutWidth === WorkspaceListRowLayout.NONE, * component will return null to prevent layout from jumping on initial render and when parent width changes. */ From e23837306e753ff554119ff95dc718593be068b9 Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Wed, 10 Jan 2024 08:59:16 +0100 Subject: [PATCH 134/391] remove unused WorkspacesListRow component --- src/pages/workspace/WorkspacesListRow.tsx | 178 ---------------------- 1 file changed, 178 deletions(-) delete mode 100755 src/pages/workspace/WorkspacesListRow.tsx diff --git a/src/pages/workspace/WorkspacesListRow.tsx b/src/pages/workspace/WorkspacesListRow.tsx deleted file mode 100755 index 5346ae6f5107..000000000000 --- a/src/pages/workspace/WorkspacesListRow.tsx +++ /dev/null @@ -1,178 +0,0 @@ -import React, {useMemo} from 'react'; -import {View} from 'react-native'; -import type {ValueOf} from 'type-fest'; -import Avatar from '@components/Avatar'; -import Icon from '@components/Icon'; -import * as Illustrations from '@components/Icon/Illustrations'; -import type {PopoverMenuItem} from '@components/PopoverMenu'; -import Text from '@components/Text'; -import ThreeDotsMenu from '@components/ThreeDotsMenu'; -import type {WithCurrentUserPersonalDetailsProps} from '@components/withCurrentUserPersonalDetails'; -import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalDetails'; -import useLocalize from '@hooks/useLocalize'; -import useThemeStyles from '@hooks/useThemeStyles'; -import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; -import type {AvatarSource} from '@libs/UserUtils'; -import variables from '@styles/variables'; -import CONST from '@src/CONST'; -import type IconAsset from '@src/types/utils/IconAsset'; - -type WorkspacesListRowProps = WithCurrentUserPersonalDetailsProps & { - /** Name of the workspace */ - title: string; - - /** Account ID of the workspace's owner */ - ownerAccountID?: number; - - /** Type of workspace. Type personal is not valid in this context so it's omitted */ - workspaceType: typeof CONST.POLICY.TYPE.FREE | typeof CONST.POLICY.TYPE.CORPORATE | typeof CONST.POLICY.TYPE.TEAM; - - /** Icon to show next to the workspace name */ - workspaceIcon?: AvatarSource; - - /** Icon to be used when workspaceIcon is not present */ - fallbackWorkspaceIcon?: AvatarSource; - - /** Items for the three dots menu */ - menuItems: PopoverMenuItem[]; - - /** Renders the component using big screen layout or small screen layout. When layoutWidth === WorkspaceListRowLayout.NONE, - * component will return null to prevent layout from jumping on initial render and when parent width changes. */ - layoutWidth?: ValueOf; -}; - -const workspaceTypeIcon = (workspaceType: WorkspacesListRowProps['workspaceType']): IconAsset => { - switch (workspaceType) { - case CONST.POLICY.TYPE.FREE: - return Illustrations.HandCard; - case CONST.POLICY.TYPE.CORPORATE: - return Illustrations.ShieldYellow; - case CONST.POLICY.TYPE.TEAM: - return Illustrations.Mailbox; - default: - throw new Error(`Don't know which icon to serve for workspace type`); - } -}; - -function WorkspacesListRow({ - title, - menuItems, - workspaceIcon, - fallbackWorkspaceIcon, - ownerAccountID, - workspaceType, - currentUserPersonalDetails, - layoutWidth = CONST.LAYOUT_WIDTH.NONE, -}: WorkspacesListRowProps) { - const styles = useThemeStyles(); - const {translate} = useLocalize(); - - const ownerDetails = ownerAccountID && PersonalDetailsUtils.getPersonalDetailsByIDs([ownerAccountID], currentUserPersonalDetails.accountID)[0]; - - const userFriendlyWorkspaceType = useMemo(() => { - switch (workspaceType) { - case CONST.POLICY.TYPE.FREE: - return translate('workspace.type.free'); - case CONST.POLICY.TYPE.CORPORATE: - return translate('workspace.type.control'); - case CONST.POLICY.TYPE.TEAM: - return translate('workspace.type.collect'); - default: - throw new Error(`Don't know a friendly workspace name for this workspace type`); - } - }, [workspaceType, translate]); - - if (layoutWidth === CONST.LAYOUT_WIDTH.NONE) { - // To prevent layout from jumping or rendering for a split second, when - // isWide is undefined we don't assume anything and simply return null. - return null; - } - - const isWide = layoutWidth === CONST.LAYOUT_WIDTH.WIDE; - const isNarrow = layoutWidth === CONST.LAYOUT_WIDTH.NARROW; - - return ( - - - - - {title} - - {isNarrow && ( - - )} - - - {!!ownerDetails && ( - <> - - - - {PersonalDetailsUtils.getDisplayNameOrDefault(ownerDetails)} - - - {ownerDetails.login} - - - - )} - - - - - - {userFriendlyWorkspaceType} - - - {translate('workspace.common.plan')} - - - - {isWide && ( - - )} - - ); -} - -WorkspacesListRow.displayName = 'WorkspacesListRow'; - -export default withCurrentUserPersonalDetails(WorkspacesListRow); From 9f2907e1c2bda85c6d0fc5fd3fb4bf019788ebd4 Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Wed, 10 Jan 2024 09:16:29 +0100 Subject: [PATCH 135/391] wrap iconStyles type with StyleProp --- src/components/ThreeDotsMenu/index.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/ThreeDotsMenu/index.tsx b/src/components/ThreeDotsMenu/index.tsx index ced33c6b2ef9..ced2b67826a0 100644 --- a/src/components/ThreeDotsMenu/index.tsx +++ b/src/components/ThreeDotsMenu/index.tsx @@ -1,5 +1,5 @@ import React, {useRef, useState} from 'react'; -import type {ViewStyle} from 'react-native'; +import type {StyleProp, ViewStyle} from 'react-native'; import {View} from 'react-native'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; @@ -26,7 +26,7 @@ type ThreeDotsMenuProps = { icon?: IconAsset; /** Any additional styles to pass to the icon container. */ - iconStyles?: ViewStyle[]; + iconStyles?: StyleProp; /** The fill color to pass into the icon. */ iconFill?: string; @@ -57,7 +57,7 @@ function ThreeDotsMenu({ iconTooltip = 'common.more', icon = Expensicons.ThreeDots, iconFill, - iconStyles = [], + iconStyles, onIconPress = () => {}, menuItems, anchorPosition, @@ -107,7 +107,7 @@ function ThreeDotsMenu({ e.preventDefault(); }} ref={buttonRef} - style={[styles.touchableButtonImage, ...iconStyles]} + style={[styles.touchableButtonImage, iconStyles]} role={CONST.ROLE.BUTTON} accessibilityLabel={translate(iconTooltip)} > From 882b8c6e504d359246f6f5e30360b48136865228 Mon Sep 17 00:00:00 2001 From: hurali97 Date: Wed, 10 Jan 2024 14:29:29 +0500 Subject: [PATCH 136/391] feat: enable ProGuard --- android/app/build.gradle | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 645f36ef876a..256b21e39810 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -70,7 +70,7 @@ project.ext.envConfigFiles = [ /** * Set this to true to Run Proguard on Release builds to minify the Java bytecode. */ -def enableProguardInReleaseBuilds = false +def enableProguardInReleaseBuilds = true /** * The preferred build flavor of JavaScriptCore (JSC) @@ -150,8 +150,9 @@ android { } release { productFlavors.production.signingConfig signingConfigs.release + shrinkResources enableProguardInReleaseBuilds minifyEnabled enableProguardInReleaseBuilds - proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro" + proguardFiles getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" signingConfig null // buildTypes take precedence over productFlavors when it comes to the signing configuration, From 0ae9249bf2a27dc5caac44c8dba978a5af2ba71f Mon Sep 17 00:00:00 2001 From: hurali97 Date: Wed, 10 Jan 2024 14:29:44 +0500 Subject: [PATCH 137/391] feat: add ProGuard rules --- android/app/proguard-rules.pro | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/android/app/proguard-rules.pro b/android/app/proguard-rules.pro index 7dab035002a2..57650844b780 100644 --- a/android/app/proguard-rules.pro +++ b/android/app/proguard-rules.pro @@ -8,5 +8,5 @@ # http://developer.android.com/guide/developing/tools/proguard.html # Add any project specific keep options here: --keep class com.facebook.hermes.unicode.** { *; } --keep class com.facebook.jni.** { *; } +-keep class com.expensify.chat.BuildConfig { *; } +-keep, allowoptimization, allowobfuscation class expo.modules.** { *; } \ No newline at end of file From b20b0785dac4ee7f0d65b72b07a0ea69daf57f6c Mon Sep 17 00:00:00 2001 From: Ishpaul Singh Date: Wed, 10 Jan 2024 16:01:23 +0530 Subject: [PATCH 138/391] fix type error --- src/pages/home/report/comment/TextCommentFragment.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/home/report/comment/TextCommentFragment.tsx b/src/pages/home/report/comment/TextCommentFragment.tsx index 763f8d2c143c..8140c00f8c3f 100644 --- a/src/pages/home/report/comment/TextCommentFragment.tsx +++ b/src/pages/home/report/comment/TextCommentFragment.tsx @@ -37,7 +37,7 @@ type TextCommentFragmentProps = { iouMessage?: string; }; -function TextCommentFragment({fragment, styleAsDeleted, source, style, displayAsGroup, iouMessage}: TextCommentFragmentProps) { +function TextCommentFragment({fragment, styleAsDeleted, source, style, displayAsGroup, iouMessage=''}: TextCommentFragmentProps) { const theme = useTheme(); const styles = useThemeStyles(); const {html = '', text} = fragment; From 207484fe0739f89e5e9ab0ed99ba704d557e5a1c Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Wed, 10 Jan 2024 14:40:38 +0100 Subject: [PATCH 139/391] bring back WorkspacesListRow component --- src/pages/workspace/WorkspacesListRow.tsx | 178 ++++++++++++++++++++++ 1 file changed, 178 insertions(+) create mode 100644 src/pages/workspace/WorkspacesListRow.tsx diff --git a/src/pages/workspace/WorkspacesListRow.tsx b/src/pages/workspace/WorkspacesListRow.tsx new file mode 100644 index 000000000000..3f084b4f770b --- /dev/null +++ b/src/pages/workspace/WorkspacesListRow.tsx @@ -0,0 +1,178 @@ +import React, {useMemo} from 'react'; +import {View} from 'react-native'; +import type {ValueOf} from 'type-fest'; +import Avatar from '@components/Avatar'; +import Icon from '@components/Icon'; +import * as Illustrations from '@components/Icon/Illustrations'; +import type {PopoverMenuItem} from '@components/PopoverMenu'; +import Text from '@components/Text'; +import ThreeDotsMenu from '@components/ThreeDotsMenu'; +import type {WithCurrentUserPersonalDetailsProps} from '@components/withCurrentUserPersonalDetails'; +import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalDetails'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; +import type {AvatarSource} from '@libs/UserUtils'; +import variables from '@styles/variables'; +import CONST from '@src/CONST'; +import type IconAsset from '@src/types/utils/IconAsset'; + +type WorkspacesListRowProps = WithCurrentUserPersonalDetailsProps & { + /** Name of the workspace */ + title: string; + + /** Account ID of the workspace's owner */ + ownerAccountID?: number; + + /** Type of workspace. Type personal is not valid in this context so it's omitted */ + workspaceType: typeof CONST.POLICY.TYPE.FREE | typeof CONST.POLICY.TYPE.CORPORATE | typeof CONST.POLICY.TYPE.TEAM; + + /** Icon to show next to the workspace name */ + workspaceIcon?: AvatarSource; + + /** Icon to be used when workspaceIcon is not present */ + fallbackWorkspaceIcon?: AvatarSource; + + /** Items for the three dots menu */ + menuItems: PopoverMenuItem[]; + + /** Renders the component using big screen layout or small screen layout. When layoutWidth === WorkspaceListRowLayout.NONE, + * component will return null to prevent layout from jumping on initial render and when parent width changes. */ + layoutWidth?: ValueOf; +}; + +const workspaceTypeIcon = (workspaceType: WorkspacesListRowProps['workspaceType']): IconAsset => { + switch (workspaceType) { + case CONST.POLICY.TYPE.FREE: + return Illustrations.HandCard; + case CONST.POLICY.TYPE.CORPORATE: + return Illustrations.ShieldYellow; + case CONST.POLICY.TYPE.TEAM: + return Illustrations.Mailbox; + default: + throw new Error(`Don't know which icon to serve for workspace type`); + } +}; + +function WorkspacesListRow({ + title, + menuItems, + workspaceIcon, + fallbackWorkspaceIcon, + ownerAccountID, + workspaceType, + currentUserPersonalDetails, + layoutWidth = CONST.LAYOUT_WIDTH.NONE, +}: WorkspacesListRowProps) { + const styles = useThemeStyles(); + const {translate} = useLocalize(); + + const ownerDetails = ownerAccountID && PersonalDetailsUtils.getPersonalDetailsByIDs([ownerAccountID], currentUserPersonalDetails.accountID)[0]; + + const userFriendlyWorkspaceType = useMemo(() => { + switch (workspaceType) { + case CONST.POLICY.TYPE.FREE: + return translate('workspace.type.free'); + case CONST.POLICY.TYPE.CORPORATE: + return translate('workspace.type.control'); + case CONST.POLICY.TYPE.TEAM: + return translate('workspace.type.collect'); + default: + throw new Error(`Don't know a friendly workspace name for this workspace type`); + } + }, [workspaceType, translate]); + + if (layoutWidth === CONST.LAYOUT_WIDTH.NONE) { + // To prevent layout from jumping or rendering for a split second, when + // isWide is undefined we don't assume anything and simply return null. + return null; + } + + const isWide = layoutWidth === CONST.LAYOUT_WIDTH.WIDE; + const isNarrow = layoutWidth === CONST.LAYOUT_WIDTH.NARROW; + + return ( + + + + + {title} + + {isNarrow && ( + + )} + + + {!!ownerDetails && ( + <> + + + + {PersonalDetailsUtils.getDisplayNameOrDefault(ownerDetails)} + + + {ownerDetails.login} + + + + )} + + + + + + {userFriendlyWorkspaceType} + + + {translate('workspace.common.plan')} + + + + {isWide && ( + + )} + + ); +} + +WorkspacesListRow.displayName = 'WorkspacesListRow'; + +export default withCurrentUserPersonalDetails(WorkspacesListRow); From f45e662116e39b4794aee0c4739339491c2916ca Mon Sep 17 00:00:00 2001 From: someone-here Date: Wed, 10 Jan 2024 20:03:26 +0530 Subject: [PATCH 140/391] [TS migration] AvatarCropModal --- ...AvatarCropModal.js => AvatarCropModal.tsx} | 99 +++++++++---------- .../{ImageCropView.js => ImageCropView.tsx} | 66 +++++++------ .../AvatarCropModal/{Slider.js => Slider.tsx} | 40 ++++---- .../gestureHandlerPropTypes.js | 21 ---- src/components/Button/index.tsx | 2 +- .../Pressable/GenericPressable/types.ts | 2 +- .../Pressable/PressableWithDelayToggle.tsx | 2 +- .../Pressable/PressableWithoutFocus.tsx | 2 +- src/libs/ControlSelection/index.native.ts | 2 +- src/libs/ControlSelection/index.ts | 15 ++- src/libs/ControlSelection/types.ts | 8 +- src/types/utils/CustomRefObject.ts | 5 - 12 files changed, 114 insertions(+), 150 deletions(-) rename src/components/AvatarCropModal/{AvatarCropModal.js => AvatarCropModal.tsx} (87%) rename src/components/AvatarCropModal/{ImageCropView.js => ImageCropView.tsx} (68%) rename src/components/AvatarCropModal/{Slider.js => Slider.tsx} (69%) delete mode 100644 src/components/AvatarCropModal/gestureHandlerPropTypes.js delete mode 100644 src/types/utils/CustomRefObject.ts diff --git a/src/components/AvatarCropModal/AvatarCropModal.js b/src/components/AvatarCropModal/AvatarCropModal.tsx similarity index 87% rename from src/components/AvatarCropModal/AvatarCropModal.js rename to src/components/AvatarCropModal/AvatarCropModal.tsx index eb3e21c3ad9d..2a9174c6d7a3 100644 --- a/src/components/AvatarCropModal/AvatarCropModal.js +++ b/src/components/AvatarCropModal/AvatarCropModal.tsx @@ -1,6 +1,5 @@ -import PropTypes from 'prop-types'; import React, {useCallback, useEffect, useState} from 'react'; -import {ActivityIndicator, Image, View} from 'react-native'; +import {ActivityIndicator, Image, LayoutChangeEvent, View} from 'react-native'; import {GestureHandlerRootView} from 'react-native-gesture-handler'; import {interpolate, runOnUI, useAnimatedGestureHandler, useSharedValue, useWorkletCallback} from 'react-native-reanimated'; import Button from '@components/Button'; @@ -8,71 +7,62 @@ import HeaderGap from '@components/HeaderGap'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; -import sourcePropTypes from '@components/Image/sourcePropTypes'; import Modal from '@components/Modal'; import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; import ScreenWrapper from '@components/ScreenWrapper'; import Text from '@components/Text'; import Tooltip from '@components/Tooltip'; -import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; -import withWindowDimensions, {windowDimensionsPropTypes} from '@components/withWindowDimensions'; +import useLocalize from '@hooks/useLocalize'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; +import useWindowDimensions from '@hooks/useWindowDimensions'; import compose from '@libs/compose'; import cropOrRotateImage from '@libs/cropOrRotateImage'; +import {CustomRNImageManipulatorResult} from '@libs/cropOrRotateImage/types'; import CONST from '@src/CONST'; +import IconAsset from '@src/types/utils/IconAsset'; import ImageCropView from './ImageCropView'; import Slider from './Slider'; -const propTypes = { +type AvatarCropModalProps = { /** Link to image for cropping */ - imageUri: PropTypes.string, + imageUri: string; /** Name of the image */ - imageName: PropTypes.string, + imageName: string; /** Type of the image file */ - imageType: PropTypes.string, + imageType: string; /** Callback to be called when user closes the modal */ - onClose: PropTypes.func, + onClose: () => void; /** Callback to be called when user saves the image */ - onSave: PropTypes.func, + onSave: (image: File | CustomRNImageManipulatorResult) => void; /** Modal visibility */ - isVisible: PropTypes.bool.isRequired, + isVisible: boolean; /** Image crop vector mask */ - maskImage: sourcePropTypes, - - ...withLocalizePropTypes, - ...windowDimensionsPropTypes, -}; - -const defaultProps = { - imageUri: '', - imageName: '', - imageType: '', - onClose: () => {}, - onSave: () => {}, - maskImage: undefined, + maskImage?: IconAsset; }; // This component can't be written using class since reanimated API uses hooks. -function AvatarCropModal(props) { +function AvatarCropModal({imageUri = '', imageName = '', imageType = '', onClose = () => {}, onSave = () => {}, ...props}: AvatarCropModalProps) { const theme = useTheme(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); - const originalImageWidth = useSharedValue(CONST.AVATAR_CROP_MODAL.INITIAL_SIZE); - const originalImageHeight = useSharedValue(CONST.AVATAR_CROP_MODAL.INITIAL_SIZE); + const originalImageWidth = useSharedValue(CONST.AVATAR_CROP_MODAL.INITIAL_SIZE); + const originalImageHeight = useSharedValue(CONST.AVATAR_CROP_MODAL.INITIAL_SIZE); const translateY = useSharedValue(0); const translateX = useSharedValue(0); - const scale = useSharedValue(CONST.AVATAR_CROP_MODAL.MIN_SCALE); + const scale = useSharedValue(CONST.AVATAR_CROP_MODAL.MIN_SCALE); const rotation = useSharedValue(0); const translateSlider = useSharedValue(0); const isPressableEnabled = useSharedValue(true); + const {translate} = useLocalize(); + const {isSmallScreenWidth} = useWindowDimensions(); // Check if image cropping, saving or uploading is in progress const isLoading = useSharedValue(false); @@ -82,13 +72,13 @@ function AvatarCropModal(props) { const prevMaxOffsetX = useSharedValue(0); const prevMaxOffsetY = useSharedValue(0); - const [imageContainerSize, setImageContainerSize] = useState(CONST.AVATAR_CROP_MODAL.INITIAL_SIZE); - const [sliderContainerSize, setSliderContainerSize] = useState(CONST.AVATAR_CROP_MODAL.INITIAL_SIZE); + const [imageContainerSize, setImageContainerSize] = useState(CONST.AVATAR_CROP_MODAL.INITIAL_SIZE); + const [sliderContainerSize, setSliderContainerSize] = useState(CONST.AVATAR_CROP_MODAL.INITIAL_SIZE); const [isImageContainerInitialized, setIsImageContainerInitialized] = useState(false); const [isImageInitialized, setIsImageInitialized] = useState(false); // An onLayout callback, that initializes the image container, for proper render of an image - const initializeImageContainer = useCallback((event) => { + const initializeImageContainer = useCallback((event: LayoutChangeEvent) => { setIsImageContainerInitialized(true); const {height, width} = event.nativeEvent.layout; @@ -98,7 +88,7 @@ function AvatarCropModal(props) { }, []); // An onLayout callback, that initializes the slider container size, for proper render of a slider - const initializeSliderContainer = useCallback((event) => { + const initializeSliderContainer = useCallback((event: LayoutChangeEvent) => { setSliderContainerSize(event.nativeEvent.layout.width); }, []); @@ -122,7 +112,6 @@ function AvatarCropModal(props) { // In order to calculate proper image position/size/animation, we have to know its size. // And we have to update image size if image url changes. - const imageUri = props.imageUri; useEffect(() => { if (!imageUri) { return; @@ -148,7 +137,7 @@ function AvatarCropModal(props) { * @param {Array} minMax * @returns {Number} */ - const clamp = useWorkletCallback((value, [min, max]) => interpolate(value, [min, max], [min, max], 'clamp'), []); + const clamp = useWorkletCallback((value: number, [min, max]) => interpolate(value, [min, max], [min, max], 'clamp'), []); /** * Returns current image size taking into account scale and rotation. @@ -177,7 +166,7 @@ function AvatarCropModal(props) { * @param {Number} newY */ const updateImageOffset = useWorkletCallback( - (offsetX, offsetY) => { + (offsetX: number, offsetY: number) => { const {height, width} = getDisplayedImageSize(); const maxOffsetX = (width - imageContainerSize) / 2; const maxOffsetY = (height - imageContainerSize) / 2; @@ -194,7 +183,7 @@ function AvatarCropModal(props) { * @param {Number} containerSize * @returns {Number} */ - const newScaleValue = useWorkletCallback((newSliderValue, containerSize) => { + const newScaleValue = useWorkletCallback((newSliderValue: number, containerSize: number) => { const {MAX_SCALE, MIN_SCALE} = CONST.AVATAR_CROP_MODAL; return (newSliderValue / containerSize) * (MAX_SCALE - MIN_SCALE) + MIN_SCALE; }); @@ -323,14 +312,14 @@ function AvatarCropModal(props) { // Svg images are converted to a png blob to preserve transparency, so we need to update the // image name and type accordingly. - const isSvg = props.imageType.includes('image/svg'); - const imageName = isSvg ? 'fileName.png' : props.imageName; - const imageType = isSvg ? 'image/png' : props.imageType; + const isSvg = imageType.includes('image/svg'); + const imgName = isSvg ? 'fileName.png' : imageName; + const imgType = isSvg ? 'image/png' : imageType; - cropOrRotateImage(props.imageUri, [{rotate: rotation.value % 360}, {crop}], {compress: 1, name: imageName, type: imageType}) + cropOrRotateImage(imageUri, [{rotate: rotation.value % 360}, {crop}], {compress: 1, name: imgName, type: imgType}) .then((newImage) => { - props.onClose(); - props.onSave(newImage); + onClose(); + onSave(newImage); }) .catch(() => { isLoading.value = false; @@ -340,7 +329,7 @@ function AvatarCropModal(props) { /** * @param {Number} locationX */ - const sliderOnPress = (locationX) => { + const sliderOnPress = (locationX: number) => { // We are using the worklet directive here and running on the UI thread to ensure the Reanimated // shared values are updated synchronously, as they update asynchronously on the JS thread. @@ -361,7 +350,7 @@ function AvatarCropModal(props) { return ( - {props.isSmallScreenWidth && } + {isSmallScreenWidth && } - {props.translate('avatarCropModal.description')} + {translate('avatarCropModal.description')} runOnUI(sliderOnPress)(e.nativeEvent.locationX)} + accessible={false} accessibilityLabel="slider" role={CONST.ROLE.SLIDER} > @@ -422,7 +412,7 @@ function AvatarCropModal(props) { /> @@ -444,7 +434,7 @@ function AvatarCropModal(props) { style={[styles.m5]} onPress={cropAndSaveImage} pressOnEnter - text={props.translate('common.save')} + text={translate('common.save')} /> @@ -452,6 +442,5 @@ function AvatarCropModal(props) { } AvatarCropModal.displayName = 'AvatarCropModal'; -AvatarCropModal.propTypes = propTypes; -AvatarCropModal.defaultProps = defaultProps; -export default compose(withWindowDimensions, withLocalize)(AvatarCropModal); + +export default AvatarCropModal; diff --git a/src/components/AvatarCropModal/ImageCropView.js b/src/components/AvatarCropModal/ImageCropView.tsx similarity index 68% rename from src/components/AvatarCropModal/ImageCropView.js rename to src/components/AvatarCropModal/ImageCropView.tsx index 92cbe3a4da04..0f17fb6ba5b3 100644 --- a/src/components/AvatarCropModal/ImageCropView.js +++ b/src/components/AvatarCropModal/ImageCropView.tsx @@ -1,7 +1,7 @@ -import PropTypes from 'prop-types'; import React from 'react'; import {View} from 'react-native'; import {PanGestureHandler} from 'react-native-gesture-handler'; +import type {GestureEvent, PanGestureHandlerEventPayload} from 'react-native-gesture-handler'; import Animated, {interpolate, useAnimatedStyle} from 'react-native-reanimated'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; @@ -9,52 +9,58 @@ import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import ControlSelection from '@libs/ControlSelection'; -import gestureHandlerPropTypes from './gestureHandlerPropTypes'; +import {SelectionElement} from '@libs/ControlSelection/types'; +import type IconAsset from '@src/types/utils/IconAsset'; -const propTypes = { +type ImageCropViewProps = { /** Link to image for cropping */ - imageUri: PropTypes.string, + imageUri: string; /** Size of the image container that will be rendered */ - containerSize: PropTypes.number, + containerSize: number; /** The height of the selected image */ - originalImageHeight: PropTypes.shape({value: PropTypes.number}).isRequired, + originalImageHeight: { + value: number; + }; /** The width of the selected image */ - originalImageWidth: PropTypes.shape({value: PropTypes.number}).isRequired, + originalImageWidth: { + value: number; + }; /** The rotation value of the selected image */ - rotation: PropTypes.shape({value: PropTypes.number}).isRequired, + rotation: { + value: number; + }; /** The relative image shift along X-axis */ - translateX: PropTypes.shape({value: PropTypes.number}).isRequired, + translateX: { + value: number; + }; /** The relative image shift along Y-axis */ - translateY: PropTypes.shape({value: PropTypes.number}).isRequired, + translateY: { + value: number; + }; /** The scale factor of the image */ - scale: PropTypes.shape({value: PropTypes.number}).isRequired, + scale: { + value: number; + }; /** React-native-reanimated lib handler which executes when the user is panning image */ - panGestureEventHandler: gestureHandlerPropTypes, + panGestureEventHandler: (event: GestureEvent) => void; /** Image crop vector mask */ - maskImage: PropTypes.func, + maskImage?: IconAsset; }; -const defaultProps = { - imageUri: '', - containerSize: 0, - panGestureEventHandler: () => {}, - maskImage: Expensicons.ImageCropCircleMask, -}; - -function ImageCropView(props) { +function ImageCropView({imageUri = '', containerSize = 0, panGestureEventHandler = () => {}, maskImage = Expensicons.ImageCropCircleMask, ...props}: ImageCropViewProps) { const theme = useTheme(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); - const containerStyle = StyleUtils.getWidthAndHeightStyle(props.containerSize, props.containerSize); + const containerStyle = StyleUtils.getWidthAndHeightStyle(containerSize, containerSize); const originalImageHeight = props.originalImageHeight; const originalImageWidth = props.originalImageWidth; @@ -77,22 +83,24 @@ function ImageCropView(props) { // We're preventing text selection with ControlSelection.blockElement to prevent safari // default behaviour of cursor - I-beam cursor on drag. See https://github.com/Expensify/App/issues/13688 return ( - + { + ControlSelection.blockElement(el as SelectionElement); + }} style={[containerStyle, styles.imageCropContainer]} > @@ -101,8 +109,6 @@ function ImageCropView(props) { } ImageCropView.displayName = 'ImageCropView'; -ImageCropView.propTypes = propTypes; -ImageCropView.defaultProps = defaultProps; // React.memo is needed here to prevent styles recompilation // which sometimes may cause glitches during rerender of the modal diff --git a/src/components/AvatarCropModal/Slider.js b/src/components/AvatarCropModal/Slider.tsx similarity index 69% rename from src/components/AvatarCropModal/Slider.js rename to src/components/AvatarCropModal/Slider.tsx index ba2e1471ce9e..841b8d5bd473 100644 --- a/src/components/AvatarCropModal/Slider.js +++ b/src/components/AvatarCropModal/Slider.tsx @@ -1,34 +1,29 @@ -import PropTypes from 'prop-types'; -import React, {useState} from 'react'; +import React, {useRef, useState} from 'react'; import {View} from 'react-native'; import {PanGestureHandler} from 'react-native-gesture-handler'; +import type {GestureEvent, PanGestureHandlerEventPayload} from 'react-native-gesture-handler'; import Animated, {useAnimatedStyle} from 'react-native-reanimated'; import Tooltip from '@components/Tooltip'; -import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; +import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import ControlSelection from '@libs/ControlSelection'; -import gestureHandlerPropTypes from './gestureHandlerPropTypes'; +import {SelectionElement} from '@libs/ControlSelection/types'; -const propTypes = { +type SliderProps = { /** React-native-reanimated lib handler which executes when the user is panning slider */ - onGesture: gestureHandlerPropTypes, + onGesture: (event: GestureEvent) => void; /** X position of the slider knob */ - sliderValue: PropTypes.shape({value: PropTypes.number}), - - ...withLocalizePropTypes, -}; - -const defaultProps = { - onGesture: () => {}, - sliderValue: {}, + sliderValue: { + value: number; + }; }; // This component can't be written using class since reanimated API uses hooks. -function Slider(props) { +function Slider({onGesture = () => {}, sliderValue = {value: 0}}: SliderProps) { const styles = useThemeStyles(); - const sliderValue = props.sliderValue; const [tooltipIsVisible, setTooltipIsVisible] = useState(true); + const {translate} = useLocalize(); // A reanimated memoized style, which tracks // a translateX shared value and updates the slider position. @@ -40,18 +35,20 @@ function Slider(props) { // default behaviour of cursor - I-beam cursor on drag. See https://github.com/Expensify/App/issues/13688 return ( { + ControlSelection.blockElement(el as SelectionElement); + }} style={styles.sliderBar} > setTooltipIsVisible(false)} onEnded={() => setTooltipIsVisible(true)} - onGestureEvent={props.onGesture} + onGestureEvent={onGesture} > {tooltipIsVisible && ( @@ -64,6 +61,5 @@ function Slider(props) { } Slider.displayName = 'Slider'; -Slider.propTypes = propTypes; -Slider.defaultProps = defaultProps; -export default withLocalize(Slider); + +export default Slider; diff --git a/src/components/AvatarCropModal/gestureHandlerPropTypes.js b/src/components/AvatarCropModal/gestureHandlerPropTypes.js deleted file mode 100644 index c473a162ba7e..000000000000 --- a/src/components/AvatarCropModal/gestureHandlerPropTypes.js +++ /dev/null @@ -1,21 +0,0 @@ -import PropTypes from 'prop-types'; - -export default PropTypes.oneOfType([ - // Executes once a gesture is triggered - PropTypes.func, - PropTypes.shape({ - current: PropTypes.shape({ - // Array of event names that will be handled by animation handler - eventNames: PropTypes.arrayOf(PropTypes.string), - - // Array of registered event handlers ids - registrations: PropTypes.arrayOf(PropTypes.number), - - // React tag of the node we want to manage - viewTag: PropTypes.number, - - // Executes once a gesture is triggered - worklet: PropTypes.func, - }), - }), -]); diff --git a/src/components/Button/index.tsx b/src/components/Button/index.tsx index fb72f0cc845f..b422f512ca47 100644 --- a/src/components/Button/index.tsx +++ b/src/components/Button/index.tsx @@ -20,7 +20,7 @@ import validateSubmitShortcut from './validateSubmitShortcut'; type ButtonWithText = { /** The text for the button label */ - text: string; + text?: string; /** Boolean whether to display the right icon */ shouldShowRightIcon?: boolean; diff --git a/src/components/Pressable/GenericPressable/types.ts b/src/components/Pressable/GenericPressable/types.ts index dc04b6fcf329..2dd2e17e0454 100644 --- a/src/components/Pressable/GenericPressable/types.ts +++ b/src/components/Pressable/GenericPressable/types.ts @@ -40,7 +40,7 @@ type PressableProps = RNPressableProps & /** * onPress callback */ - onPress: (event?: GestureResponderEvent | KeyboardEvent) => void | Promise; + onPress?: (event?: GestureResponderEvent | KeyboardEvent) => void | Promise; /** * Specifies keyboard shortcut to trigger onPressHandler diff --git a/src/components/Pressable/PressableWithDelayToggle.tsx b/src/components/Pressable/PressableWithDelayToggle.tsx index ab1fa95efeb5..86f6c9d8aff8 100644 --- a/src/components/Pressable/PressableWithDelayToggle.tsx +++ b/src/components/Pressable/PressableWithDelayToggle.tsx @@ -78,7 +78,7 @@ function PressableWithDelayToggle( return; } temporarilyDisableInteractions(); - onPress(); + onPress?.(); }; // Due to limitations in RN regarding the vertical text alignment of non-Text elements, diff --git a/src/components/Pressable/PressableWithoutFocus.tsx b/src/components/Pressable/PressableWithoutFocus.tsx index f887b0ea9b7d..240ef4a9873a 100644 --- a/src/components/Pressable/PressableWithoutFocus.tsx +++ b/src/components/Pressable/PressableWithoutFocus.tsx @@ -15,7 +15,7 @@ function PressableWithoutFocus({children, onPress, onLongPress, ...rest}: Pressa const pressAndBlur = () => { ref?.current?.blur(); - onPress(); + onPress?.(); }; return ( diff --git a/src/libs/ControlSelection/index.native.ts b/src/libs/ControlSelection/index.native.ts index b45af6da6441..2bccea946cda 100644 --- a/src/libs/ControlSelection/index.native.ts +++ b/src/libs/ControlSelection/index.native.ts @@ -1,4 +1,4 @@ -import type ControlSelectionModule from './types'; +import type {ControlSelectionModule} from './types'; function block() {} function unblock() {} diff --git a/src/libs/ControlSelection/index.ts b/src/libs/ControlSelection/index.ts index ab11e66bc369..89f1e62aa6f6 100644 --- a/src/libs/ControlSelection/index.ts +++ b/src/libs/ControlSelection/index.ts @@ -1,5 +1,4 @@ -import type CustomRefObject from '@src/types/utils/CustomRefObject'; -import type ControlSelectionModule from './types'; +import type {ControlSelectionModule, SelectionElement} from './types'; /** * Block selection on the whole app @@ -20,25 +19,25 @@ function unblock() { /** * Block selection on particular element */ -function blockElement(ref?: CustomRefObject | null) { - if (!ref) { +function blockElement(element?: SelectionElement | null) { + if (!element) { return; } // eslint-disable-next-line no-param-reassign - ref.onselectstart = () => false; + element.onselectstart = () => false; } /** * Unblock selection on particular element */ -function unblockElement(ref?: CustomRefObject | null) { - if (!ref) { +function unblockElement(element?: SelectionElement | null) { + if (!element) { return; } // eslint-disable-next-line no-param-reassign - ref.onselectstart = () => true; + element.onselectstart = () => true; } const ControlSelection: ControlSelectionModule = { diff --git a/src/libs/ControlSelection/types.ts b/src/libs/ControlSelection/types.ts index fc0b488577ec..8433a366ed91 100644 --- a/src/libs/ControlSelection/types.ts +++ b/src/libs/ControlSelection/types.ts @@ -1,10 +1,10 @@ -import type CustomRefObject from '@src/types/utils/CustomRefObject'; +type SelectionElement = T & {onselectstart: () => boolean}; type ControlSelectionModule = { block: () => void; unblock: () => void; - blockElement: (ref?: CustomRefObject | null) => void; - unblockElement: (ref?: CustomRefObject | null) => void; + blockElement: (element?: SelectionElement | null) => void; + unblockElement: (element?: SelectionElement | null) => void; }; -export default ControlSelectionModule; +export type {ControlSelectionModule, SelectionElement}; diff --git a/src/types/utils/CustomRefObject.ts b/src/types/utils/CustomRefObject.ts deleted file mode 100644 index 13bb0f27a42e..000000000000 --- a/src/types/utils/CustomRefObject.ts +++ /dev/null @@ -1,5 +0,0 @@ -import type {RefObject} from 'react'; - -type CustomRefObject = RefObject & {onselectstart: () => boolean}; - -export default CustomRefObject; From 32d5cc9e822bd78606c1372b126183baedecf5c6 Mon Sep 17 00:00:00 2001 From: Kamil Owczarz Date: Wed, 10 Jan 2024 15:34:32 +0100 Subject: [PATCH 141/391] Modify Onyx typings --- src/components/Form/FormProvider.tsx | 19 +++++++++---------- src/components/Form/FormWrapper.tsx | 5 +++-- src/types/onyx/Form.ts | 2 ++ 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/src/components/Form/FormProvider.tsx b/src/components/Form/FormProvider.tsx index 6581cef8ac95..7177fb88a7db 100644 --- a/src/components/Form/FormProvider.tsx +++ b/src/components/Form/FormProvider.tsx @@ -36,14 +36,12 @@ function getInitialValueByType(valueType?: ValueType): InitialDefaultValue { } } -type GenericFormValues = Form & Record; - type FormProviderOnyxProps = { /** Contains the form state that must be accessed outside the component */ - formState: OnyxEntry; + formState: OnyxEntry; /** Contains draft values for each input in the form */ - draftValues: OnyxEntry; + draftValues: OnyxEntry; /** Information about the network */ network: OnyxEntry; @@ -86,7 +84,7 @@ function FormProvider( ) { const inputRefs = useRef({} as InputRefs); const touchedInputs = useRef>({}); - const [inputValues, setInputValues] = useState(() => ({...draftValues})); + const [inputValues, setInputValues] = useState(() => ({...draftValues})); const [errors, setErrors] = useState({}); const hasServerError = useMemo(() => !!formState && !isEmptyObject(formState?.errors), [formState]); @@ -185,7 +183,7 @@ function FormProvider( }, [enabledWhenOffline, formState?.isLoading, inputValues, network?.isOffline, onSubmit, onValidate]); const resetForm = useCallback( - (optionalValue: GenericFormValues) => { + (optionalValue: Form) => { Object.keys(inputValues).forEach((inputID) => { setInputValues((prevState) => { const copyPrevState = {...prevState}; @@ -348,12 +346,13 @@ export default withOnyx({ network: { key: ONYXKEYS.NETWORK, }, + // withOnyx typings are not able to handle such generic cases like this one, since it's a generic component we had to cast the keys to any formState: { - // @ts-expect-error TODO: fix this - key: ({formID}) => formID, + // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-explicit-any + key: ({formID}) => formID as any, }, draftValues: { - // @ts-expect-error TODO: fix this - key: (props) => `${props.formID}Draft` as const, + // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-explicit-any + key: (props) => `${props.formID}Draft` as any, }, })(forwardRef(FormProvider)) as (props: Omit, keyof FormProviderOnyxProps>) => ReactNode; diff --git a/src/components/Form/FormWrapper.tsx b/src/components/Form/FormWrapper.tsx index f1d32486de5e..151600c9c12a 100644 --- a/src/components/Form/FormWrapper.tsx +++ b/src/components/Form/FormWrapper.tsx @@ -168,7 +168,8 @@ FormWrapper.displayName = 'FormWrapper'; export default withOnyx({ formState: { - // FIX: Fabio plz help 😂 - key: (props) => props.formID as typeof ONYXKEYS.FORMS.EDIT_TASK_FORM, + // withOnyx typings are not able to handle such generic cases like this one, since it's a generic component we had to cast the keys to any + // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-explicit-any + key: (props) => props.formID as any, }, })(FormWrapper); diff --git a/src/types/onyx/Form.ts b/src/types/onyx/Form.ts index 666898450a93..a6d276e50b9f 100644 --- a/src/types/onyx/Form.ts +++ b/src/types/onyx/Form.ts @@ -1,6 +1,8 @@ import type * as OnyxCommon from './OnyxCommon'; type Form = { + [key: string]: unknown; + /** Controls the loading state of the form */ isLoading?: boolean; From 83c42ab3a9d8ad20df68d90a8816b3bb9a5bda25 Mon Sep 17 00:00:00 2001 From: someone-here Date: Wed, 10 Jan 2024 20:25:29 +0530 Subject: [PATCH 142/391] Fix lint --- .../AvatarCropModal/AvatarCropModal.tsx | 38 ++++++++++++++----- .../AvatarCropModal/ImageCropView.tsx | 2 +- src/components/AvatarCropModal/Slider.tsx | 4 +- 3 files changed, 31 insertions(+), 13 deletions(-) diff --git a/src/components/AvatarCropModal/AvatarCropModal.tsx b/src/components/AvatarCropModal/AvatarCropModal.tsx index 2a9174c6d7a3..3f4b7d38451e 100644 --- a/src/components/AvatarCropModal/AvatarCropModal.tsx +++ b/src/components/AvatarCropModal/AvatarCropModal.tsx @@ -1,5 +1,6 @@ import React, {useCallback, useEffect, useState} from 'react'; -import {ActivityIndicator, Image, LayoutChangeEvent, View} from 'react-native'; +import {ActivityIndicator, Image, View} from 'react-native'; +import type {LayoutChangeEvent} from 'react-native'; import {GestureHandlerRootView} from 'react-native-gesture-handler'; import {interpolate, runOnUI, useAnimatedGestureHandler, useSharedValue, useWorkletCallback} from 'react-native-reanimated'; import Button from '@components/Button'; @@ -17,11 +18,10 @@ import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; -import compose from '@libs/compose'; import cropOrRotateImage from '@libs/cropOrRotateImage'; -import {CustomRNImageManipulatorResult} from '@libs/cropOrRotateImage/types'; +import type {CustomRNImageManipulatorResult} from '@libs/cropOrRotateImage/types'; import CONST from '@src/CONST'; -import IconAsset from '@src/types/utils/IconAsset'; +import type IconAsset from '@src/types/utils/IconAsset'; import ImageCropView from './ImageCropView'; import Slider from './Slider'; @@ -48,6 +48,12 @@ type AvatarCropModalProps = { maskImage?: IconAsset; }; +type PanHandlerContextType = { + translateX: number; + translateY: number; + translateSliderX: number; +}; + // This component can't be written using class since reanimated API uses hooks. function AvatarCropModal({imageUri = '', imageName = '', imageType = '', onClose = () => {}, onSave = () => {}, ...props}: AvatarCropModalProps) { const theme = useTheme(); @@ -194,7 +200,7 @@ function AvatarCropModal({imageUri = '', imageName = '', imageType = '', onClose */ const panGestureEventHandler = useAnimatedGestureHandler( { - onStart: (_, context) => { + onStart: (a, context: PanHandlerContextType) => { // we have to assign translate values to a context // since that is required for proper work of turbo modules. // eslint-disable-next-line no-param-reassign @@ -242,7 +248,7 @@ function AvatarCropModal({imageUri = '', imageName = '', imageType = '', onClose */ const panSliderGestureEventHandler = useAnimatedGestureHandler( { - onStart: (_, context) => { + onStart: (a, context: PanHandlerContextType) => { // we have to assign this value to a context // since that is required for proper work of turbo modules. // eslint-disable-next-line no-param-reassign @@ -324,11 +330,23 @@ function AvatarCropModal({imageUri = '', imageName = '', imageType = '', onClose .catch(() => { isLoading.value = false; }); - }, [originalImageHeight.value, originalImageWidth.value, scale.value, translateX.value, imageContainerSize, translateY.value, props, rotation.value, isLoading]); + }, [ + imageName, + imageUri, + imageType, + onSave, + onClose, + originalImageHeight.value, + originalImageWidth.value, + scale.value, + translateX.value, + imageContainerSize, + translateY.value, + props, + rotation.value, + isLoading, + ]); - /** - * @param {Number} locationX - */ const sliderOnPress = (locationX: number) => { // We are using the worklet directive here and running on the UI thread to ensure the Reanimated // shared values are updated synchronously, as they update asynchronously on the JS thread. diff --git a/src/components/AvatarCropModal/ImageCropView.tsx b/src/components/AvatarCropModal/ImageCropView.tsx index 0f17fb6ba5b3..e76ffc2d1f10 100644 --- a/src/components/AvatarCropModal/ImageCropView.tsx +++ b/src/components/AvatarCropModal/ImageCropView.tsx @@ -9,7 +9,7 @@ import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import ControlSelection from '@libs/ControlSelection'; -import {SelectionElement} from '@libs/ControlSelection/types'; +import type {SelectionElement} from '@libs/ControlSelection/types'; import type IconAsset from '@src/types/utils/IconAsset'; type ImageCropViewProps = { diff --git a/src/components/AvatarCropModal/Slider.tsx b/src/components/AvatarCropModal/Slider.tsx index 841b8d5bd473..26e35517f48e 100644 --- a/src/components/AvatarCropModal/Slider.tsx +++ b/src/components/AvatarCropModal/Slider.tsx @@ -1,4 +1,4 @@ -import React, {useRef, useState} from 'react'; +import React, {useState} from 'react'; import {View} from 'react-native'; import {PanGestureHandler} from 'react-native-gesture-handler'; import type {GestureEvent, PanGestureHandlerEventPayload} from 'react-native-gesture-handler'; @@ -7,7 +7,7 @@ import Tooltip from '@components/Tooltip'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import ControlSelection from '@libs/ControlSelection'; -import {SelectionElement} from '@libs/ControlSelection/types'; +import type {SelectionElement} from '@libs/ControlSelection/types'; type SliderProps = { /** React-native-reanimated lib handler which executes when the user is panning slider */ From 3c222b345b5dba0a05c17e75aa91f9510761e087 Mon Sep 17 00:00:00 2001 From: someone-here Date: Wed, 10 Jan 2024 20:34:21 +0530 Subject: [PATCH 143/391] Fix lint --- src/components/AvatarCropModal/AvatarCropModal.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/AvatarCropModal/AvatarCropModal.tsx b/src/components/AvatarCropModal/AvatarCropModal.tsx index 3f4b7d38451e..7c9313a6d394 100644 --- a/src/components/AvatarCropModal/AvatarCropModal.tsx +++ b/src/components/AvatarCropModal/AvatarCropModal.tsx @@ -342,7 +342,6 @@ function AvatarCropModal({imageUri = '', imageName = '', imageType = '', onClose translateX.value, imageContainerSize, translateY.value, - props, rotation.value, isLoading, ]); From 029493228a4b120d40646c922f2cbf61008bcff2 Mon Sep 17 00:00:00 2001 From: Kamil Owczarz Date: Wed, 10 Jan 2024 16:05:16 +0100 Subject: [PATCH 144/391] Code review changes --- src/components/Form/FormProvider.tsx | 4 ++-- src/components/Form/FormWrapper.tsx | 5 +---- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/components/Form/FormProvider.tsx b/src/components/Form/FormProvider.tsx index 7177fb88a7db..26e045c6a0b9 100644 --- a/src/components/Form/FormProvider.tsx +++ b/src/components/Form/FormProvider.tsx @@ -82,7 +82,7 @@ function FormProvider( }: FormProviderProps, forwardedRef: ForwardedRef, ) { - const inputRefs = useRef({} as InputRefs); + const inputRefs = useRef({}); const touchedInputs = useRef>({}); const [inputValues, setInputValues] = useState(() => ({...draftValues})); const [errors, setErrors] = useState({}); @@ -90,7 +90,7 @@ function FormProvider( const onValidate = useCallback( (values: OnyxFormValuesFields, shouldClearServerError = true) => { - const trimmedStringValues = ValidationUtils.prepareValues(values) as OnyxFormValuesFields; + const trimmedStringValues = ValidationUtils.prepareValues(values); if (shouldClearServerError) { FormActions.setErrors(formID, null); diff --git a/src/components/Form/FormWrapper.tsx b/src/components/Form/FormWrapper.tsx index 151600c9c12a..a513b8fa0845 100644 --- a/src/components/Form/FormWrapper.tsx +++ b/src/components/Form/FormWrapper.tsx @@ -11,7 +11,6 @@ import type {SafeAreaChildrenProps} from '@components/SafeAreaConsumer/types'; import ScrollViewWithContext from '@components/ScrollViewWithContext'; import useThemeStyles from '@hooks/useThemeStyles'; import * as ErrorUtils from '@libs/ErrorUtils'; -import type ONYXKEYS from '@src/ONYXKEYS'; import type {Form} from '@src/types/onyx'; import type {Errors} from '@src/types/onyx/OnyxCommon'; import type ChildrenProps from '@src/types/utils/ChildrenProps'; @@ -71,7 +70,6 @@ function FormWrapper({ buttonText={submitButtonText} isAlertVisible={!isEmptyObject(errors) || !!errorMessage || !isEmptyObject(formState?.errorFields)} isLoading={!!formState?.isLoading} - // eslint-disable-next-line no-extra-boolean-cast message={isEmptyObject(formState?.errorFields) ? errorMessage : undefined} onSubmit={onSubmit} footerContent={footerContent} @@ -95,8 +93,7 @@ function FormWrapper({ if (formContentRef.current) { // We measure relative to the content root, not the scroll view, as that gives // consistent results across mobile and web - // eslint-disable-next-line @typescript-eslint/naming-convention - focusInput?.measureLayout?.(formContentRef.current, (_x: number, y: number) => + focusInput?.measureLayout?.(formContentRef.current, (X: number, y: number) => formRef.current?.scrollTo({ y: y - 10, animated: false, From 81d347a375477be76512490e1f4eeec61205006f Mon Sep 17 00:00:00 2001 From: Kamil Owczarz Date: Wed, 10 Jan 2024 16:53:26 +0100 Subject: [PATCH 145/391] Code review changes --- src/ONYXKEYS.ts | 9 +++++---- src/libs/ValidationUtils.ts | 11 +++++------ src/types/onyx/Form.ts | 8 +++++++- src/types/onyx/ReimbursementAccount.ts | 5 ++++- src/types/onyx/ReimbursementAccountDraft.ts | 6 +++++- src/types/onyx/index.ts | 7 ++++++- 6 files changed, 32 insertions(+), 14 deletions(-) diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 6f55e771de6a..13de58a2c21c 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -2,6 +2,7 @@ import type {OnyxEntry} from 'react-native-onyx/lib/types'; import type {ValueOf} from 'type-fest'; import type CONST from './CONST'; import type * as OnyxTypes from './types/onyx'; +import {ReimbursementAccountForm, ReimbursementAccountFormDraft} from './types/onyx'; import type DeepValueOf from './types/utils/DeepValueOf'; /** @@ -408,8 +409,8 @@ type OnyxValues = { [ONYXKEYS.CARD_LIST]: Record; [ONYXKEYS.WALLET_STATEMENT]: OnyxTypes.WalletStatement; [ONYXKEYS.PERSONAL_BANK_ACCOUNT]: OnyxTypes.PersonalBankAccount; - [ONYXKEYS.REIMBURSEMENT_ACCOUNT]: OnyxTypes.ReimbursementAccount & OnyxTypes.Form; - [ONYXKEYS.REIMBURSEMENT_ACCOUNT_DRAFT]: OnyxTypes.ReimbursementAccountDraft & OnyxTypes.Form; + [ONYXKEYS.REIMBURSEMENT_ACCOUNT]: OnyxTypes.ReimbursementAccountForm; + [ONYXKEYS.REIMBURSEMENT_ACCOUNT_DRAFT]: OnyxTypes.ReimbursementAccountFormDraft; [ONYXKEYS.PREFERRED_EMOJI_SKIN_TONE]: string | number; [ONYXKEYS.FREQUENTLY_USED_EMOJIS]: OnyxTypes.FrequentlyUsedEmoji[]; [ONYXKEYS.REIMBURSEMENT_ACCOUNT_WORKSPACE_ID]: string; @@ -475,8 +476,8 @@ type OnyxValues = { [ONYXKEYS.FORMS.CLOSE_ACCOUNT_FORM_DRAFT]: OnyxTypes.Form; [ONYXKEYS.FORMS.PROFILE_SETTINGS_FORM]: OnyxTypes.Form; [ONYXKEYS.FORMS.PROFILE_SETTINGS_FORM_DRAFT]: OnyxTypes.Form; - [ONYXKEYS.FORMS.DISPLAY_NAME_FORM]: OnyxTypes.Form & {firstName: string; lastName: string}; - [ONYXKEYS.FORMS.DISPLAY_NAME_FORM_DRAFT]: OnyxTypes.Form & {firstName: string; lastName: string}; + [ONYXKEYS.FORMS.DISPLAY_NAME_FORM]: OnyxTypes.DisplayNameForm; + [ONYXKEYS.FORMS.DISPLAY_NAME_FORM_DRAFT]: OnyxTypes.DisplayNameForm; [ONYXKEYS.FORMS.ROOM_NAME_FORM]: OnyxTypes.Form; [ONYXKEYS.FORMS.ROOM_NAME_FORM_DRAFT]: OnyxTypes.Form; [ONYXKEYS.FORMS.WELCOME_MESSAGE_FORM]: OnyxTypes.Form; diff --git a/src/libs/ValidationUtils.ts b/src/libs/ValidationUtils.ts index 9bbdf20a9003..7eff51c354df 100644 --- a/src/libs/ValidationUtils.ts +++ b/src/libs/ValidationUtils.ts @@ -392,17 +392,16 @@ function isValidAccountRoute(accountID: number): boolean { return CONST.REGEX.NUMBER.test(String(accountID)) && accountID > 0; } +type DateTimeValidationErrorKeys = { + dateValidationErrorKey: string; + timeValidationErrorKey: string; +}; /** * Validates that the date and time are at least one minute in the future. * data - A date and time string in 'YYYY-MM-DD HH:mm:ss.sssZ' format * returns an object containing the error messages for the date and time */ -const validateDateTimeIsAtLeastOneMinuteInFuture = ( - data: string, -): { - dateValidationErrorKey: string; - timeValidationErrorKey: string; -} => { +const validateDateTimeIsAtLeastOneMinuteInFuture = (data: string): DateTimeValidationErrorKeys => { if (!data) { return { dateValidationErrorKey: '', diff --git a/src/types/onyx/Form.ts b/src/types/onyx/Form.ts index a6d276e50b9f..9306ab5736fc 100644 --- a/src/types/onyx/Form.ts +++ b/src/types/onyx/Form.ts @@ -1,3 +1,4 @@ +import type * as OnyxTypes from './index'; import type * as OnyxCommon from './OnyxCommon'; type Form = { @@ -23,6 +24,11 @@ type DateOfBirthForm = Form & { dob?: string; }; +type DisplayNameForm = OnyxTypes.Form & { + firstName: string; + lastName: string; +}; + export default Form; -export type {AddDebitCardForm, DateOfBirthForm}; +export type {AddDebitCardForm, DateOfBirthForm, DisplayNameForm}; diff --git a/src/types/onyx/ReimbursementAccount.ts b/src/types/onyx/ReimbursementAccount.ts index c0ade25e4d79..4779b790eac0 100644 --- a/src/types/onyx/ReimbursementAccount.ts +++ b/src/types/onyx/ReimbursementAccount.ts @@ -1,5 +1,6 @@ import type {ValueOf} from 'type-fest'; import type CONST from '@src/CONST'; +import type * as OnyxTypes from './index'; import type * as OnyxCommon from './OnyxCommon'; type BankAccountStep = ValueOf; @@ -48,5 +49,7 @@ type ReimbursementAccount = { pendingAction?: OnyxCommon.PendingAction; }; +type ReimbursementAccountForm = ReimbursementAccount & OnyxTypes.Form; + export default ReimbursementAccount; -export type {BankAccountStep, BankAccountSubStep}; +export type {BankAccountStep, BankAccountSubStep, ReimbursementAccountForm}; diff --git a/src/types/onyx/ReimbursementAccountDraft.ts b/src/types/onyx/ReimbursementAccountDraft.ts index cab1283943bc..5b3c604fdab6 100644 --- a/src/types/onyx/ReimbursementAccountDraft.ts +++ b/src/types/onyx/ReimbursementAccountDraft.ts @@ -1,3 +1,5 @@ +import type * as OnyxTypes from './index'; + type OnfidoData = Record; type BankAccountStepProps = { @@ -57,5 +59,7 @@ type ReimbursementAccountProps = { type ReimbursementAccountDraft = BankAccountStepProps & CompanyStepProps & RequestorStepProps & ACHContractStepProps & ReimbursementAccountProps; +type ReimbursementAccountFormDraft = ReimbursementAccountDraft & OnyxTypes.Form; + export default ReimbursementAccountDraft; -export type {ACHContractStepProps, RequestorStepProps, OnfidoData, BankAccountStepProps, CompanyStepProps, ReimbursementAccountProps}; +export type {ACHContractStepProps, RequestorStepProps, OnfidoData, BankAccountStepProps, CompanyStepProps, ReimbursementAccountProps, ReimbursementAccountFormDraft}; diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts index 7bd9c321be5e..efb578a03295 100644 --- a/src/types/onyx/index.ts +++ b/src/types/onyx/index.ts @@ -9,7 +9,7 @@ import type Credentials from './Credentials'; import type Currency from './Currency'; import type CustomStatusDraft from './CustomStatusDraft'; import type Download from './Download'; -import type {AddDebitCardForm, DateOfBirthForm} from './Form'; +import type {AddDebitCardForm, DateOfBirthForm, DisplayNameForm} from './Form'; import type Form from './Form'; import type FrequentlyUsedEmoji from './FrequentlyUsedEmoji'; import type {FundList} from './Fund'; @@ -38,7 +38,9 @@ import type RecentlyUsedReportFields from './RecentlyUsedReportFields'; import type RecentlyUsedTags from './RecentlyUsedTags'; import type RecentWaypoint from './RecentWaypoint'; import type ReimbursementAccount from './ReimbursementAccount'; +import type {ReimbursementAccountForm} from './ReimbursementAccount'; import type ReimbursementAccountDraft from './ReimbursementAccountDraft'; +import type {ReimbursementAccountFormDraft} from './ReimbursementAccountDraft'; import type Report from './Report'; import type {ReportActions} from './ReportAction'; import type ReportAction from './ReportAction'; @@ -69,6 +71,7 @@ export type { Account, AccountData, AddDebitCardForm, + DisplayNameForm, BankAccount, BankAccountList, Beta, @@ -108,7 +111,9 @@ export type { RecentlyUsedCategories, RecentlyUsedTags, ReimbursementAccount, + ReimbursementAccountForm, ReimbursementAccountDraft, + ReimbursementAccountFormDraft, Report, ReportAction, ReportActionReactions, From 4edfd165b7c158a47f81a905dd19ca663ba2eefa Mon Sep 17 00:00:00 2001 From: Ishpaul Singh Date: Wed, 10 Jan 2024 23:41:56 +0530 Subject: [PATCH 146/391] prettier diffs --- src/pages/home/report/comment/TextCommentFragment.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/home/report/comment/TextCommentFragment.tsx b/src/pages/home/report/comment/TextCommentFragment.tsx index 8140c00f8c3f..7450dc14e6bf 100644 --- a/src/pages/home/report/comment/TextCommentFragment.tsx +++ b/src/pages/home/report/comment/TextCommentFragment.tsx @@ -37,7 +37,7 @@ type TextCommentFragmentProps = { iouMessage?: string; }; -function TextCommentFragment({fragment, styleAsDeleted, source, style, displayAsGroup, iouMessage=''}: TextCommentFragmentProps) { +function TextCommentFragment({fragment, styleAsDeleted, source, style, displayAsGroup, iouMessage = ''}: TextCommentFragmentProps) { const theme = useTheme(); const styles = useThemeStyles(); const {html = '', text} = fragment; From ed2ffb4a04097f4de857ffca6ffe9c4e863d2104 Mon Sep 17 00:00:00 2001 From: brunovjk Date: Wed, 10 Jan 2024 15:40:14 -0300 Subject: [PATCH 147/391] Move 'isOptionsDataReady' to an 'useState' --- .../MoneyTemporaryForRefactorRequestParticipantsSelector.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js index 250f68b2b504..025250a4b70a 100644 --- a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js +++ b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js @@ -96,6 +96,7 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({ didScreenTransitionEnd, }) { const styles = useThemeStyles(); + const [isOptionsDataReady, setIsOptionsDataReady] = useState(false); const [searchTerm, setSearchTerm] = useState(''); const [newChatOptions, setNewChatOptions] = useState({ recentReports: [], @@ -225,7 +226,6 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({ maxParticipantsReached, _.some(participants, (participant) => lodashGet(participant, 'searchText', '').toLowerCase().includes(searchTerm.trim().toLowerCase())), ); - const isOptionsDataReady = ReportUtils.isReportDataReady() && OptionsListUtils.isPersonalDetailsReady(personalDetails); useEffect(() => { if (!didScreenTransitionEnd) { @@ -262,6 +262,7 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({ personalDetails: chatOptions.personalDetails, userToInvite: chatOptions.userToInvite, }); + setIsOptionsDataReady(ReportUtils.isReportDataReady() && OptionsListUtils.isPersonalDetailsReady(personalDetails)) }, [betas, reports, participants, personalDetails, translate, searchTerm, setNewChatOptions, iouType, iouRequestType, didScreenTransitionEnd]); // When search term updates we will fetch any reports @@ -326,7 +327,7 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({ textInputLabel={translate('optionsSelector.nameEmailOrPhoneNumber')} textInputAlert={isOffline ? `${translate('common.youAppearToBeOffline')} ${translate('search.resultsAreLimited')}` : ''} safeAreaPaddingBottomStyle={safeAreaPaddingBottomStyle} - shouldShowOptions={didScreenTransitionEnd && isOptionsDataReady} + shouldShowOptions={isOptionsDataReady} shouldShowReferralCTA referralContentType={iouType === 'send' ? CONST.REFERRAL_PROGRAM.CONTENT_TYPES.SEND_MONEY : CONST.REFERRAL_PROGRAM.CONTENT_TYPES.MONEY_REQUEST} shouldPreventDefaultFocusOnSelectRow={!DeviceCapabilities.canUseTouchScreen()} From 6290efaa16f811f873c4e8fb1c04551e803b93e4 Mon Sep 17 00:00:00 2001 From: Krishna Gupta Date: Thu, 11 Jan 2024 02:01:48 +0530 Subject: [PATCH 148/391] fix: Workspace - User is scrolled down in Workspace invite page Signed-off-by: Krishna Gupta --- src/pages/RoomInvitePage.js | 113 ++++++++++-------- src/pages/workspace/WorkspaceInvitePage.js | 127 ++++++++++++--------- 2 files changed, 133 insertions(+), 107 deletions(-) diff --git a/src/pages/RoomInvitePage.js b/src/pages/RoomInvitePage.js index f290eff91669..1eb69f454ab4 100644 --- a/src/pages/RoomInvitePage.js +++ b/src/pages/RoomInvitePage.js @@ -1,3 +1,4 @@ +import {useNavigation} from '@react-navigation/native'; import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; import React, {useCallback, useEffect, useMemo, useState} from 'react'; @@ -68,6 +69,8 @@ function RoomInvitePage(props) { const [selectedOptions, setSelectedOptions] = useState([]); const [personalDetails, setPersonalDetails] = useState([]); const [userToInvite, setUserToInvite] = useState(null); + const [didScreenTransitionEnd, setDidScreenTransitionEnd] = useState(false); + const navigation = useNavigation(); // Any existing participants and Expensify emails should not be eligible for invitation const excludedUsers = useMemo( @@ -92,10 +95,26 @@ function RoomInvitePage(props) { // eslint-disable-next-line react-hooks/exhaustive-deps -- we don't want to recalculate when selectedOptions change }, [props.personalDetails, props.betas, searchTerm, excludedUsers]); - const getSections = () => { - const sections = []; + useEffect(() => { + const unsubscribeTransitionEnd = navigation.addListener('transitionEnd', () => { + setDidScreenTransitionEnd(true); + }); + + return () => { + unsubscribeTransitionEnd(); + }; + // Rule disabled because this effect is only for component did mount & will component unmount lifecycle event + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const sections = useMemo(() => { + const sectionsArr = []; let indexOffset = 0; + if (!didScreenTransitionEnd) { + return []; + } + // Filter all options that is a part of the search term or in the personal details let filterSelectedOptions = selectedOptions; if (searchTerm !== '') { @@ -108,7 +127,7 @@ function RoomInvitePage(props) { }); } - sections.push({ + sectionsArr.push({ title: undefined, data: filterSelectedOptions, shouldShow: true, @@ -122,7 +141,7 @@ function RoomInvitePage(props) { const personalDetailsFormatted = _.map(personalDetailsWithoutSelected, (personalDetail) => OptionsListUtils.formatMemberForList(personalDetail, false)); const hasUnselectedUserToInvite = userToInvite && !_.contains(selectedLogins, userToInvite.login); - sections.push({ + sectionsArr.push({ title: translate('common.contacts'), data: personalDetailsFormatted, shouldShow: !_.isEmpty(personalDetailsFormatted), @@ -131,7 +150,7 @@ function RoomInvitePage(props) { indexOffset += personalDetailsFormatted.length; if (hasUnselectedUserToInvite) { - sections.push({ + sectionsArr.push({ title: undefined, data: [OptionsListUtils.formatMemberForList(userToInvite, false)], shouldShow: true, @@ -139,8 +158,8 @@ function RoomInvitePage(props) { }); } - return sections; - }; + return sectionsArr; + }, [personalDetails, searchTerm, selectedOptions, translate, userToInvite, didScreenTransitionEnd]); const toggleOption = useCallback( (option) => { @@ -204,49 +223,43 @@ function RoomInvitePage(props) { shouldEnableMaxHeight testID={RoomInvitePage.displayName} > - {({didScreenTransitionEnd}) => { - const sections = didScreenTransitionEnd ? getSections() : []; - - return ( - Navigation.goBack(backRoute)} - > - { - Navigation.goBack(backRoute); - }} - /> - - - - - - ); - }} + Navigation.goBack(backRoute)} + > + { + Navigation.goBack(backRoute); + }} + /> + + + + + ); } diff --git a/src/pages/workspace/WorkspaceInvitePage.js b/src/pages/workspace/WorkspaceInvitePage.js index 6496fbecfc9f..6954cb030985 100644 --- a/src/pages/workspace/WorkspaceInvitePage.js +++ b/src/pages/workspace/WorkspaceInvitePage.js @@ -1,3 +1,4 @@ +import {useNavigation} from '@react-navigation/native'; import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; import React, {useEffect, useMemo, useState} from 'react'; @@ -71,6 +72,8 @@ function WorkspaceInvitePage(props) { const [selectedOptions, setSelectedOptions] = useState([]); const [personalDetails, setPersonalDetails] = useState([]); const [usersToInvite, setUsersToInvite] = useState([]); + const [didScreenTransitionEnd, setDidScreenTransitionEnd] = useState(false); + const navigation = useNavigation(); const openWorkspaceInvitePage = () => { const policyMemberEmailsToAccountIDs = PolicyUtils.getMemberAccountIDsForWorkspace(props.policyMembers, props.personalDetails); Policy.openWorkspaceInvitePage(props.route.params.policyID, _.keys(policyMemberEmailsToAccountIDs)); @@ -86,6 +89,18 @@ function WorkspaceInvitePage(props) { // eslint-disable-next-line react-hooks/exhaustive-deps -- policyID changes remount the component }, []); + useEffect(() => { + const unsubscribeTransitionEnd = navigation.addListener('transitionEnd', () => { + setDidScreenTransitionEnd(true); + }); + + return () => { + unsubscribeTransitionEnd(); + }; + // Rule disabled because this effect is only for component did mount & will component unmount lifecycle event + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + useNetwork({onReconnect: openWorkspaceInvitePage}); const excludedUsers = useMemo(() => PolicyUtils.getIneligibleInvitees(props.policyMembers, props.personalDetails), [props.policyMembers, props.personalDetails]); @@ -131,10 +146,14 @@ function WorkspaceInvitePage(props) { // eslint-disable-next-line react-hooks/exhaustive-deps -- we don't want to recalculate when selectedOptions change }, [props.personalDetails, props.policyMembers, props.betas, searchTerm, excludedUsers]); - const getSections = () => { - const sections = []; + const sections = useMemo(() => { + const sectionsArr = []; let indexOffset = 0; + if (!didScreenTransitionEnd) { + return []; + } + // Filter all options that is a part of the search term or in the personal details let filterSelectedOptions = selectedOptions; if (searchTerm !== '') { @@ -147,7 +166,7 @@ function WorkspaceInvitePage(props) { }); } - sections.push({ + sectionsArr.push({ title: undefined, data: filterSelectedOptions, shouldShow: true, @@ -160,7 +179,7 @@ function WorkspaceInvitePage(props) { const personalDetailsWithoutSelected = _.filter(personalDetails, ({login}) => !_.contains(selectedLogins, login)); const personalDetailsFormatted = _.map(personalDetailsWithoutSelected, OptionsListUtils.formatMemberForList); - sections.push({ + sectionsArr.push({ title: translate('common.contacts'), data: personalDetailsFormatted, shouldShow: !_.isEmpty(personalDetailsFormatted), @@ -172,7 +191,7 @@ function WorkspaceInvitePage(props) { const hasUnselectedUserToInvite = !_.contains(selectedLogins, userToInvite.login); if (hasUnselectedUserToInvite) { - sections.push({ + sectionsArr.push({ title: undefined, data: [OptionsListUtils.formatMemberForList(userToInvite)], shouldShow: true, @@ -181,8 +200,8 @@ function WorkspaceInvitePage(props) { } }); - return sections; - }; + return sectionsArr; + }, [personalDetails, searchTerm, selectedOptions, usersToInvite, translate, didScreenTransitionEnd]); const toggleOption = (option) => { Policy.clearErrors(props.route.params.policyID); @@ -248,56 +267,50 @@ function WorkspaceInvitePage(props) { shouldEnableMaxHeight testID={WorkspaceInvitePage.displayName} > - {({didScreenTransitionEnd}) => { - const sections = didScreenTransitionEnd ? getSections() : []; - - return ( - Navigation.goBack(ROUTES.SETTINGS_WORKSPACES)} - > - { - Policy.clearErrors(props.route.params.policyID); - Navigation.goBack(ROUTES.WORKSPACE_MEMBERS.getRoute(props.route.params.policyID)); - }} - /> - { - SearchInputManager.searchInput = value; - setSearchTerm(value); - }} - headerMessage={headerMessage} - onSelectRow={toggleOption} - onConfirm={inviteUser} - showScrollIndicator - showLoadingPlaceholder={!didScreenTransitionEnd || !OptionsListUtils.isPersonalDetailsReady(props.personalDetails)} - shouldPreventDefaultFocusOnSelectRow={!DeviceCapabilities.canUseTouchScreen()} - /> - - - - - ); - }} + Navigation.goBack(ROUTES.SETTINGS_WORKSPACES)} + > + { + Policy.clearErrors(props.route.params.policyID); + Navigation.goBack(ROUTES.WORKSPACE_MEMBERS.getRoute(props.route.params.policyID)); + }} + /> + { + SearchInputManager.searchInput = value; + setSearchTerm(value); + }} + headerMessage={headerMessage} + onSelectRow={toggleOption} + onConfirm={inviteUser} + showScrollIndicator + showLoadingPlaceholder={!didScreenTransitionEnd || !OptionsListUtils.isPersonalDetailsReady(props.personalDetails)} + shouldPreventDefaultFocusOnSelectRow={!DeviceCapabilities.canUseTouchScreen()} + /> + + + + ); } From 5a7eda2e246ece0334d9cc1613e2932ee617f077 Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Thu, 11 Jan 2024 10:38:04 +0100 Subject: [PATCH 149/391] remove unused js files --- .../headerWithBackButtonPropTypes.js | 101 ------------------ .../ThreeDotsMenuItemPropTypes.js | 12 --- src/components/ThreeDotsMenu/index.tsx | 2 - 3 files changed, 115 deletions(-) delete mode 100644 src/components/HeaderWithBackButton/headerWithBackButtonPropTypes.js delete mode 100644 src/components/ThreeDotsMenu/ThreeDotsMenuItemPropTypes.js diff --git a/src/components/HeaderWithBackButton/headerWithBackButtonPropTypes.js b/src/components/HeaderWithBackButton/headerWithBackButtonPropTypes.js deleted file mode 100644 index 109e60adf672..000000000000 --- a/src/components/HeaderWithBackButton/headerWithBackButtonPropTypes.js +++ /dev/null @@ -1,101 +0,0 @@ -import PropTypes from 'prop-types'; -import participantPropTypes from '@components/participantPropTypes'; -import {ThreeDotsMenuItemPropTypes} from '@components/ThreeDotsMenu'; -import iouReportPropTypes from '@pages/iouReportPropTypes'; - -const propTypes = { - /** Title of the Header */ - title: PropTypes.string, - - /** Subtitle of the header */ - subtitle: PropTypes.oneOfType([PropTypes.string, PropTypes.element]), - - /** Method to trigger when pressing download button of the header */ - onDownloadButtonPress: PropTypes.func, - - /** Method to trigger when pressing close button of the header */ - onCloseButtonPress: PropTypes.func, - - /** Method to trigger when pressing back button of the header */ - onBackButtonPress: PropTypes.func, - - /** Method to trigger when pressing more options button of the header */ - onThreeDotsButtonPress: PropTypes.func, - - /** Whether we should show a border on the bottom of the Header */ - shouldShowBorderBottom: PropTypes.bool, - - /** Whether we should show a download button */ - shouldShowDownloadButton: PropTypes.bool, - - /** Whether we should show a get assistance (question mark) button */ - shouldShowGetAssistanceButton: PropTypes.bool, - - /** Whether we should disable the get assistance button */ - shouldDisableGetAssistanceButton: PropTypes.bool, - - /** Whether we should show a pin button */ - shouldShowPinButton: PropTypes.bool, - - /** Whether we should show a more options (threedots) button */ - shouldShowThreeDotsButton: PropTypes.bool, - - /** Whether we should disable threedots button */ - shouldDisableThreeDotsButton: PropTypes.bool, - - /** List of menu items for more(three dots) menu */ - threeDotsMenuItems: ThreeDotsMenuItemPropTypes, - - /** The anchor position of the menu */ - threeDotsAnchorPosition: PropTypes.shape({ - top: PropTypes.number, - right: PropTypes.number, - bottom: PropTypes.number, - left: PropTypes.number, - }), - - /** Whether we should show a close button */ - shouldShowCloseButton: PropTypes.bool, - - /** Whether we should show a back button */ - shouldShowBackButton: PropTypes.bool, - - /** The guides call taskID to associate with the get assistance button, if we show it */ - guidesCallTaskID: PropTypes.string, - - /** Data to display a step counter in the header */ - stepCounter: PropTypes.shape({ - step: PropTypes.number, - total: PropTypes.number, - text: PropTypes.string, - }), - - /** Whether we should show an avatar */ - shouldShowAvatarWithDisplay: PropTypes.bool, - - /** Parent report, if provided it will override props.report for AvatarWithDisplay */ - parentReport: iouReportPropTypes, - - /** Report, if we're showing the details for one and using AvatarWithDisplay */ - report: iouReportPropTypes, - - /** The report's policy, if we're showing the details for a report and need info about it for AvatarWithDisplay */ - policy: PropTypes.shape({ - /** Name of the policy */ - name: PropTypes.string, - }), - - /** Policies, if we're showing the details for a report and need participant details for AvatarWithDisplay */ - personalDetails: PropTypes.objectOf(participantPropTypes), - - /** Children to wrap in Header */ - children: PropTypes.node, - - /** Single execution function to prevent concurrent navigation actions */ - singleExecution: PropTypes.func, - - /** Whether we should navigate to report page when the route have a topMostReport */ - shouldNavigateToTopMostReport: PropTypes.bool, -}; - -export default propTypes; diff --git a/src/components/ThreeDotsMenu/ThreeDotsMenuItemPropTypes.js b/src/components/ThreeDotsMenu/ThreeDotsMenuItemPropTypes.js deleted file mode 100644 index 9f09eabbc7f7..000000000000 --- a/src/components/ThreeDotsMenu/ThreeDotsMenuItemPropTypes.js +++ /dev/null @@ -1,12 +0,0 @@ -import PropTypes from 'prop-types'; -import sourcePropTypes from '@components/Image/sourcePropTypes'; - -const menuItemProps = PropTypes.arrayOf( - PropTypes.shape({ - icon: PropTypes.oneOfType([PropTypes.string, sourcePropTypes]), - text: PropTypes.string, - onPress: PropTypes.func, - }), -); - -export default menuItemProps; diff --git a/src/components/ThreeDotsMenu/index.tsx b/src/components/ThreeDotsMenu/index.tsx index ced2b67826a0..920b8f9f4130 100644 --- a/src/components/ThreeDotsMenu/index.tsx +++ b/src/components/ThreeDotsMenu/index.tsx @@ -16,7 +16,6 @@ import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; import type {AnchorPosition} from '@src/styles'; import type IconAsset from '@src/types/utils/IconAsset'; -import ThreeDotsMenuItemPropTypes from './ThreeDotsMenuItemPropTypes'; type ThreeDotsMenuProps = { /** Tooltip for the popup icon */ @@ -136,4 +135,3 @@ function ThreeDotsMenu({ ThreeDotsMenu.displayName = 'ThreeDotsMenu'; export default ThreeDotsMenu; -export {ThreeDotsMenuItemPropTypes}; From 35155fd1772c1c1d840cdaff9f1290b693b67507 Mon Sep 17 00:00:00 2001 From: hurali97 Date: Thu, 11 Jan 2024 16:50:28 +0500 Subject: [PATCH 150/391] fix: add resource keep rules to not remove assets --- android/app/src/main/res/raw/keep.xml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 android/app/src/main/res/raw/keep.xml diff --git a/android/app/src/main/res/raw/keep.xml b/android/app/src/main/res/raw/keep.xml new file mode 100644 index 000000000000..972e0416855c --- /dev/null +++ b/android/app/src/main/res/raw/keep.xml @@ -0,0 +1,5 @@ + + \ No newline at end of file From 256b9ae9d06765d2ef882fc8440c37f3b7c2a3c0 Mon Sep 17 00:00:00 2001 From: Kamil Owczarz Date: Fri, 12 Jan 2024 12:05:36 +0100 Subject: [PATCH 151/391] Update types --- src/components/Form/InputWrapper.tsx | 2 +- src/components/Form/types.ts | 9 ++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/components/Form/InputWrapper.tsx b/src/components/Form/InputWrapper.tsx index a12b181c07bd..8e824875c6d4 100644 --- a/src/components/Form/InputWrapper.tsx +++ b/src/components/Form/InputWrapper.tsx @@ -16,7 +16,7 @@ function InputWrapper({InputComponent, inputID, value // TODO: Sometimes we return too many props with register input, so we need to consider if it's better to make the returned type more general and disregard the issue, or we would like to omit the unused props somehow. // eslint-disable-next-line react/jsx-props-no-spreading - return ; + return ; } InputWrapper.displayName = 'InputWrapper'; diff --git a/src/components/Form/types.ts b/src/components/Form/types.ts index d6a9463f188f..0a9069ea596a 100644 --- a/src/components/Form/types.ts +++ b/src/components/Form/types.ts @@ -1,20 +1,19 @@ -import type {FocusEvent, MutableRefObject, ReactNode} from 'react'; +import type {ComponentProps, ElementType, FocusEvent, MutableRefObject, ReactNode} from 'react'; import type {StyleProp, ViewStyle} from 'react-native'; -import type TextInput from '@components/TextInput'; import type {BaseTextInputRef} from '@components/TextInput/BaseTextInput/types'; import type {OnyxFormKey, OnyxValues} from '@src/ONYXKEYS'; import type {Form} from '@src/types/onyx'; type ValueType = 'string' | 'boolean' | 'date'; -type ValidInput = typeof TextInput; +type ValidInput = ElementType; -type InputProps = Parameters[0] & { +type InputProps = ComponentProps & { shouldSetTouchedOnBlurOnly?: boolean; onValueChange?: (value: unknown, key: string) => void; onTouched?: (event: unknown) => void; valueType?: ValueType; - onBlur: (event: FocusEvent | Parameters[0]['onBlur']>>[0]) => void; + onBlur: (event: FocusEvent | Parameters['onBlur']>>[0]) => void; }; type InputWrapperProps = InputProps & { From d872300bcb879c4e9b447e116b74d539cd6c3e36 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Fri, 12 Jan 2024 17:18:14 +0100 Subject: [PATCH 152/391] remove empty line --- src/components/Composer/index.native.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/Composer/index.native.tsx b/src/components/Composer/index.native.tsx index c63a379ffeaa..c079091268ef 100644 --- a/src/components/Composer/index.native.tsx +++ b/src/components/Composer/index.native.tsx @@ -84,5 +84,4 @@ function Composer( } Composer.displayName = 'Composer'; - export default React.forwardRef(Composer); From 0bb8337928ef0d3a1dc216258116fd309abccc1d Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Fri, 12 Jan 2024 17:18:22 +0100 Subject: [PATCH 153/391] add empty line --- src/components/Composer/index.native.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/Composer/index.native.tsx b/src/components/Composer/index.native.tsx index c079091268ef..c63a379ffeaa 100644 --- a/src/components/Composer/index.native.tsx +++ b/src/components/Composer/index.native.tsx @@ -84,4 +84,5 @@ function Composer( } Composer.displayName = 'Composer'; + export default React.forwardRef(Composer); From c3676a73facd197c57d2c64bd2e2cd910b42df99 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Fri, 12 Jan 2024 17:26:47 +0100 Subject: [PATCH 154/391] rename prop --- src/components/Composer/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Composer/index.tsx b/src/components/Composer/index.tsx index 19b7bb6bb30a..a38f120ff237 100755 --- a/src/components/Composer/index.tsx +++ b/src/components/Composer/index.tsx @@ -367,7 +367,7 @@ function Composer( /* eslint-disable-next-line react/jsx-props-no-spreading */ {...props} onSelectionChange={addCursorPositionToSelectionChange} - rows={numberOfLines} + numberOfLines={numberOfLines} disabled={isDisabled} onKeyPress={handleKeyPress} onFocus={(e) => { From 15d87b177f2ec7c233a7ec036afbfab22cfae7cd Mon Sep 17 00:00:00 2001 From: gijoe0295 Date: Sat, 13 Jan 2024 03:28:57 +0700 Subject: [PATCH 155/391] dismiss money request banner --- .../iou/MoneyRequestReferralProgramCTA.tsx | 29 ++++++++++++++----- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/src/pages/iou/MoneyRequestReferralProgramCTA.tsx b/src/pages/iou/MoneyRequestReferralProgramCTA.tsx index 31394e1bd0e1..a752696baca2 100644 --- a/src/pages/iou/MoneyRequestReferralProgramCTA.tsx +++ b/src/pages/iou/MoneyRequestReferralProgramCTA.tsx @@ -1,6 +1,6 @@ -import React from 'react'; +import React, {useState} from 'react'; import Icon from '@components/Icon'; -import {Info} from '@components/Icon/Expensicons'; +import {Close} from '@components/Icon/Expensicons'; import {PressableWithoutFeedback} from '@components/Pressable'; import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; @@ -18,6 +18,11 @@ function MoneyRequestReferralProgramCTA({referralContentType}: MoneyRequestRefer const {translate} = useLocalize(); const styles = useThemeStyles(); const theme = useTheme(); + const [isHidden, setIsHidden] = useState(false); + + if (isHidden) { + return null; + } return ( - + setIsHidden(true)} + onMouseDown={(e) => { + e.preventDefault(); + }} + role={CONST.ACCESSIBILITY_ROLE.BUTTON} + accessibilityLabel={translate('common.close')} + > + + ); } From 06d2fbb62cc56eddac9bbf4895456eb6762dd733 Mon Sep 17 00:00:00 2001 From: Antony Kithinzi Date: Fri, 12 Jan 2024 16:49:56 -0500 Subject: [PATCH 156/391] feat: ReportActionItemCreated.js file TS migration * Created ReportActionItemCreated.tsx * feat: ReportActionItemCreated.js migration * "Migrated ReportActionItemCreated.js to TS" * "Created ReportActionItemCreated.tsx --- ...Created.js => ReportActionItemCreated.tsx} | 77 ++++++++++--------- 1 file changed, 40 insertions(+), 37 deletions(-) rename src/pages/home/report/{ReportActionItemCreated.js => ReportActionItemCreated.tsx} (68%) diff --git a/src/pages/home/report/ReportActionItemCreated.js b/src/pages/home/report/ReportActionItemCreated.tsx similarity index 68% rename from src/pages/home/report/ReportActionItemCreated.js rename to src/pages/home/report/ReportActionItemCreated.tsx index e5ec3e4b8744..22f9d6665e06 100644 --- a/src/pages/home/report/ReportActionItemCreated.js +++ b/src/pages/home/report/ReportActionItemCreated.tsx @@ -1,56 +1,61 @@ -import lodashGet from 'lodash/get'; -import PropTypes from 'prop-types'; import React, {memo} from 'react'; import {View} from 'react-native'; +import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import MultipleAvatars from '@components/MultipleAvatars'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; -import participantPropTypes from '@components/participantPropTypes'; import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; import ReportWelcomeText from '@components/ReportWelcomeText'; +import type {WithLocalizeProps} from '@components/withLocalize'; import withLocalize from '@components/withLocalize'; -import withWindowDimensions, {windowDimensionsPropTypes} from '@components/withWindowDimensions'; +import withWindowDimensions from '@components/withWindowDimensions'; +import type {WindowDimensionsProps} from '@components/withWindowDimensions/types'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; import compose from '@libs/compose'; import reportWithoutHasDraftSelector from '@libs/OnyxSelectors/reportWithoutHasDraftSelector'; import * as ReportUtils from '@libs/ReportUtils'; -import reportPropTypes from '@pages/reportPropTypes'; -import * as Report from '@userActions/Report'; +import {navigateToConciergeChatAndDeleteReport} from '@userActions/Report'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import type {PersonalDetailsList, Policy, Report} from '@src/types/onyx'; import AnimatedEmptyStateBackground from './AnimatedEmptyStateBackground'; -const propTypes = { - /** The id of the report */ - reportID: PropTypes.string.isRequired, - +type OnyxProps = { /** The report currently being looked at */ - report: reportPropTypes, + report: OnyxEntry; + + /** The policy being used */ + policy: OnyxEntry; /** Personal details of all the users */ - personalDetails: PropTypes.objectOf(participantPropTypes), + personalDetails: OnyxEntry; +}; + +type ReportActionItemCreatedProps = { + /** The id of the report */ + reportID: string; + + /** The id of the policy */ + // eslint-disable-next-line react/no-unused-prop-types + policyID: string; /** The policy object for the current route */ - policy: PropTypes.shape({ + policy?: { /** The name of the policy */ - name: PropTypes.string, + name?: string; /** The URL for the policy avatar */ - avatar: PropTypes.string, - }), - - ...windowDimensionsPropTypes, -}; -const defaultProps = { - report: {}, - personalDetails: {}, - policy: {}, -}; + avatar?: string; + }; +} & WindowDimensionsProps & + WithLocalizeProps & + OnyxProps; -function ReportActionItemCreated(props) { +function ReportActionItemCreated(props: ReportActionItemCreatedProps) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); + if (!ReportUtils.isChatReport(props.report)) { return null; } @@ -60,10 +65,10 @@ function ReportActionItemCreated(props) { return ( Report.navigateToConciergeChatAndDeleteReport(props.report.reportID)} + onClose={() => navigateToConciergeChatAndDeleteReport(props.report?.reportID ?? props.reportID)} needsOffscreenAlphaCompositing > @@ -99,14 +104,12 @@ function ReportActionItemCreated(props) { ); } -ReportActionItemCreated.defaultProps = defaultProps; -ReportActionItemCreated.propTypes = propTypes; ReportActionItemCreated.displayName = 'ReportActionItemCreated'; export default compose( - withWindowDimensions, + withWindowDimensions, withLocalize, - withOnyx({ + withOnyx({ report: { key: ({reportID}) => `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, selector: reportWithoutHasDraftSelector, @@ -122,10 +125,10 @@ export default compose( memo( ReportActionItemCreated, (prevProps, nextProps) => - lodashGet(prevProps.props, 'policy.name') === lodashGet(nextProps, 'policy.name') && - lodashGet(prevProps.props, 'policy.avatar') === lodashGet(nextProps, 'policy.avatar') && - lodashGet(prevProps.props, 'report.lastReadTime') === lodashGet(nextProps, 'report.lastReadTime') && - lodashGet(prevProps.props, 'report.statusNum') === lodashGet(nextProps, 'report.statusNum') && - lodashGet(prevProps.props, 'report.stateNum') === lodashGet(nextProps, 'report.stateNum'), + prevProps.policy?.name === nextProps.policy?.name && + prevProps.policy?.avatar === nextProps.policy?.avatar && + prevProps.report?.lastReadTime === nextProps.report?.lastReadTime && + prevProps.report?.statusNum === nextProps.report?.statusNum && + prevProps.report?.stateNum === nextProps.report?.stateNum, ), ); From f2dbeb169c26723ebfbb65c837e5c210aa42a8e6 Mon Sep 17 00:00:00 2001 From: Antony Kithinzi Date: Sat, 13 Jan 2024 00:10:37 +0100 Subject: [PATCH 157/391] refactor: improving code quality --- .../home/report/ReportActionItemCreated.tsx | 49 +++++++++---------- 1 file changed, 23 insertions(+), 26 deletions(-) diff --git a/src/pages/home/report/ReportActionItemCreated.tsx b/src/pages/home/report/ReportActionItemCreated.tsx index 22f9d6665e06..c1af2d08321a 100644 --- a/src/pages/home/report/ReportActionItemCreated.tsx +++ b/src/pages/home/report/ReportActionItemCreated.tsx @@ -6,19 +6,16 @@ import MultipleAvatars from '@components/MultipleAvatars'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; import ReportWelcomeText from '@components/ReportWelcomeText'; -import type {WithLocalizeProps} from '@components/withLocalize'; -import withLocalize from '@components/withLocalize'; -import withWindowDimensions from '@components/withWindowDimensions'; -import type {WindowDimensionsProps} from '@components/withWindowDimensions/types'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; -import compose from '@libs/compose'; import reportWithoutHasDraftSelector from '@libs/OnyxSelectors/reportWithoutHasDraftSelector'; import * as ReportUtils from '@libs/ReportUtils'; import {navigateToConciergeChatAndDeleteReport} from '@userActions/Report'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {PersonalDetailsList, Policy, Report} from '@src/types/onyx'; +import useLocalize from '@hooks/useLocalize'; +import useWindowDimensions from '@hooks/useWindowDimensions'; import AnimatedEmptyStateBackground from './AnimatedEmptyStateBackground'; type OnyxProps = { @@ -32,7 +29,7 @@ type OnyxProps = { personalDetails: OnyxEntry; }; -type ReportActionItemCreatedProps = { +type ReportActionItemCreatedProps = OnyxProps & { /** The id of the report */ reportID: string; @@ -48,14 +45,15 @@ type ReportActionItemCreatedProps = { /** The URL for the policy avatar */ avatar?: string; }; -} & WindowDimensionsProps & - WithLocalizeProps & - OnyxProps; - +} function ReportActionItemCreated(props: ReportActionItemCreatedProps) { + const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); + const {translate} = useLocalize(); + const {isSmallScreenWidth, isLargeScreenWidth} = useWindowDimensions(); + if (!ReportUtils.isChatReport(props.report)) { return null; } @@ -71,25 +69,25 @@ function ReportActionItemCreated(props: ReportActionItemCreatedProps) { onClose={() => navigateToConciergeChatAndDeleteReport(props.report?.reportID ?? props.reportID)} needsOffscreenAlphaCompositing > - + ReportUtils.navigateToDetailsPage(props.report)} style={[styles.mh5, styles.mb3, styles.alignSelfStart]} - accessibilityLabel={props.translate('common.details')} + accessibilityLabel={translate('common.details')} role={CONST.ROLE.BUTTON} disabled={shouldDisableDetailPage} > @@ -106,22 +104,21 @@ function ReportActionItemCreated(props: ReportActionItemCreatedProps) { ReportActionItemCreated.displayName = 'ReportActionItemCreated'; -export default compose( - withWindowDimensions, - withLocalize, - withOnyx({ +export default withOnyx({ report: { key: ({reportID}) => `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, selector: reportWithoutHasDraftSelector, }, - personalDetails: { - key: ONYXKEYS.PERSONAL_DETAILS_LIST, - }, + policy: { key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, }, - }), -)( + + personalDetails: { + key: ONYXKEYS.PERSONAL_DETAILS_LIST, + }, + + })( memo( ReportActionItemCreated, (prevProps, nextProps) => From 8e521b407ea9a934e2dfa3d031a3ba5f50ef27f4 Mon Sep 17 00:00:00 2001 From: Antony Kithinzi Date: Sat, 13 Jan 2024 00:15:05 +0100 Subject: [PATCH 158/391] fmt: prettier --- .../home/report/ReportActionItemCreated.tsx | 36 +++++++++---------- 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/src/pages/home/report/ReportActionItemCreated.tsx b/src/pages/home/report/ReportActionItemCreated.tsx index c1af2d08321a..86b7eef2c8ae 100644 --- a/src/pages/home/report/ReportActionItemCreated.tsx +++ b/src/pages/home/report/ReportActionItemCreated.tsx @@ -6,16 +6,16 @@ import MultipleAvatars from '@components/MultipleAvatars'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; import ReportWelcomeText from '@components/ReportWelcomeText'; +import useLocalize from '@hooks/useLocalize'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; +import useWindowDimensions from '@hooks/useWindowDimensions'; import reportWithoutHasDraftSelector from '@libs/OnyxSelectors/reportWithoutHasDraftSelector'; import * as ReportUtils from '@libs/ReportUtils'; import {navigateToConciergeChatAndDeleteReport} from '@userActions/Report'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {PersonalDetailsList, Policy, Report} from '@src/types/onyx'; -import useLocalize from '@hooks/useLocalize'; -import useWindowDimensions from '@hooks/useWindowDimensions'; import AnimatedEmptyStateBackground from './AnimatedEmptyStateBackground'; type OnyxProps = { @@ -45,9 +45,8 @@ type ReportActionItemCreatedProps = OnyxProps & { /** The URL for the policy avatar */ avatar?: string; }; -} +}; function ReportActionItemCreated(props: ReportActionItemCreatedProps) { - const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); @@ -105,27 +104,26 @@ function ReportActionItemCreated(props: ReportActionItemCreatedProps) { ReportActionItemCreated.displayName = 'ReportActionItemCreated'; export default withOnyx({ - report: { - key: ({reportID}) => `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, - selector: reportWithoutHasDraftSelector, - }, + report: { + key: ({reportID}) => `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, + selector: reportWithoutHasDraftSelector, + }, + + policy: { + key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + }, - policy: { - key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, - }, - - personalDetails: { - key: ONYXKEYS.PERSONAL_DETAILS_LIST, - }, - - })( + personalDetails: { + key: ONYXKEYS.PERSONAL_DETAILS_LIST, + }, +})( memo( ReportActionItemCreated, (prevProps, nextProps) => prevProps.policy?.name === nextProps.policy?.name && prevProps.policy?.avatar === nextProps.policy?.avatar && - prevProps.report?.lastReadTime === nextProps.report?.lastReadTime && + prevProps.report?.stateNum === nextProps.report?.stateNum && prevProps.report?.statusNum === nextProps.report?.statusNum && - prevProps.report?.stateNum === nextProps.report?.stateNum, + prevProps.report?.lastReadTime === nextProps.report?.lastReadTime, ), ); From c4929bae75fa1b3211ab91a7f103536daa17c6da Mon Sep 17 00:00:00 2001 From: Esh Tanya Gupta <77237602+esh-g@users.noreply.github.com> Date: Sat, 13 Jan 2024 15:52:22 +0530 Subject: [PATCH 159/391] Update src/components/AvatarCropModal/AvatarCropModal.tsx MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Fábio Henriques --- src/components/AvatarCropModal/AvatarCropModal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/AvatarCropModal/AvatarCropModal.tsx b/src/components/AvatarCropModal/AvatarCropModal.tsx index 7c9313a6d394..ec8b87d9f232 100644 --- a/src/components/AvatarCropModal/AvatarCropModal.tsx +++ b/src/components/AvatarCropModal/AvatarCropModal.tsx @@ -27,7 +27,7 @@ import Slider from './Slider'; type AvatarCropModalProps = { /** Link to image for cropping */ - imageUri: string; + imageUri?: string; /** Name of the image */ imageName: string; From 5278e70cb628450776f0b8ec31dfe9761991bea5 Mon Sep 17 00:00:00 2001 From: Esh Tanya Gupta <77237602+esh-g@users.noreply.github.com> Date: Sat, 13 Jan 2024 15:52:33 +0530 Subject: [PATCH 160/391] Update src/components/AvatarCropModal/AvatarCropModal.tsx MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Fábio Henriques --- src/components/AvatarCropModal/AvatarCropModal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/AvatarCropModal/AvatarCropModal.tsx b/src/components/AvatarCropModal/AvatarCropModal.tsx index ec8b87d9f232..fa2e7fb6a770 100644 --- a/src/components/AvatarCropModal/AvatarCropModal.tsx +++ b/src/components/AvatarCropModal/AvatarCropModal.tsx @@ -30,7 +30,7 @@ type AvatarCropModalProps = { imageUri?: string; /** Name of the image */ - imageName: string; + imageName?: string; /** Type of the image file */ imageType: string; From 050e162046df43152186077aed05cb361725aa64 Mon Sep 17 00:00:00 2001 From: Esh Tanya Gupta <77237602+esh-g@users.noreply.github.com> Date: Sat, 13 Jan 2024 15:52:42 +0530 Subject: [PATCH 161/391] Update src/components/AvatarCropModal/AvatarCropModal.tsx MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Fábio Henriques --- src/components/AvatarCropModal/AvatarCropModal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/AvatarCropModal/AvatarCropModal.tsx b/src/components/AvatarCropModal/AvatarCropModal.tsx index fa2e7fb6a770..cf26c0e568f2 100644 --- a/src/components/AvatarCropModal/AvatarCropModal.tsx +++ b/src/components/AvatarCropModal/AvatarCropModal.tsx @@ -33,7 +33,7 @@ type AvatarCropModalProps = { imageName?: string; /** Type of the image file */ - imageType: string; + imageType?: string; /** Callback to be called when user closes the modal */ onClose: () => void; From 993d3f55882704b8e7e3dc2beb6c937ce28ab5ad Mon Sep 17 00:00:00 2001 From: Esh Tanya Gupta <77237602+esh-g@users.noreply.github.com> Date: Sat, 13 Jan 2024 15:52:53 +0530 Subject: [PATCH 162/391] Update src/components/AvatarCropModal/AvatarCropModal.tsx MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Fábio Henriques --- src/components/AvatarCropModal/AvatarCropModal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/AvatarCropModal/AvatarCropModal.tsx b/src/components/AvatarCropModal/AvatarCropModal.tsx index cf26c0e568f2..06aa8e673c4f 100644 --- a/src/components/AvatarCropModal/AvatarCropModal.tsx +++ b/src/components/AvatarCropModal/AvatarCropModal.tsx @@ -36,7 +36,7 @@ type AvatarCropModalProps = { imageType?: string; /** Callback to be called when user closes the modal */ - onClose: () => void; + onClose?: () => void; /** Callback to be called when user saves the image */ onSave: (image: File | CustomRNImageManipulatorResult) => void; From f10ab52c44707fed958b82e378d8ac40e7fa0e08 Mon Sep 17 00:00:00 2001 From: Esh Tanya Gupta <77237602+esh-g@users.noreply.github.com> Date: Sat, 13 Jan 2024 15:53:03 +0530 Subject: [PATCH 163/391] Update src/components/AvatarCropModal/AvatarCropModal.tsx MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Fábio Henriques --- src/components/AvatarCropModal/AvatarCropModal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/AvatarCropModal/AvatarCropModal.tsx b/src/components/AvatarCropModal/AvatarCropModal.tsx index 06aa8e673c4f..b5dc240f5fbf 100644 --- a/src/components/AvatarCropModal/AvatarCropModal.tsx +++ b/src/components/AvatarCropModal/AvatarCropModal.tsx @@ -39,7 +39,7 @@ type AvatarCropModalProps = { onClose?: () => void; /** Callback to be called when user saves the image */ - onSave: (image: File | CustomRNImageManipulatorResult) => void; + onSave?: (image: File | CustomRNImageManipulatorResult) => void; /** Modal visibility */ isVisible: boolean; From 4d3f0be78573d743dc3897354d77adca09e1167f Mon Sep 17 00:00:00 2001 From: Esh Tanya Gupta <77237602+esh-g@users.noreply.github.com> Date: Sat, 13 Jan 2024 15:57:18 +0530 Subject: [PATCH 164/391] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Fábio Henriques --- src/components/AvatarCropModal/AvatarCropModal.tsx | 6 ++++++ src/components/AvatarCropModal/ImageCropView.tsx | 6 +++--- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/components/AvatarCropModal/AvatarCropModal.tsx b/src/components/AvatarCropModal/AvatarCropModal.tsx index b5dc240f5fbf..ba8b0bc14590 100644 --- a/src/components/AvatarCropModal/AvatarCropModal.tsx +++ b/src/components/AvatarCropModal/AvatarCropModal.tsx @@ -143,6 +143,9 @@ function AvatarCropModal({imageUri = '', imageName = '', imageType = '', onClose * @param {Array} minMax * @returns {Number} */ + /** + * Validates that value is within the provided mix/max range. + */ const clamp = useWorkletCallback((value: number, [min, max]) => interpolate(value, [min, max], [min, max], 'clamp'), []); /** @@ -171,6 +174,9 @@ function AvatarCropModal({imageUri = '', imageName = '', imageType = '', onClose * @param {Number} newX * @param {Number} newY */ + /** + * Validates the offset to prevent overflow, and updates the image offset. + */ const updateImageOffset = useWorkletCallback( (offsetX: number, offsetY: number) => { const {height, width} = getDisplayedImageSize(); diff --git a/src/components/AvatarCropModal/ImageCropView.tsx b/src/components/AvatarCropModal/ImageCropView.tsx index e76ffc2d1f10..d6077cd9eb1c 100644 --- a/src/components/AvatarCropModal/ImageCropView.tsx +++ b/src/components/AvatarCropModal/ImageCropView.tsx @@ -14,10 +14,10 @@ import type IconAsset from '@src/types/utils/IconAsset'; type ImageCropViewProps = { /** Link to image for cropping */ - imageUri: string; + imageUri?: string; /** Size of the image container that will be rendered */ - containerSize: number; + containerSize?: number; /** The height of the selected image */ originalImageHeight: { @@ -50,7 +50,7 @@ type ImageCropViewProps = { }; /** React-native-reanimated lib handler which executes when the user is panning image */ - panGestureEventHandler: (event: GestureEvent) => void; + panGestureEventHandler?: (event: GestureEvent) => void; /** Image crop vector mask */ maskImage?: IconAsset; From 7f4e78fe41597a8b0fbc31601436490d82ac1559 Mon Sep 17 00:00:00 2001 From: someone-here Date: Sat, 13 Jan 2024 16:04:28 +0530 Subject: [PATCH 165/391] Remove JS-Doc comments --- .../AvatarCropModal/AvatarCropModal.tsx | 20 ------------------- 1 file changed, 20 deletions(-) diff --git a/src/components/AvatarCropModal/AvatarCropModal.tsx b/src/components/AvatarCropModal/AvatarCropModal.tsx index ba8b0bc14590..3190b9c75fc5 100644 --- a/src/components/AvatarCropModal/AvatarCropModal.tsx +++ b/src/components/AvatarCropModal/AvatarCropModal.tsx @@ -136,13 +136,6 @@ function AvatarCropModal({imageUri = '', imageName = '', imageType = '', onClose }); }, [imageUri, originalImageHeight, originalImageWidth, rotation, translateSlider]); - /** - * Validates that value is within the provided mix/max range. - * - * @param {Number} value - * @param {Array} minMax - * @returns {Number} - */ /** * Validates that value is within the provided mix/max range. */ @@ -150,8 +143,6 @@ function AvatarCropModal({imageUri = '', imageName = '', imageType = '', onClose /** * Returns current image size taking into account scale and rotation. - * - * @returns {Object} */ const getDisplayedImageSize = useWorkletCallback(() => { let height = imageContainerSize * scale.value; @@ -168,12 +159,6 @@ function AvatarCropModal({imageUri = '', imageName = '', imageType = '', onClose return {height, width}; }, [imageContainerSize, scale]); - /** - * Validates the offset to prevent overflow, and updates the image offset. - * - * @param {Number} newX - * @param {Number} newY - */ /** * Validates the offset to prevent overflow, and updates the image offset. */ @@ -190,11 +175,6 @@ function AvatarCropModal({imageUri = '', imageName = '', imageType = '', onClose [imageContainerSize, scale, clamp], ); - /** - * @param {Number} newSliderValue - * @param {Number} containerSize - * @returns {Number} - */ const newScaleValue = useWorkletCallback((newSliderValue: number, containerSize: number) => { const {MAX_SCALE, MIN_SCALE} = CONST.AVATAR_CROP_MODAL; return (newSliderValue / containerSize) * (MAX_SCALE - MIN_SCALE) + MIN_SCALE; From 96d86459d4a5a99eea547c73a2e9dfdcb5360420 Mon Sep 17 00:00:00 2001 From: someone-here Date: Sat, 13 Jan 2024 16:19:35 +0530 Subject: [PATCH 166/391] Use shared value type --- .../AvatarCropModal/ImageCropView.tsx | 25 ++++++------------- 1 file changed, 7 insertions(+), 18 deletions(-) diff --git a/src/components/AvatarCropModal/ImageCropView.tsx b/src/components/AvatarCropModal/ImageCropView.tsx index d6077cd9eb1c..f086697a3e24 100644 --- a/src/components/AvatarCropModal/ImageCropView.tsx +++ b/src/components/AvatarCropModal/ImageCropView.tsx @@ -3,6 +3,7 @@ import {View} from 'react-native'; import {PanGestureHandler} from 'react-native-gesture-handler'; import type {GestureEvent, PanGestureHandlerEventPayload} from 'react-native-gesture-handler'; import Animated, {interpolate, useAnimatedStyle} from 'react-native-reanimated'; +import type {SharedValue} from 'react-native-reanimated'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; import useStyleUtils from '@hooks/useStyleUtils'; @@ -20,34 +21,22 @@ type ImageCropViewProps = { containerSize?: number; /** The height of the selected image */ - originalImageHeight: { - value: number; - }; + originalImageHeight: SharedValue; /** The width of the selected image */ - originalImageWidth: { - value: number; - }; + originalImageWidth: SharedValue; /** The rotation value of the selected image */ - rotation: { - value: number; - }; + rotation: SharedValue; /** The relative image shift along X-axis */ - translateX: { - value: number; - }; + translateX: SharedValue; /** The relative image shift along Y-axis */ - translateY: { - value: number; - }; + translateY: SharedValue; /** The scale factor of the image */ - scale: { - value: number; - }; + scale: SharedValue; /** React-native-reanimated lib handler which executes when the user is panning image */ panGestureEventHandler?: (event: GestureEvent) => void; From e9a397960ca982ae30b5ca0d17442200e62f19b8 Mon Sep 17 00:00:00 2001 From: someone-here Date: Sat, 13 Jan 2024 16:26:59 +0530 Subject: [PATCH 167/391] Remove SelectionElement and use HTMLElement --- src/components/AvatarCropModal/ImageCropView.tsx | 3 +-- src/components/AvatarCropModal/Slider.tsx | 3 +-- src/libs/ControlSelection/index.ts | 6 +++--- src/libs/ControlSelection/types.ts | 8 +++----- 4 files changed, 8 insertions(+), 12 deletions(-) diff --git a/src/components/AvatarCropModal/ImageCropView.tsx b/src/components/AvatarCropModal/ImageCropView.tsx index f086697a3e24..b45d7c63b088 100644 --- a/src/components/AvatarCropModal/ImageCropView.tsx +++ b/src/components/AvatarCropModal/ImageCropView.tsx @@ -10,7 +10,6 @@ import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import ControlSelection from '@libs/ControlSelection'; -import type {SelectionElement} from '@libs/ControlSelection/types'; import type IconAsset from '@src/types/utils/IconAsset'; type ImageCropViewProps = { @@ -75,7 +74,7 @@ function ImageCropView({imageUri = '', containerSize = 0, panGestureEventHandler { - ControlSelection.blockElement(el as SelectionElement); + ControlSelection.blockElement(el as HTMLElement | null); }} style={[containerStyle, styles.imageCropContainer]} > diff --git a/src/components/AvatarCropModal/Slider.tsx b/src/components/AvatarCropModal/Slider.tsx index 26e35517f48e..686255ac430a 100644 --- a/src/components/AvatarCropModal/Slider.tsx +++ b/src/components/AvatarCropModal/Slider.tsx @@ -7,7 +7,6 @@ import Tooltip from '@components/Tooltip'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import ControlSelection from '@libs/ControlSelection'; -import type {SelectionElement} from '@libs/ControlSelection/types'; type SliderProps = { /** React-native-reanimated lib handler which executes when the user is panning slider */ @@ -36,7 +35,7 @@ function Slider({onGesture = () => {}, sliderValue = {value: 0}}: SliderProps) { return ( { - ControlSelection.blockElement(el as SelectionElement); + ControlSelection.blockElement(el as HTMLElement | null); }} style={styles.sliderBar} > diff --git a/src/libs/ControlSelection/index.ts b/src/libs/ControlSelection/index.ts index 89f1e62aa6f6..61808cfbc1b5 100644 --- a/src/libs/ControlSelection/index.ts +++ b/src/libs/ControlSelection/index.ts @@ -1,4 +1,4 @@ -import type {ControlSelectionModule, SelectionElement} from './types'; +import type {ControlSelectionModule} from './types'; /** * Block selection on the whole app @@ -19,7 +19,7 @@ function unblock() { /** * Block selection on particular element */ -function blockElement(element?: SelectionElement | null) { +function blockElement(element?: HTMLElement | null) { if (!element) { return; } @@ -31,7 +31,7 @@ function blockElement(element?: SelectionElement | null) { /** * Unblock selection on particular element */ -function unblockElement(element?: SelectionElement | null) { +function unblockElement(element?: HTMLElement | null) { if (!element) { return; } diff --git a/src/libs/ControlSelection/types.ts b/src/libs/ControlSelection/types.ts index 8433a366ed91..b40e4d6f7a84 100644 --- a/src/libs/ControlSelection/types.ts +++ b/src/libs/ControlSelection/types.ts @@ -1,10 +1,8 @@ -type SelectionElement = T & {onselectstart: () => boolean}; - type ControlSelectionModule = { block: () => void; unblock: () => void; - blockElement: (element?: SelectionElement | null) => void; - unblockElement: (element?: SelectionElement | null) => void; + blockElement: (element?: HTMLElement | null) => void; + unblockElement: (element?: HTMLElement | null) => void; }; -export type {ControlSelectionModule, SelectionElement}; +export type {ControlSelectionModule}; From 2c25037fcb763a8f79694dd4fbfe82cf73ca6db2 Mon Sep 17 00:00:00 2001 From: someone-here Date: Sat, 13 Jan 2024 16:35:05 +0530 Subject: [PATCH 168/391] Use default export --- src/libs/ControlSelection/index.native.ts | 2 +- src/libs/ControlSelection/index.ts | 2 +- src/libs/ControlSelection/types.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/libs/ControlSelection/index.native.ts b/src/libs/ControlSelection/index.native.ts index 2bccea946cda..b45af6da6441 100644 --- a/src/libs/ControlSelection/index.native.ts +++ b/src/libs/ControlSelection/index.native.ts @@ -1,4 +1,4 @@ -import type {ControlSelectionModule} from './types'; +import type ControlSelectionModule from './types'; function block() {} function unblock() {} diff --git a/src/libs/ControlSelection/index.ts b/src/libs/ControlSelection/index.ts index 61808cfbc1b5..44787dc77dbe 100644 --- a/src/libs/ControlSelection/index.ts +++ b/src/libs/ControlSelection/index.ts @@ -1,4 +1,4 @@ -import type {ControlSelectionModule} from './types'; +import type ControlSelectionModule from './types'; /** * Block selection on the whole app diff --git a/src/libs/ControlSelection/types.ts b/src/libs/ControlSelection/types.ts index b40e4d6f7a84..c4ca4b713b9b 100644 --- a/src/libs/ControlSelection/types.ts +++ b/src/libs/ControlSelection/types.ts @@ -5,4 +5,4 @@ type ControlSelectionModule = { unblockElement: (element?: HTMLElement | null) => void; }; -export type {ControlSelectionModule}; +export default ControlSelectionModule; From 32e59df705bc16b4581dc6c554f1bc13b5cd3b87 Mon Sep 17 00:00:00 2001 From: someone-here Date: Sat, 13 Jan 2024 16:40:53 +0530 Subject: [PATCH 169/391] Match the optional props --- src/components/AvatarCropModal/Slider.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/AvatarCropModal/Slider.tsx b/src/components/AvatarCropModal/Slider.tsx index 686255ac430a..89b470be2cd3 100644 --- a/src/components/AvatarCropModal/Slider.tsx +++ b/src/components/AvatarCropModal/Slider.tsx @@ -10,10 +10,10 @@ import ControlSelection from '@libs/ControlSelection'; type SliderProps = { /** React-native-reanimated lib handler which executes when the user is panning slider */ - onGesture: (event: GestureEvent) => void; + onGesture?: (event: GestureEvent) => void; /** X position of the slider knob */ - sliderValue: { + sliderValue?: { value: number; }; }; From 5afbe63d3d9e90b162f8db49110c442c8f202342 Mon Sep 17 00:00:00 2001 From: Fitsum Abebe Date: Mon, 15 Jan 2024 00:10:33 +0300 Subject: [PATCH 170/391] set unread marker and read the report on visibility change --- src/libs/actions/Report.ts | 7 +++++- src/pages/home/report/ReportActionsList.js | 28 +++++++++++++++++++++- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index b182b7019846..6951a05be5a1 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -918,7 +918,7 @@ function expandURLPreview(reportID: string, reportActionID: string) { } /** Marks the new report actions as read */ -function readNewestAction(reportID: string) { +function readNewestAction(reportID: string, shouldEmitEvent = true) { const lastReadTime = DateUtils.getDBTime(); const optimisticData: OnyxUpdate[] = [ @@ -942,6 +942,11 @@ function readNewestAction(reportID: string) { }; API.write('ReadNewestAction', parameters, {optimisticData}); + + if (!shouldEmitEvent) { + return; + } + DeviceEventEmitter.emit(`readNewestAction_${reportID}`, lastReadTime); } diff --git a/src/pages/home/report/ReportActionsList.js b/src/pages/home/report/ReportActionsList.js index dba8ef2e11d0..d08fd2a42ea6 100644 --- a/src/pages/home/report/ReportActionsList.js +++ b/src/pages/home/report/ReportActionsList.js @@ -167,6 +167,7 @@ function ReportActionsList({ const reportActionSize = useRef(sortedVisibleReportActions.length); const previousLastIndex = useRef(lastActionIndex); + const visibilityCallback = useRef(() => {}); const linkedReportActionID = lodashGet(route, 'params.reportActionID', ''); @@ -386,7 +387,7 @@ function ReportActionsList({ [currentUnreadMarker, sortedVisibleReportActions, report.reportID, messageManuallyMarkedUnread], ); - useEffect(() => { + const calculateUnreadMarker = () => { // Iterate through the report actions and set appropriate unread marker. // This is to avoid a warning of: // Cannot update a component (ReportActionsList) while rendering a different component (CellRenderer). @@ -404,8 +405,33 @@ function ReportActionsList({ if (!markerFound) { setCurrentUnreadMarker(null); } + }; + + useEffect(() => { + calculateUnreadMarker(); + + /* eslint-disable-next-line react-hooks/exhaustive-deps */ }, [sortedVisibleReportActions, report.lastReadTime, report.reportID, messageManuallyMarkedUnread, shouldDisplayNewMarker, currentUnreadMarker]); + visibilityCallback.current = () => { + if (!Visibility.isVisible() || scrollingVerticalOffset.current >= MSG_VISIBLE_THRESHOLD || !ReportUtils.isUnread(report)) { + return; + } + + Report.readNewestAction(report.reportID, false); + userActiveSince.current = DateUtils.getDBTime(); + setCurrentUnreadMarker(null); + calculateUnreadMarker(); + }; + + useEffect(() => { + const unsubscribeVisibilityListener = Visibility.onVisibilityChange(() => visibilityCallback.current()); + + return unsubscribeVisibilityListener; + + /* eslint-disable-next-line react-hooks/exhaustive-deps */ + }, [report.reportID]); + const renderItem = useCallback( ({item: reportAction, index}) => ( Date: Sun, 14 Jan 2024 19:25:14 -0300 Subject: [PATCH 171/391] Pass 'didScreenTransitionEnd' from 'StepParticipants.StepScreenWrapper' to 'ParticipantsSelector' --- ...aryForRefactorRequestParticipantsSelector.js | 5 +++++ .../request/step/IOURequestStepParticipants.js | 17 ++++++++++------- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js index d9ae8b9fab1c..9246fb202534 100644 --- a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js +++ b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js @@ -56,6 +56,9 @@ const propTypes = { /** Whether we are searching for reports in the server */ isSearchingForReports: PropTypes.bool, + + /** Whether the parent screen transition has ended */ + didScreenTransitionEnd: PropTypes.bool, }; const defaultProps = { @@ -64,6 +67,7 @@ const defaultProps = { reports: {}, betas: [], isSearchingForReports: false, + didScreenTransitionEnd: false, }; function MoneyTemporaryForRefactorRequestParticipantsSelector({ @@ -76,6 +80,7 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({ iouType, iouRequestType, isSearchingForReports, + didScreenTransitionEnd, }) { const {translate} = useLocalize(); const styles = useThemeStyles(); diff --git a/src/pages/iou/request/step/IOURequestStepParticipants.js b/src/pages/iou/request/step/IOURequestStepParticipants.js index 9f06360ef3b1..aad85307b3e4 100644 --- a/src/pages/iou/request/step/IOURequestStepParticipants.js +++ b/src/pages/iou/request/step/IOURequestStepParticipants.js @@ -87,13 +87,16 @@ function IOURequestStepParticipants({ testID={IOURequestStepParticipants.displayName} includeSafeAreaPaddingBottom > - + {({didScreenTransitionEnd}) => ( + + )} ); } From 8af2b8d78b27d08305879c39e097a776060f2d6e Mon Sep 17 00:00:00 2001 From: brunovjk Date: Sun, 14 Jan 2024 19:48:24 -0300 Subject: [PATCH 172/391] Handle OptionsListUtils logic after screen transition --- ...emporaryForRefactorRequestParticipantsSelector.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js index 9246fb202534..f6731f775b9b 100644 --- a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js +++ b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js @@ -99,6 +99,16 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({ */ const [sections, newChatOptions] = useMemo(() => { const newSections = []; + if (!didScreenTransitionEnd) { + return [ + newSections, + { + recentReports: {}, + personalDetails: {}, + userToInvite: {}, + } + ]; + } let indexOffset = 0; const chatOptions = OptionsListUtils.getFilteredOptions( @@ -173,7 +183,7 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({ } return [newSections, chatOptions]; - }, [reports, personalDetails, betas, searchTerm, participants, iouType, iouRequestType, maxParticipantsReached, translate]); + }, [didScreenTransitionEnd, reports, personalDetails, betas, searchTerm, participants, iouType, iouRequestType, maxParticipantsReached, translate]); /** * Adds a single participant to the request From a2611cc2e153b10d571bb4362821fc0b92467456 Mon Sep 17 00:00:00 2001 From: brunovjk Date: Sun, 14 Jan 2024 21:01:27 -0300 Subject: [PATCH 173/391] Implement 'isLoadingNewOptions' to 'BaseSelectionList' --- src/components/SelectionList/BaseSelectionList.js | 2 ++ src/components/SelectionList/selectionListPropTypes.js | 3 +++ 2 files changed, 5 insertions(+) diff --git a/src/components/SelectionList/BaseSelectionList.js b/src/components/SelectionList/BaseSelectionList.js index 960618808fd9..221436e1020e 100644 --- a/src/components/SelectionList/BaseSelectionList.js +++ b/src/components/SelectionList/BaseSelectionList.js @@ -45,6 +45,7 @@ function BaseSelectionList({ inputMode = CONST.INPUT_MODE.TEXT, onChangeText, initiallyFocusedOptionKey = '', + isLoadingNewOptions = false, onScroll, onScrollBeginDrag, headerMessage = '', @@ -428,6 +429,7 @@ function BaseSelectionList({ spellCheck={false} onSubmitEditing={selectFocusedOption} blurOnSubmit={Boolean(flattenedSections.allOptions.length)} + isLoading={isLoadingNewOptions} /> )} diff --git a/src/components/SelectionList/selectionListPropTypes.js b/src/components/SelectionList/selectionListPropTypes.js index f5178112a4c3..b0c5dd37867e 100644 --- a/src/components/SelectionList/selectionListPropTypes.js +++ b/src/components/SelectionList/selectionListPropTypes.js @@ -151,6 +151,9 @@ const propTypes = { /** Item `keyForList` to focus initially */ initiallyFocusedOptionKey: PropTypes.string, + /** Whether we are loading new options */ + isLoadingNewOptions: PropTypes.bool, + /** Callback to fire when the list is scrolled */ onScroll: PropTypes.func, From 301df39b60fc9f49b608b6932727f1eebd743c28 Mon Sep 17 00:00:00 2001 From: brunovjk Date: Sun, 14 Jan 2024 21:07:20 -0300 Subject: [PATCH 174/391] Adjust 'SelectionList' props --- ...oneyTemporaryForRefactorRequestParticipantsSelector.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js index f6731f775b9b..9fb91e34fb33 100644 --- a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js +++ b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js @@ -16,6 +16,7 @@ import useThemeStyles from '@hooks/useThemeStyles'; import * as Report from '@libs/actions/Report'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import * as OptionsListUtils from '@libs/OptionsListUtils'; +import * as ReportUtils from '@libs/ReportUtils'; import MoneyRequestReferralProgramCTA from '@pages/iou/MoneyRequestReferralProgramCTA'; import reportPropTypes from '@pages/reportPropTypes'; import CONST from '@src/CONST'; @@ -337,11 +338,13 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({ [addParticipantToSelection, isAllowedToSplit, styles, translate], ); + const isOptionsDataReady = useMemo(() => ReportUtils.isReportDataReady() && OptionsListUtils.isPersonalDetailsReady(personalDetails), [personalDetails]); + return ( 0 ? safeAreaPaddingBottomStyle : {}]}> ); From 6d15a87cc10a4f59e1b474272e563aa5968ba8c8 Mon Sep 17 00:00:00 2001 From: brunovjk Date: Sun, 14 Jan 2024 21:09:36 -0300 Subject: [PATCH 175/391] Fill MoneyRequestReferralProgramCTA icon --- src/pages/iou/MoneyRequestReferralProgramCTA.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pages/iou/MoneyRequestReferralProgramCTA.tsx b/src/pages/iou/MoneyRequestReferralProgramCTA.tsx index 31394e1bd0e1..30db04dffdac 100644 --- a/src/pages/iou/MoneyRequestReferralProgramCTA.tsx +++ b/src/pages/iou/MoneyRequestReferralProgramCTA.tsx @@ -41,6 +41,7 @@ function MoneyRequestReferralProgramCTA({referralContentType}: MoneyRequestRefer src={Info} height={20} width={20} + fill={theme.icon} /> ); From 43c38ba40b27b6f8450c4280f9cf6cd720640067 Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Mon, 15 Jan 2024 14:02:07 +0700 Subject: [PATCH 176/391] only display pending message for foreign currency transaction --- src/components/ReportActionItem/MoneyRequestAction.js | 2 +- src/libs/IOUUtils.ts | 10 ++++++---- tests/unit/IOUUtilsTest.js | 6 +++--- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/components/ReportActionItem/MoneyRequestAction.js b/src/components/ReportActionItem/MoneyRequestAction.js index 46226969636e..d242dbe23e86 100644 --- a/src/components/ReportActionItem/MoneyRequestAction.js +++ b/src/components/ReportActionItem/MoneyRequestAction.js @@ -116,7 +116,7 @@ function MoneyRequestAction({ const isDeletedParentAction = ReportActionsUtils.isDeletedParentAction(action); const isReversedTransaction = ReportActionsUtils.isReversedTransaction(action); if (!_.isEmpty(iouReport) && !_.isEmpty(reportActions) && chatReport.iouReportID && action.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD && network.isOffline) { - shouldShowPendingConversionMessage = IOUUtils.isIOUReportPendingCurrencyConversion(iouReport); + shouldShowPendingConversionMessage = IOUUtils.isTransactionPendingCurrencyConversion(iouReport, (action && action.originalMessage && action.originalMessage.IOUTransactionID) || 0); } return isDeletedParentAction || isReversedTransaction ? ( diff --git a/src/libs/IOUUtils.ts b/src/libs/IOUUtils.ts index 11dd0f5badda..aab9aac2391d 100644 --- a/src/libs/IOUUtils.ts +++ b/src/libs/IOUUtils.ts @@ -110,12 +110,14 @@ function updateIOUOwnerAndTotal(iouReport: OnyxEntry, actorAccountID: nu } /** - * Returns whether or not an IOU report contains money requests in a different currency + * Returns whether or not a transaction of IOU report contains money requests in a different currency * that are either created or cancelled offline, and thus haven't been converted to the report's currency yet */ -function isIOUReportPendingCurrencyConversion(iouReport: Report): boolean { +function isTransactionPendingCurrencyConversion(iouReport: Report, transactionID: string): boolean { const reportTransactions: Transaction[] = TransactionUtils.getAllReportTransactions(iouReport.reportID); - const pendingRequestsInDifferentCurrency = reportTransactions.filter((transaction) => transaction.pendingAction && TransactionUtils.getCurrency(transaction) !== iouReport.currency); + const pendingRequestsInDifferentCurrency = reportTransactions.filter( + (transaction) => transaction.pendingAction && transaction.transactionID === transactionID && TransactionUtils.getCurrency(transaction) !== iouReport.currency, + ); return pendingRequestsInDifferentCurrency.length > 0; } @@ -127,4 +129,4 @@ function isValidMoneyRequestType(iouType: string): boolean { return moneyRequestType.includes(iouType); } -export {calculateAmount, updateIOUOwnerAndTotal, isIOUReportPendingCurrencyConversion, isValidMoneyRequestType, navigateToStartMoneyRequestStep, navigateToStartStepIfScanFileCannotBeRead}; +export {calculateAmount, updateIOUOwnerAndTotal, isTransactionPendingCurrencyConversion, isValidMoneyRequestType, navigateToStartMoneyRequestStep, navigateToStartStepIfScanFileCannotBeRead}; diff --git a/tests/unit/IOUUtilsTest.js b/tests/unit/IOUUtilsTest.js index ac04b74a0ca5..7f239d9bb576 100644 --- a/tests/unit/IOUUtilsTest.js +++ b/tests/unit/IOUUtilsTest.js @@ -17,7 +17,7 @@ function initCurrencyList() { } describe('IOUUtils', () => { - describe('isIOUReportPendingCurrencyConversion', () => { + describe('isTransactionPendingCurrencyConversion', () => { beforeAll(() => { Onyx.init({ keys: ONYXKEYS, @@ -34,7 +34,7 @@ describe('IOUUtils', () => { [`${ONYXKEYS.COLLECTION.TRANSACTION}${aedPendingTransaction.transactionID}`]: aedPendingTransaction, }).then(() => { // We requested money offline in a different currency, we don't know the total of the iouReport until we're back online - expect(IOUUtils.isIOUReportPendingCurrencyConversion(iouReport)).toBe(true); + expect(IOUUtils.isTransactionPendingCurrencyConversion(iouReport, aedPendingTransaction.transactionID)).toBe(true); }); }); @@ -54,7 +54,7 @@ describe('IOUUtils', () => { }, }).then(() => { // We requested money online in a different currency, we know the iouReport total and there's no need to show the pending conversion message - expect(IOUUtils.isIOUReportPendingCurrencyConversion(iouReport)).toBe(false); + expect(IOUUtils.isTransactionPendingCurrencyConversion(iouReport, aedPendingTransaction.transactionID)).toBe(false); }); }); }); From a6b46827254d9e06249d98207771a6ede85150b3 Mon Sep 17 00:00:00 2001 From: tienifr Date: Mon, 15 Jan 2024 17:31:34 +0700 Subject: [PATCH 177/391] type fix --- src/components/ImageView/index.native.tsx | 18 +++++++++--------- src/components/ImageView/index.tsx | 21 ++++++++++++++++----- src/components/ImageView/types.ts | 7 ++----- src/components/Lightbox.js | 3 ++- src/components/MultiGestureCanvas/types.ts | 3 ++- src/styles/stylePropTypes.js | 2 +- 6 files changed, 32 insertions(+), 22 deletions(-) diff --git a/src/components/ImageView/index.native.tsx b/src/components/ImageView/index.native.tsx index 74db0869f9e1..e36bb39d2bed 100644 --- a/src/components/ImageView/index.native.tsx +++ b/src/components/ImageView/index.native.tsx @@ -1,21 +1,21 @@ import React from 'react'; import Lightbox from '@components/Lightbox'; import {zoomRangeDefaultProps} from '@components/MultiGestureCanvas/propTypes'; -import type * as ImageViewTypes from './types'; +import type {ImageViewProps} from './types'; function ImageView({ - isAuthTokenRequired, + isAuthTokenRequired = false, url, onScaleChanged, - onPress = () => {}, - style = {}, + onPress, + style, zoomRange = zoomRangeDefaultProps.zoomRange, onError, - isUsedInCarousel, - isSingleCarouselItem, - carouselItemIndex, - carouselActiveItemIndex, -}: ImageViewTypes.ImageViewProps) { + isUsedInCarousel = false, + isSingleCarouselItem = false, + carouselItemIndex = 0, + carouselActiveItemIndex = 0, +}: ImageViewProps) { const hasSiblingCarouselItems = isUsedInCarousel && !isSingleCarouselItem; return ( diff --git a/src/components/ImageView/index.tsx b/src/components/ImageView/index.tsx index f0d43121f9ef..3731a4806cbc 100644 --- a/src/components/ImageView/index.tsx +++ b/src/components/ImageView/index.tsx @@ -1,5 +1,5 @@ import React, {useCallback, useEffect, useRef, useState} from 'react'; -import type {GestureResponderEvent, LayoutChangeEvent} from 'react-native'; +import type {GestureResponderEvent, LayoutChangeEvent, NativeSyntheticEvent} from 'react-native'; import {View} from 'react-native'; import FullscreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import Image from '@components/Image'; @@ -10,9 +10,11 @@ import useThemeStyles from '@hooks/useThemeStyles'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import CONST from '@src/CONST'; import viewRef from '@src/types/utils/viewRef'; -import type * as ImageViewTypes from './types'; +import type {ImageLoadNativeEventData, ImageViewProps} from './types'; -function ImageView({isAuthTokenRequired = false, url, fileName, onError = () => {}}: ImageViewTypes.ImageViewProps) { +type ZoomDelta = {offsetX: number; offsetY: number}; + +function ImageView({isAuthTokenRequired = false, url, fileName, onError}: ImageViewProps) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); const [isLoading, setIsLoading] = useState(true); @@ -28,7 +30,7 @@ function ImageView({isAuthTokenRequired = false, url, fileName, onError = () => const [imgWidth, setImgWidth] = useState(0); const [imgHeight, setImgHeight] = useState(0); const [zoomScale, setZoomScale] = useState(0); - const [zoomDelta, setZoomDelta] = useState({offsetX: 0, offsetY: 0}); + const [zoomDelta, setZoomDelta] = useState(); const scrollableRef = useRef(null); const canUseTouchScreen = DeviceCapabilities.canUseTouchScreen(); @@ -49,6 +51,9 @@ function ImageView({isAuthTokenRequired = false, url, fileName, onError = () => setContainerWidth(width); }; + /** + * When open image, set image width, height. + */ const setImageRegion = (imageWidth: number, imageHeight: number) => { if (imageHeight <= 0) { return; @@ -67,7 +72,7 @@ function ImageView({isAuthTokenRequired = false, url, fileName, onError = () => setIsZoomed(false); }; - const imageLoad = ({nativeEvent}: {nativeEvent: ImageViewTypes.ImageLoadNativeEventData}) => { + const imageLoad = ({nativeEvent}: NativeSyntheticEvent) => { setImageRegion(nativeEvent.width, nativeEvent.height); setIsLoading(false); }; @@ -81,6 +86,12 @@ function ImageView({isAuthTokenRequired = false, url, fileName, onError = () => setInitialScrollTop(scrollableRef.current?.scrollTop ?? 0); }; + /** + * Convert touch point to zoomed point + * @param x point when click zoom + * @param y point when click zoom + * @returns converted touch point + */ const getScrollOffset = (x: number, y: number) => { let offsetX = 0; let offsetY = 0; diff --git a/src/components/ImageView/types.ts b/src/components/ImageView/types.ts index 80fabed0ee58..9ea51fd3c82c 100644 --- a/src/components/ImageView/types.ts +++ b/src/components/ImageView/types.ts @@ -1,12 +1,11 @@ import type {StyleProp, ViewStyle} from 'react-native'; -import type ZoomRange from '@components/MultiGestureCanvas/types'; +import type {ZoomRange} from '@components/MultiGestureCanvas/types'; type ImageViewProps = { /** Whether source url requires authentication */ isAuthTokenRequired?: boolean; /** Handles scale changed event in image zoom component. Used on native only */ - // eslint-disable-next-line react/no-unused-prop-types onScaleChanged: (scale: number) => void; /** URL to full-sized image */ @@ -18,9 +17,6 @@ type ImageViewProps = { /** Handles errors while displaying the image */ onError?: () => void; - /** Whether this view is the active screen */ - isFocused?: boolean; - /** Whether this AttachmentView is shown as part of a AttachmentCarousel */ isUsedInCarousel?: boolean; @@ -47,4 +43,5 @@ type ImageLoadNativeEventData = { width: number; height: number; }; + export type {ImageViewProps, ImageLoadNativeEventData}; diff --git a/src/components/Lightbox.js b/src/components/Lightbox.js index a941d23a5f16..8b7d68befafd 100644 --- a/src/components/Lightbox.js +++ b/src/components/Lightbox.js @@ -3,6 +3,7 @@ import PropTypes from 'prop-types'; import React, {useCallback, useEffect, useMemo, useState} from 'react'; import {ActivityIndicator, PixelRatio, StyleSheet, View} from 'react-native'; import useStyleUtils from '@hooks/useStyleUtils'; +import stylePropTypes from '@styles/stylePropTypes'; import * as AttachmentsPropTypes from './Attachments/propTypes'; import Image from './Image'; import MultiGestureCanvas from './MultiGestureCanvas'; @@ -44,7 +45,7 @@ const propTypes = { activeIndex: PropTypes.number, /** Additional styles to add to the component */ - style: PropTypes.oneOfType([PropTypes.bool, PropTypes.arrayOf(PropTypes.object), PropTypes.object]), + style: stylePropTypes, }; const defaultProps = { diff --git a/src/components/MultiGestureCanvas/types.ts b/src/components/MultiGestureCanvas/types.ts index 0242f045feef..3c8480257700 100644 --- a/src/components/MultiGestureCanvas/types.ts +++ b/src/components/MultiGestureCanvas/types.ts @@ -3,4 +3,5 @@ type ZoomRange = { max: number; }; -export default ZoomRange; +// eslint-disable-next-line import/prefer-default-export +export type {ZoomRange}; diff --git a/src/styles/stylePropTypes.js b/src/styles/stylePropTypes.js index f9ecdb98ff13..b82db94140ee 100644 --- a/src/styles/stylePropTypes.js +++ b/src/styles/stylePropTypes.js @@ -1,5 +1,5 @@ import PropTypes from 'prop-types'; -const stylePropTypes = PropTypes.oneOfType([PropTypes.object, PropTypes.arrayOf(PropTypes.object), PropTypes.func]); +const stylePropTypes = PropTypes.oneOfType([PropTypes.object, PropTypes.bool, PropTypes.arrayOf(PropTypes.object), PropTypes.func]); export default stylePropTypes; From f470ff9d2461632d5ba3800fe1c854f6b2f85768 Mon Sep 17 00:00:00 2001 From: Kamil Owczarz Date: Mon, 15 Jan 2024 11:35:39 +0100 Subject: [PATCH 178/391] WIP --- src/ONYXKEYS.ts | 1 - src/pages/settings/Profile/DisplayNamePage.tsx | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 13de58a2c21c..2915b7a4aa12 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -2,7 +2,6 @@ import type {OnyxEntry} from 'react-native-onyx/lib/types'; import type {ValueOf} from 'type-fest'; import type CONST from './CONST'; import type * as OnyxTypes from './types/onyx'; -import {ReimbursementAccountForm, ReimbursementAccountFormDraft} from './types/onyx'; import type DeepValueOf from './types/utils/DeepValueOf'; /** diff --git a/src/pages/settings/Profile/DisplayNamePage.tsx b/src/pages/settings/Profile/DisplayNamePage.tsx index 22c1c173e637..a481b9ccdbec 100644 --- a/src/pages/settings/Profile/DisplayNamePage.tsx +++ b/src/pages/settings/Profile/DisplayNamePage.tsx @@ -4,6 +4,7 @@ import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import FormProvider from '@components/Form/FormProvider'; import InputWrapper from '@components/Form/InputWrapper'; +import type {OnyxFormValuesFields} from '@components/Form/types'; import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; @@ -20,7 +21,6 @@ import * as PersonalDetails from '@userActions/PersonalDetails'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import { OnyxFormValuesFields } from '@components/Form/types'; const updateDisplayName = (values: any) => { PersonalDetails.updateDisplayName(values.firstName.trim(), values.lastName.trim()); @@ -38,7 +38,6 @@ function DisplayNamePage(props: any) { * @returns - An object containing the errors for each inputID */ const validate = (values: OnyxFormValuesFields) => { - console.log(`values = `, values); const errors = {}; // First we validate the first name field From 3f19479cb86d4204e9faf7d717b705a96a5b60a7 Mon Sep 17 00:00:00 2001 From: tienifr Date: Mon, 15 Jan 2024 17:41:57 +0700 Subject: [PATCH 179/391] fix: revert native event --- src/components/ImageView/index.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/ImageView/index.tsx b/src/components/ImageView/index.tsx index 3731a4806cbc..1c10c8116325 100644 --- a/src/components/ImageView/index.tsx +++ b/src/components/ImageView/index.tsx @@ -138,9 +138,10 @@ function ImageView({isAuthTokenRequired = false, url, fileName, onError}: ImageV }; const trackPointerPosition = useCallback( - (e: MouseEvent) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e: any) => { // Whether the pointer is released inside the ImageView - const isInsideImageView = scrollableRef.current?.contains(e.target as Node); + const isInsideImageView = scrollableRef.current?.contains(e.nativeEvent.target); if (!isInsideImageView && isZoomed && isDragging && isMouseDown) { setIsDragging(false); From 09dadf45d7434e97654c721ad4ef899eda118af4 Mon Sep 17 00:00:00 2001 From: Fitsum Abebe Date: Mon, 15 Jan 2024 15:20:21 +0300 Subject: [PATCH 180/391] updated to consider manual mark as read case --- src/pages/home/report/ReportActionsList.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/pages/home/report/ReportActionsList.js b/src/pages/home/report/ReportActionsList.js index d08fd2a42ea6..61e2e1ce14bb 100644 --- a/src/pages/home/report/ReportActionsList.js +++ b/src/pages/home/report/ReportActionsList.js @@ -414,13 +414,14 @@ function ReportActionsList({ }, [sortedVisibleReportActions, report.lastReadTime, report.reportID, messageManuallyMarkedUnread, shouldDisplayNewMarker, currentUnreadMarker]); visibilityCallback.current = () => { - if (!Visibility.isVisible() || scrollingVerticalOffset.current >= MSG_VISIBLE_THRESHOLD || !ReportUtils.isUnread(report)) { + if (!Visibility.isVisible() || scrollingVerticalOffset.current >= MSG_VISIBLE_THRESHOLD || !ReportUtils.isUnread(report) || messageManuallyMarkedUnread) { return; } Report.readNewestAction(report.reportID, false); userActiveSince.current = DateUtils.getDBTime(); setCurrentUnreadMarker(null); + cacheUnreadMarkers.delete(report.reportID); calculateUnreadMarker(); }; From e9cdc56ea85f994f2a8765b0c79535b1e4583f92 Mon Sep 17 00:00:00 2001 From: Filip Solecki Date: Mon, 15 Jan 2024 13:34:24 +0100 Subject: [PATCH 181/391] Migrate Remaining Group 3 to TS --- ...ocusManager.js => ComposerFocusManager.ts} | 2 +- ...{SuggestionUtils.js => SuggestionUtils.ts} | 23 ++++++++----------- 2 files changed, 11 insertions(+), 14 deletions(-) rename src/libs/{ComposerFocusManager.js => ComposerFocusManager.ts} (86%) rename src/libs/{SuggestionUtils.js => SuggestionUtils.ts} (72%) diff --git a/src/libs/ComposerFocusManager.js b/src/libs/ComposerFocusManager.ts similarity index 86% rename from src/libs/ComposerFocusManager.js rename to src/libs/ComposerFocusManager.ts index 569e165da962..4b7037e6e2c5 100644 --- a/src/libs/ComposerFocusManager.js +++ b/src/libs/ComposerFocusManager.ts @@ -1,5 +1,5 @@ let isReadyToFocusPromise = Promise.resolve(); -let resolveIsReadyToFocus; +let resolveIsReadyToFocus: (value: void | PromiseLike) => void; function resetReadyToFocus() { isReadyToFocusPromise = new Promise((resolve) => { diff --git a/src/libs/SuggestionUtils.js b/src/libs/SuggestionUtils.ts similarity index 72% rename from src/libs/SuggestionUtils.js rename to src/libs/SuggestionUtils.ts index 45641ebb5a0f..261dcbf39edc 100644 --- a/src/libs/SuggestionUtils.js +++ b/src/libs/SuggestionUtils.ts @@ -2,11 +2,10 @@ import CONST from '@src/CONST'; /** * Return the max available index for arrow manager. - * @param {Number} numRows - * @param {Boolean} isAutoSuggestionPickerLarge - * @returns {Number} + * @param numRows + * @param isAutoSuggestionPickerLarge */ -function getMaxArrowIndex(numRows, isAutoSuggestionPickerLarge) { +function getMaxArrowIndex(numRows: number, isAutoSuggestionPickerLarge: boolean): number { // rowCount is number of emoji/mention suggestions. For small screen we can fit 3 items // and for large we show up to 20 items for mentions/emojis const rowCount = isAutoSuggestionPickerLarge @@ -19,21 +18,19 @@ function getMaxArrowIndex(numRows, isAutoSuggestionPickerLarge) { /** * Trims first character of the string if it is a space - * @param {String} str - * @returns {String} + * @param str */ -function trimLeadingSpace(str) { - return str.slice(0, 1) === ' ' ? str.slice(1) : str; +function trimLeadingSpace(str: string): string { + return str.startsWith(' ') ? str.slice(1) : str; } /** * Checks if space is available to render large suggestion menu - * @param {Number} listHeight - * @param {Number} composerHeight - * @param {Number} totalSuggestions - * @returns {Boolean} + * @param listHeight + * @param composerHeight + * @param totalSuggestions */ -function hasEnoughSpaceForLargeSuggestionMenu(listHeight, composerHeight, totalSuggestions) { +function hasEnoughSpaceForLargeSuggestionMenu(listHeight: number, composerHeight: number, totalSuggestions: number): boolean { const maxSuggestions = CONST.AUTO_COMPLETE_SUGGESTER.MAX_AMOUNT_OF_VISIBLE_SUGGESTIONS_IN_CONTAINER; const chatFooterHeight = CONST.CHAT_FOOTER_SECONDARY_ROW_HEIGHT + 2 * CONST.CHAT_FOOTER_SECONDARY_ROW_PADDING; const availableHeight = listHeight - composerHeight - chatFooterHeight; From 7880016e9f2b1ee53752f2f7879bf2ffd8bea2bc Mon Sep 17 00:00:00 2001 From: Filip Solecki Date: Mon, 15 Jan 2024 13:45:38 +0100 Subject: [PATCH 182/391] Add empty lines --- src/libs/ComposerFocusManager.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/libs/ComposerFocusManager.ts b/src/libs/ComposerFocusManager.ts index 4b7037e6e2c5..0dfb0d97dac3 100644 --- a/src/libs/ComposerFocusManager.ts +++ b/src/libs/ComposerFocusManager.ts @@ -6,12 +6,14 @@ function resetReadyToFocus() { resolveIsReadyToFocus = resolve; }); } + function setReadyToFocus() { if (!resolveIsReadyToFocus) { return; } resolveIsReadyToFocus(); } + function isReadyToFocus() { return isReadyToFocusPromise; } From 10fba1290fe181304f8528bfc18ab9345351747a Mon Sep 17 00:00:00 2001 From: Filip Solecki Date: Mon, 15 Jan 2024 13:49:57 +0100 Subject: [PATCH 183/391] Add return type for isReadyToFocus --- src/libs/ComposerFocusManager.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/ComposerFocusManager.ts b/src/libs/ComposerFocusManager.ts index 0dfb0d97dac3..b66bbe92599e 100644 --- a/src/libs/ComposerFocusManager.ts +++ b/src/libs/ComposerFocusManager.ts @@ -14,7 +14,7 @@ function setReadyToFocus() { resolveIsReadyToFocus(); } -function isReadyToFocus() { +function isReadyToFocus(): Promise { return isReadyToFocusPromise; } From 7f2ac3fac47e9181699bd7915556122c73375112 Mon Sep 17 00:00:00 2001 From: Filip Solecki Date: Mon, 15 Jan 2024 13:53:58 +0100 Subject: [PATCH 184/391] Remove params with no descriptions --- src/libs/SuggestionUtils.ts | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/src/libs/SuggestionUtils.ts b/src/libs/SuggestionUtils.ts index 261dcbf39edc..213a2a9e49f1 100644 --- a/src/libs/SuggestionUtils.ts +++ b/src/libs/SuggestionUtils.ts @@ -1,10 +1,6 @@ import CONST from '@src/CONST'; -/** - * Return the max available index for arrow manager. - * @param numRows - * @param isAutoSuggestionPickerLarge - */ +/** Return the max available index for arrow manager. */ function getMaxArrowIndex(numRows: number, isAutoSuggestionPickerLarge: boolean): number { // rowCount is number of emoji/mention suggestions. For small screen we can fit 3 items // and for large we show up to 20 items for mentions/emojis @@ -16,20 +12,12 @@ function getMaxArrowIndex(numRows: number, isAutoSuggestionPickerLarge: boolean) return rowCount - 1; } -/** - * Trims first character of the string if it is a space - * @param str - */ +/** Trims first character of the string if it is a space */ function trimLeadingSpace(str: string): string { return str.startsWith(' ') ? str.slice(1) : str; } -/** - * Checks if space is available to render large suggestion menu - * @param listHeight - * @param composerHeight - * @param totalSuggestions - */ +/** Checks if space is available to render large suggestion menu */ function hasEnoughSpaceForLargeSuggestionMenu(listHeight: number, composerHeight: number, totalSuggestions: number): boolean { const maxSuggestions = CONST.AUTO_COMPLETE_SUGGESTER.MAX_AMOUNT_OF_VISIBLE_SUGGESTIONS_IN_CONTAINER; const chatFooterHeight = CONST.CHAT_FOOTER_SECONDARY_ROW_HEIGHT + 2 * CONST.CHAT_FOOTER_SECONDARY_ROW_PADDING; From ea5c3a2b6c180a94601b4d2408614db0e44dd5d4 Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Mon, 15 Jan 2024 23:03:49 +0700 Subject: [PATCH 185/391] fallback string instead of number --- src/components/ReportActionItem/MoneyRequestAction.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ReportActionItem/MoneyRequestAction.js b/src/components/ReportActionItem/MoneyRequestAction.js index d242dbe23e86..241b3e32447a 100644 --- a/src/components/ReportActionItem/MoneyRequestAction.js +++ b/src/components/ReportActionItem/MoneyRequestAction.js @@ -116,7 +116,7 @@ function MoneyRequestAction({ const isDeletedParentAction = ReportActionsUtils.isDeletedParentAction(action); const isReversedTransaction = ReportActionsUtils.isReversedTransaction(action); if (!_.isEmpty(iouReport) && !_.isEmpty(reportActions) && chatReport.iouReportID && action.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD && network.isOffline) { - shouldShowPendingConversionMessage = IOUUtils.isTransactionPendingCurrencyConversion(iouReport, (action && action.originalMessage && action.originalMessage.IOUTransactionID) || 0); + shouldShowPendingConversionMessage = IOUUtils.isTransactionPendingCurrencyConversion(iouReport, (action && action.originalMessage && action.originalMessage.IOUTransactionID) || '0'); } return isDeletedParentAction || isReversedTransaction ? ( From 49c5c6be5f08216249f294f58a0084025cb8be8c Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Mon, 15 Jan 2024 17:25:38 +0100 Subject: [PATCH 186/391] fix: types --- src/libs/OptionsListUtils.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 9e260c28c2da..379a660ac9be 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -1,6 +1,5 @@ /* eslint-disable no-continue */ import Str from 'expensify-common/lib/str'; -import lodashExtend from 'lodash/extend'; // eslint-disable-next-line you-dont-need-lodash-underscore/get import lodashGet from 'lodash/get'; import lodashOrderBy from 'lodash/orderBy'; @@ -450,10 +449,10 @@ function getSearchText( /** * Get an object of error messages keyed by microtime by combining all error objects related to the report. */ -function getAllReportErrors(report: OnyxEntry, reportActions: OnyxEntry): OnyxCommon.Errors | OnyxCommon.ErrorFields { +function getAllReportErrors(report: OnyxEntry, reportActions: OnyxEntry): OnyxCommon.Errors { const reportErrors = report?.errors ?? {}; const reportErrorFields = report?.errorFields ?? {}; - const reportActionErrors: OnyxCommon.Errors = Object.values(reportActions ?? {}).reduce( + const reportActionErrors: OnyxCommon.ErrorFields = Object.values(reportActions ?? {}).reduce( (prevReportActionErrors, action) => (!action || isEmptyObject(action.errors) ? prevReportActionErrors : {...prevReportActionErrors, ...action.errors}), {}, ); @@ -477,10 +476,11 @@ function getAllReportErrors(report: OnyxEntry, reportActions: OnyxEntry< const errorSources = { reportErrors, ...reportErrorFields, - reportActionErrors, + ...reportActionErrors, }; // Combine all error messages keyed by microtime into one object - const allReportErrors = Object.values(errorSources)?.reduce((prevReportErrors, errors) => (isEmptyObject(errors) ? prevReportErrors : lodashExtend(prevReportErrors, errors)), {}); + const allReportErrors = Object.values(errorSources)?.reduce((prevReportErrors, errors) => (isEmptyObject(errors) ? prevReportErrors : {...prevReportErrors, ...errors}), {}); + return allReportErrors; } From 078b4ce44115245ac26aa2157219dd520714a877 Mon Sep 17 00:00:00 2001 From: brunovjk Date: Mon, 15 Jan 2024 14:41:47 -0300 Subject: [PATCH 187/391] Revert "Fill MoneyRequestReferralProgramCTA icon" This reverts commit 6d15a87cc10a4f59e1b474272e563aa5968ba8c8. --- src/pages/iou/MoneyRequestReferralProgramCTA.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pages/iou/MoneyRequestReferralProgramCTA.tsx b/src/pages/iou/MoneyRequestReferralProgramCTA.tsx index 30db04dffdac..31394e1bd0e1 100644 --- a/src/pages/iou/MoneyRequestReferralProgramCTA.tsx +++ b/src/pages/iou/MoneyRequestReferralProgramCTA.tsx @@ -41,7 +41,6 @@ function MoneyRequestReferralProgramCTA({referralContentType}: MoneyRequestRefer src={Info} height={20} width={20} - fill={theme.icon} /> ); From 87f5cf5db7ac4ccc8ce6cd70a35563512bb2fb28 Mon Sep 17 00:00:00 2001 From: brunovjk Date: Mon, 15 Jan 2024 15:03:35 -0300 Subject: [PATCH 188/391] Use debounce text input value --- ...ryForRefactorRequestParticipantsSelector.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js index 9fb91e34fb33..02e0eb730c43 100644 --- a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js +++ b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js @@ -1,6 +1,6 @@ import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; -import React, {useCallback, useMemo, useState} from 'react'; +import React, {useCallback, useEffect,useMemo} from 'react'; import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; @@ -10,6 +10,7 @@ import {usePersonalDetails} from '@components/OnyxProvider'; import {PressableWithFeedback} from '@components/Pressable'; import SelectCircle from '@components/SelectCircle'; import SelectionList from '@components/SelectionList'; +import useDebouncedState from '@hooks/useDebouncedState'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -85,7 +86,7 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({ }) { const {translate} = useLocalize(); const styles = useThemeStyles(); - const [searchTerm, setSearchTerm] = useState(''); + const [searchTerm, debouncedSearchTerm, setSearchTerm] = useDebouncedState(''); const {isOffline} = useNetwork(); const personalDetails = usePersonalDetails(); @@ -254,13 +255,12 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({ [maxParticipantsReached, newChatOptions.personalDetails.length, newChatOptions.recentReports.length, newChatOptions.userToInvite, participants, searchTerm], ); - // When search term updates we will fetch any reports - const setSearchTermAndSearchInServer = useCallback((text = '') => { - if (text.length) { - Report.searchInServer(text); + useEffect(() => { + if (!debouncedSearchTerm.length) { + return; } - setSearchTerm(text); - }, []); + Report.searchInServer(debouncedSearchTerm); + }, [debouncedSearchTerm]); // Right now you can't split a request with a workspace and other additional participants // This is getting properly fixed in https://github.com/Expensify/App/issues/27508, but as a stop-gap to prevent @@ -348,7 +348,7 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({ textInputValue={searchTerm} textInputLabel={translate('optionsSelector.nameEmailOrPhoneNumber')} textInputHint={offlineMessage} - onChangeText={setSearchTermAndSearchInServer} + onChangeText={setSearchTerm} shouldPreventDefaultFocusOnSelectRow={!DeviceCapabilities.canUseTouchScreen()} onSelectRow={addSingleParticipant} footerContent={footerContent} From 97f1c9aac058e9f74c10777bf61da77c1d7c3eb2 Mon Sep 17 00:00:00 2001 From: brunovjk Date: Mon, 15 Jan 2024 16:03:55 -0300 Subject: [PATCH 189/391] applying all the same changes in 'pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js' --- .../MoneyRequestParticipantsPage.js | 3 +- .../MoneyRequestParticipantsSelector.js | 43 +++++++++++++------ 2 files changed, 33 insertions(+), 13 deletions(-) diff --git a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsPage.js b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsPage.js index 216154be9cd4..76b7b80c6306 100644 --- a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsPage.js +++ b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsPage.js @@ -130,7 +130,7 @@ function MoneyRequestParticipantsPage({iou, selectedTab, route, transaction}) { shouldEnableMaxHeight={DeviceCapabilities.canUseTouchScreen()} testID={MoneyRequestParticipantsPage.displayName} > - {({safeAreaPaddingBottomStyle}) => ( + {({didScreenTransitionEnd, safeAreaPaddingBottomStyle}) => ( )} diff --git a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js index 9edede770233..708398d7ea00 100755 --- a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js +++ b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js @@ -1,6 +1,6 @@ import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; -import React, {useCallback, useMemo, useState} from 'react'; +import React, {useCallback, useEffect,useMemo} from 'react'; import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; @@ -10,16 +10,19 @@ import {usePersonalDetails} from '@components/OnyxProvider'; import {PressableWithFeedback} from '@components/Pressable'; import SelectCircle from '@components/SelectCircle'; import SelectionList from '@components/SelectionList'; +import useDebouncedState from '@hooks/useDebouncedState'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useThemeStyles from '@hooks/useThemeStyles'; import * as Report from '@libs/actions/Report'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import * as OptionsListUtils from '@libs/OptionsListUtils'; +import * as ReportUtils from '@libs/ReportUtils'; import MoneyRequestReferralProgramCTA from '@pages/iou/MoneyRequestReferralProgramCTA'; import reportPropTypes from '@pages/reportPropTypes'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import {isNotEmptyObject} from '@src/types/utils/EmptyObject'; const propTypes = { /** Beta features list */ @@ -59,6 +62,9 @@ const propTypes = { /** Whether we are searching for reports in the server */ isSearchingForReports: PropTypes.bool, + + /** Whether the parent screen transition has ended */ + didScreenTransitionEnd: PropTypes.bool, }; const defaultProps = { @@ -68,6 +74,7 @@ const defaultProps = { betas: [], isDistanceRequest: false, isSearchingForReports: false, + didScreenTransitionEnd: false, }; function MoneyRequestParticipantsSelector({ @@ -81,16 +88,24 @@ function MoneyRequestParticipantsSelector({ iouType, isDistanceRequest, isSearchingForReports, + didScreenTransitionEnd, }) { const {translate} = useLocalize(); const styles = useThemeStyles(); - const [searchTerm, setSearchTerm] = useState(''); + const [searchTerm, debouncedSearchTerm, setSearchTerm] = useDebouncedState(''); const {isOffline} = useNetwork(); const personalDetails = usePersonalDetails(); const offlineMessage = isOffline ? `${translate('common.youAppearToBeOffline')} ${translate('search.resultsAreLimited')}` : ''; const newChatOptions = useMemo(() => { + if (!didScreenTransitionEnd) { + return { + recentReports: {}, + personalDetails: {}, + userToInvite: {}, + }; + } const chatOptions = OptionsListUtils.getFilteredOptions( reports, personalDetails, @@ -121,7 +136,7 @@ function MoneyRequestParticipantsSelector({ personalDetails: chatOptions.personalDetails, userToInvite: chatOptions.userToInvite, }; - }, [betas, reports, participants, personalDetails, searchTerm, iouType, isDistanceRequest]); + }, [betas, didScreenTransitionEnd, reports, participants, personalDetails, searchTerm, iouType, isDistanceRequest]); const maxParticipantsReached = participants.length === CONST.REPORT.MAXIMUM_PARTICIPANTS; @@ -166,7 +181,7 @@ function MoneyRequestParticipantsSelector({ }); indexOffset += newChatOptions.personalDetails.length; - if (newChatOptions.userToInvite && !OptionsListUtils.isCurrentUser(newChatOptions.userToInvite)) { + if (isNotEmptyObject(newChatOptions.userToInvite) && !OptionsListUtils.isCurrentUser(newChatOptions.userToInvite)) { newSections.push({ title: undefined, data: _.map([newChatOptions.userToInvite], (participant) => { @@ -258,11 +273,12 @@ function MoneyRequestParticipantsSelector({ [maxParticipantsReached, newChatOptions.personalDetails.length, newChatOptions.recentReports.length, newChatOptions.userToInvite, participants, searchTerm], ); - // When search term updates we will fetch any reports - const setSearchTermAndSearchInServer = useCallback((text = '') => { - Report.searchInServer(text); - setSearchTerm(text); - }, []); + useEffect(() => { + if (!debouncedSearchTerm.length) { + return; + } + Report.searchInServer(debouncedSearchTerm); + }, [debouncedSearchTerm]); // Right now you can't split a request with a workspace and other additional participants // This is getting properly fixed in https://github.com/Expensify/App/issues/27508, but as a stop-gap to prevent @@ -341,21 +357,24 @@ function MoneyRequestParticipantsSelector({ [addParticipantToSelection, isAllowedToSplit, styles, translate], ); + const isOptionsDataReady = useMemo(() => ReportUtils.isReportDataReady() && OptionsListUtils.isPersonalDetailsReady(personalDetails), [personalDetails]); + return ( 0 ? safeAreaPaddingBottomStyle : {}]}> ); From 8d08f71beb23eb08f6d65770fe0a83c6a28d2aa6 Mon Sep 17 00:00:00 2001 From: brunovjk Date: Mon, 15 Jan 2024 18:20:32 -0300 Subject: [PATCH 190/391] Resolve conflict over 'ReferralProgramCTA' --- ...yForRefactorRequestParticipantsSelector.js | 43 +++++++++++++----- .../MoneyRequestParticipantsSelector.js | 45 +++++++++++++------ 2 files changed, 63 insertions(+), 25 deletions(-) diff --git a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js index ea9788ccddb5..72f9831c2c12 100644 --- a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js +++ b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js @@ -1,6 +1,6 @@ import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; -import React, {useCallback, useMemo, useState} from 'react'; +import React, {useCallback, useEffect,useMemo} from 'react'; import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; @@ -11,12 +11,14 @@ import {PressableWithFeedback} from '@components/Pressable'; import ReferralProgramCTA from '@components/ReferralProgramCTA'; import SelectCircle from '@components/SelectCircle'; import SelectionList from '@components/SelectionList'; +import useDebouncedState from '@hooks/useDebouncedState'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useThemeStyles from '@hooks/useThemeStyles'; import * as Report from '@libs/actions/Report'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import * as OptionsListUtils from '@libs/OptionsListUtils'; +import * as ReportUtils from '@libs/ReportUtils'; import reportPropTypes from '@pages/reportPropTypes'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -56,6 +58,9 @@ const propTypes = { /** Whether we are searching for reports in the server */ isSearchingForReports: PropTypes.bool, + + /** Whether the parent screen transition has ended */ + didScreenTransitionEnd: PropTypes.bool, }; const defaultProps = { @@ -64,6 +69,7 @@ const defaultProps = { reports: {}, betas: [], isSearchingForReports: false, + didScreenTransitionEnd: false, }; function MoneyTemporaryForRefactorRequestParticipantsSelector({ @@ -76,10 +82,11 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({ iouType, iouRequestType, isSearchingForReports, + didScreenTransitionEnd, }) { const {translate} = useLocalize(); const styles = useThemeStyles(); - const [searchTerm, setSearchTerm] = useState(''); + const [searchTerm, debouncedSearchTerm, setSearchTerm] = useDebouncedState(''); const {isOffline} = useNetwork(); const personalDetails = usePersonalDetails(); @@ -94,6 +101,16 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({ */ const [sections, newChatOptions] = useMemo(() => { const newSections = []; + if (!didScreenTransitionEnd) { + return [ + newSections, + { + recentReports: {}, + personalDetails: {}, + userToInvite: {}, + } + ]; + } let indexOffset = 0; const chatOptions = OptionsListUtils.getFilteredOptions( @@ -168,7 +185,7 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({ } return [newSections, chatOptions]; - }, [reports, personalDetails, betas, searchTerm, participants, iouType, iouRequestType, maxParticipantsReached, translate]); + }, [didScreenTransitionEnd, reports, personalDetails, betas, searchTerm, participants, iouType, iouRequestType, maxParticipantsReached, translate]); /** * Adds a single participant to the request @@ -238,13 +255,12 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({ [maxParticipantsReached, newChatOptions.personalDetails.length, newChatOptions.recentReports.length, newChatOptions.userToInvite, participants, searchTerm], ); - // When search term updates we will fetch any reports - const setSearchTermAndSearchInServer = useCallback((text = '') => { - if (text.length) { - Report.searchInServer(text); + useEffect(() => { + if (!debouncedSearchTerm.length) { + return; } - setSearchTerm(text); - }, []); + Report.searchInServer(debouncedSearchTerm); + }, [debouncedSearchTerm]); // Right now you can't split a request with a workspace and other additional participants // This is getting properly fixed in https://github.com/Expensify/App/issues/27508, but as a stop-gap to prevent @@ -322,21 +338,24 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({ [addParticipantToSelection, isAllowedToSplit, styles, translate], ); + const isOptionsDataReady = useMemo(() => ReportUtils.isReportDataReady() && OptionsListUtils.isPersonalDetailsReady(personalDetails), [personalDetails]); + return ( 0 ? safeAreaPaddingBottomStyle : {}]}> ); diff --git a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js index 2162e81d7bb8..76e97b140506 100755 --- a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js +++ b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js @@ -1,6 +1,6 @@ import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; -import React, {useCallback, useMemo, useState} from 'react'; +import React, {useCallback, useEffect,useMemo} from 'react'; import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; @@ -11,15 +11,18 @@ import {PressableWithFeedback} from '@components/Pressable'; import ReferralProgramCTA from '@components/ReferralProgramCTA'; import SelectCircle from '@components/SelectCircle'; import SelectionList from '@components/SelectionList'; +import useDebouncedState from '@hooks/useDebouncedState'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useThemeStyles from '@hooks/useThemeStyles'; import * as Report from '@libs/actions/Report'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import * as OptionsListUtils from '@libs/OptionsListUtils'; +import * as ReportUtils from '@libs/ReportUtils'; import reportPropTypes from '@pages/reportPropTypes'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import {isNotEmptyObject} from '@src/types/utils/EmptyObject'; const propTypes = { /** Beta features list */ @@ -59,6 +62,9 @@ const propTypes = { /** Whether we are searching for reports in the server */ isSearchingForReports: PropTypes.bool, + + /** Whether the parent screen transition has ended */ + didScreenTransitionEnd: PropTypes.bool, }; const defaultProps = { @@ -68,6 +74,7 @@ const defaultProps = { betas: [], isDistanceRequest: false, isSearchingForReports: false, + didScreenTransitionEnd: false, }; function MoneyRequestParticipantsSelector({ @@ -81,16 +88,24 @@ function MoneyRequestParticipantsSelector({ iouType, isDistanceRequest, isSearchingForReports, + didScreenTransitionEnd, }) { const {translate} = useLocalize(); const styles = useThemeStyles(); - const [searchTerm, setSearchTerm] = useState(''); + const [searchTerm, debouncedSearchTerm, setSearchTerm] = useDebouncedState(''); const {isOffline} = useNetwork(); const personalDetails = usePersonalDetails(); const offlineMessage = isOffline ? `${translate('common.youAppearToBeOffline')} ${translate('search.resultsAreLimited')}` : ''; const newChatOptions = useMemo(() => { + if (!didScreenTransitionEnd) { + return { + recentReports: {}, + personalDetails: {}, + userToInvite: {}, + }; + } const chatOptions = OptionsListUtils.getFilteredOptions( reports, personalDetails, @@ -121,7 +136,7 @@ function MoneyRequestParticipantsSelector({ personalDetails: chatOptions.personalDetails, userToInvite: chatOptions.userToInvite, }; - }, [betas, reports, participants, personalDetails, searchTerm, iouType, isDistanceRequest]); + }, [betas, didScreenTransitionEnd, reports, participants, personalDetails, searchTerm, iouType, isDistanceRequest]); const maxParticipantsReached = participants.length === CONST.REPORT.MAXIMUM_PARTICIPANTS; @@ -166,7 +181,7 @@ function MoneyRequestParticipantsSelector({ }); indexOffset += newChatOptions.personalDetails.length; - if (newChatOptions.userToInvite && !OptionsListUtils.isCurrentUser(newChatOptions.userToInvite)) { + if (isNotEmptyObject(newChatOptions.userToInvite) && !OptionsListUtils.isCurrentUser(newChatOptions.userToInvite)) { newSections.push({ title: undefined, data: _.map([newChatOptions.userToInvite], (participant) => { @@ -258,11 +273,12 @@ function MoneyRequestParticipantsSelector({ [maxParticipantsReached, newChatOptions.personalDetails.length, newChatOptions.recentReports.length, newChatOptions.userToInvite, participants, searchTerm], ); - // When search term updates we will fetch any reports - const setSearchTermAndSearchInServer = useCallback((text = '') => { - Report.searchInServer(text); - setSearchTerm(text); - }, []); + useEffect(() => { + if (!debouncedSearchTerm.length) { + return; + } + Report.searchInServer(debouncedSearchTerm); + }, [debouncedSearchTerm]); // Right now you can't split a request with a workspace and other additional participants // This is getting properly fixed in https://github.com/Expensify/App/issues/27508, but as a stop-gap to prevent @@ -341,21 +357,24 @@ function MoneyRequestParticipantsSelector({ [addParticipantToSelection, isAllowedToSplit, styles, translate], ); + const isOptionsDataReady = useMemo(() => ReportUtils.isReportDataReady() && OptionsListUtils.isPersonalDetailsReady(personalDetails), [personalDetails]); + return ( 0 ? safeAreaPaddingBottomStyle : {}]}> ); @@ -376,4 +395,4 @@ export default withOnyx({ key: ONYXKEYS.IS_SEARCHING_FOR_REPORTS, initWithStoredValues: false, }, -})(MoneyRequestParticipantsSelector); \ No newline at end of file +})(MoneyRequestParticipantsSelector); From f64a93ad047613831d3e94a136df58d15066a25b Mon Sep 17 00:00:00 2001 From: gijoe0295 Date: Tue, 16 Jan 2024 14:08:23 +0700 Subject: [PATCH 191/391] reapply changes --- .../OptionsSelector/BaseOptionsSelector.js | 5 +++- src/components/ReferralProgramCTA.tsx | 28 +++++++++++++------ src/pages/SearchPage/SearchPageFooter.tsx | 8 ++++-- ...yForRefactorRequestParticipantsSelector.js | 12 ++++++-- .../MoneyRequestParticipantsSelector.js | 12 ++++++-- 5 files changed, 48 insertions(+), 17 deletions(-) diff --git a/src/components/OptionsSelector/BaseOptionsSelector.js b/src/components/OptionsSelector/BaseOptionsSelector.js index f7fc8ca4b77d..bbcce6fff9a6 100755 --- a/src/components/OptionsSelector/BaseOptionsSelector.js +++ b/src/components/OptionsSelector/BaseOptionsSelector.js @@ -662,7 +662,10 @@ class BaseOptionsSelector extends Component { {this.props.shouldShowReferralCTA && this.state.shouldShowReferralModal && ( - + )} diff --git a/src/components/ReferralProgramCTA.tsx b/src/components/ReferralProgramCTA.tsx index 473d5cdbed08..68b97c343f81 100644 --- a/src/components/ReferralProgramCTA.tsx +++ b/src/components/ReferralProgramCTA.tsx @@ -6,7 +6,7 @@ import CONST from '@src/CONST'; import Navigation from '@src/libs/Navigation/Navigation'; import ROUTES from '@src/ROUTES'; import Icon from './Icon'; -import {Info} from './Icon/Expensicons'; +import {Close} from './Icon/Expensicons'; import {PressableWithoutFeedback} from './Pressable'; import Text from './Text'; @@ -16,9 +16,12 @@ type ReferralProgramCTAProps = { | typeof CONST.REFERRAL_PROGRAM.CONTENT_TYPES.START_CHAT | typeof CONST.REFERRAL_PROGRAM.CONTENT_TYPES.SEND_MONEY | typeof CONST.REFERRAL_PROGRAM.CONTENT_TYPES.REFER_FRIEND; + + /** Method to trigger when pressing close button of the banner */ + onCloseButtonPress?: () => void; }; -function ReferralProgramCTA({referralContentType}: ReferralProgramCTAProps) { +function ReferralProgramCTA({referralContentType, onCloseButtonPress = () => {}}: ReferralProgramCTAProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); const theme = useTheme(); @@ -41,12 +44,21 @@ function ReferralProgramCTA({referralContentType}: ReferralProgramCTAProps) { {translate(`referralProgram.${referralContentType}.buttonText2`)} - + { + e.preventDefault(); + }} + role={CONST.ACCESSIBILITY_ROLE.BUTTON} + accessibilityLabel={translate('common.close')} + > + + ); } diff --git a/src/pages/SearchPage/SearchPageFooter.tsx b/src/pages/SearchPage/SearchPageFooter.tsx index e0ef67ad9ec3..a66e24d973d9 100644 --- a/src/pages/SearchPage/SearchPageFooter.tsx +++ b/src/pages/SearchPage/SearchPageFooter.tsx @@ -1,15 +1,19 @@ -import React from 'react'; +import React, {useState} from 'react'; import {View} from 'react-native'; import ReferralProgramCTA from '@components/ReferralProgramCTA'; import useThemeStyles from '@hooks/useThemeStyles'; import CONST from '@src/CONST'; function SearchPageFooter() { + const [shouldShowReferralCTA, setShouldShowReferralCTA] = useState(true); const themeStyles = useThemeStyles(); return ( - + setShouldShowReferralCTA(false)} + /> ); } diff --git a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js index f8c412993bab..bd5af413635d 100644 --- a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js +++ b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js @@ -80,6 +80,7 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({ const {translate} = useLocalize(); const styles = useThemeStyles(); const [searchTerm, setSearchTerm] = useState(''); + const [shouldShowReferralCTA, setShouldShowReferralCTA] = useState(true); const {isOffline} = useNetwork(); const personalDetails = usePersonalDetails(); @@ -265,9 +266,14 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({ const footerContent = useMemo( () => ( - - - + {shouldShowReferralCTA && ( + + setShouldShowReferralCTA(false)} + /> + + )} {shouldShowSplitBillErrorMessage && ( ( - - - + {shouldShowReferralCTA && ( + + setShouldShowReferralCTA(false)} + /> + + )} {shouldShowSplitBillErrorMessage && ( Date: Tue, 16 Jan 2024 14:32:38 +0700 Subject: [PATCH 192/391] increase pressable space --- src/components/ReferralProgramCTA.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/ReferralProgramCTA.tsx b/src/components/ReferralProgramCTA.tsx index 68b97c343f81..4a6b8b03f2b4 100644 --- a/src/components/ReferralProgramCTA.tsx +++ b/src/components/ReferralProgramCTA.tsx @@ -31,7 +31,7 @@ function ReferralProgramCTA({referralContentType, onCloseButtonPress = () => {}} onPress={() => { Navigation.navigate(ROUTES.REFERRAL_DETAILS_MODAL.getRoute(referralContentType)); }} - style={[styles.p5, styles.w100, styles.br2, styles.highlightBG, styles.flexRow, styles.justifyContentBetween, styles.alignItemsCenter, {gap: 10}]} + style={[styles.w100, styles.br2, styles.highlightBG, styles.flexRow, styles.justifyContentBetween, styles.alignItemsCenter, {gap: 10, padding: 10}, styles.pl5]} accessibilityLabel="referral" role={CONST.ACCESSIBILITY_ROLE.BUTTON} > @@ -49,6 +49,7 @@ function ReferralProgramCTA({referralContentType, onCloseButtonPress = () => {}} onMouseDown={(e) => { e.preventDefault(); }} + style={[styles.touchableButtonImage]} role={CONST.ACCESSIBILITY_ROLE.BUTTON} accessibilityLabel={translate('common.close')} > From bdc980df6055cd93f1925c6fd7581f96dad51776 Mon Sep 17 00:00:00 2001 From: gijoe0295 Date: Tue, 16 Jan 2024 14:43:38 +0700 Subject: [PATCH 193/391] fix lint --- src/pages/SearchPage/SearchPageFooter.tsx | 16 ++++++++++------ ...raryForRefactorRequestParticipantsSelector.js | 2 +- .../MoneyRequestParticipantsSelector.js | 2 +- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/pages/SearchPage/SearchPageFooter.tsx b/src/pages/SearchPage/SearchPageFooter.tsx index a66e24d973d9..fb3644d8e570 100644 --- a/src/pages/SearchPage/SearchPageFooter.tsx +++ b/src/pages/SearchPage/SearchPageFooter.tsx @@ -9,12 +9,16 @@ function SearchPageFooter() { const themeStyles = useThemeStyles(); return ( - - setShouldShowReferralCTA(false)} - /> - + <> + {shouldShowReferralCTA && ( + + setShouldShowReferralCTA(false)} + /> + + )} + ); } diff --git a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js index bd5af413635d..fa7f13002305 100644 --- a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js +++ b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js @@ -294,7 +294,7 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({ )} ), - [handleConfirmSelection, participants.length, referralContentType, shouldShowSplitBillErrorMessage, styles, translate], + [handleConfirmSelection, participants.length, referralContentType, shouldShowSplitBillErrorMessage, shouldShowReferralCTA, styles, translate], ); const itemRightSideComponent = useCallback( diff --git a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js index f5e332f8eace..59081599736c 100755 --- a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js +++ b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js @@ -313,7 +313,7 @@ function MoneyRequestParticipantsSelector({ )} ), - [handleConfirmSelection, participants.length, referralContentType, shouldShowSplitBillErrorMessage, styles, translate], + [handleConfirmSelection, participants.length, referralContentType, shouldShowSplitBillErrorMessage, shouldShowReferralCTA, styles, translate], ); const itemRightSideComponent = useCallback( From d9dc65336727b23811ea5906bacae2c40a726bab Mon Sep 17 00:00:00 2001 From: Pujan Date: Tue, 16 Jan 2024 18:04:56 +0530 Subject: [PATCH 194/391] private notes edit page ts changes --- ...esEditPage.js => PrivateNotesEditPage.tsx} | 80 ++++++++----------- src/types/onyx/Report.ts | 2 +- 2 files changed, 35 insertions(+), 47 deletions(-) rename src/pages/PrivateNotes/{PrivateNotesEditPage.js => PrivateNotesEditPage.tsx} (72%) diff --git a/src/pages/PrivateNotes/PrivateNotesEditPage.js b/src/pages/PrivateNotes/PrivateNotesEditPage.tsx similarity index 72% rename from src/pages/PrivateNotes/PrivateNotesEditPage.js rename to src/pages/PrivateNotes/PrivateNotesEditPage.tsx index 0d4bc2c3e7e1..b6b178049024 100644 --- a/src/pages/PrivateNotes/PrivateNotesEditPage.js +++ b/src/pages/PrivateNotes/PrivateNotesEditPage.tsx @@ -1,12 +1,12 @@ import {useFocusEffect} from '@react-navigation/native'; +import type {RouteProp} from '@react-navigation/native'; import ExpensiMark from 'expensify-common/lib/ExpensiMark'; import Str from 'expensify-common/lib/str'; -import lodashGet from 'lodash/get'; -import PropTypes from 'prop-types'; +import type {OnyxCollection} from 'react-native-onyx'; +import lodashDebounce from 'lodash/debounce'; import React, {useCallback, useMemo, useRef, useState} from 'react'; import {Keyboard} from 'react-native'; import {withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; import FormProvider from '@components/Form/FormProvider'; import InputWrapper from '@components/Form/InputWrapper'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; @@ -14,50 +14,42 @@ import OfflineWithFeedback from '@components/OfflineWithFeedback'; import ScreenWrapper from '@components/ScreenWrapper'; import Text from '@components/Text'; import TextInput from '@components/TextInput'; -import withLocalize from '@components/withLocalize'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; -import compose from '@libs/compose'; import Navigation from '@libs/Navigation/Navigation'; import * as ReportUtils from '@libs/ReportUtils'; import updateMultilineInputRange from '@libs/updateMultilineInputRange'; import withReportAndPrivateNotesOrNotFound from '@pages/home/report/withReportAndPrivateNotesOrNotFound'; -import personalDetailsPropType from '@pages/personalDetailsPropType'; -import reportPropTypes from '@pages/reportPropTypes'; -import * as Report from '@userActions/Report'; +import * as ReportActions from '@userActions/Report'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import type { PersonalDetails, Report } from '@src/types/onyx'; +import type { Note } from '@src/types/onyx/Report'; + +type PrivateNotesEditPageOnyxProps = { + /* Onyx Props */ -const propTypes = { /** All of the personal details for everyone */ - personalDetailsList: PropTypes.objectOf(personalDetailsPropType), + personalDetailsList: OnyxCollection, +} + +type PrivateNotesEditPageProps = PrivateNotesEditPageOnyxProps & { /** The report currently being looked at */ - report: reportPropTypes, - route: PropTypes.shape({ - /** Params from the URL path */ - params: PropTypes.shape({ - /** reportID and accountID passed via route: /r/:reportID/notes */ - reportID: PropTypes.string, - accountID: PropTypes.string, - }), - }).isRequired, -}; - -const defaultProps = { - report: {}, - personalDetailsList: {}, -}; - -function PrivateNotesEditPage({route, personalDetailsList, report}) { + report: Report, + + route: RouteProp<{params: {reportID: string; accountID: string}}>; +} + +function PrivateNotesEditPage({route, personalDetailsList, report}: PrivateNotesEditPageProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); // We need to edit the note in markdown format, but display it in HTML format const parser = new ExpensiMark(); const [privateNote, setPrivateNote] = useState( - () => Report.getDraftPrivateNote(report.reportID).trim() || parser.htmlToMarkdown(lodashGet(report, ['privateNotes', route.params.accountID, 'note'], '')).trim(), + () => ReportActions.getDraftPrivateNote(report.reportID).trim() || parser.htmlToMarkdown(report?.privateNotes?.[Number(route.params.accountID)]?.note ?? '').trim(), ); /** @@ -67,8 +59,8 @@ function PrivateNotesEditPage({route, personalDetailsList, report}) { */ const debouncedSavePrivateNote = useMemo( () => - _.debounce((text) => { - Report.savePrivateNotesDraft(report.reportID, text); + lodashDebounce((text: string) => { + ReportActions.savePrivateNotesDraft(report.reportID, text); }, 1000), [report.reportID], ); @@ -94,18 +86,18 @@ function PrivateNotesEditPage({route, personalDetailsList, report}) { ); const savePrivateNote = () => { - const originalNote = lodashGet(report, ['privateNotes', route.params.accountID, 'note'], ''); + const originalNote = report?.privateNotes?.[Number(route.params.accountID)]?.note ?? ''; let editedNote = ''; if (privateNote.trim() !== originalNote.trim()) { - editedNote = Report.handleUserDeletedLinksInHtml(privateNote.trim(), parser.htmlToMarkdown(originalNote).trim()); - Report.updatePrivateNotes(report.reportID, route.params.accountID, editedNote); + editedNote = ReportActions.handleUserDeletedLinksInHtml(privateNote.trim(), parser.htmlToMarkdown(originalNote).trim()); + ReportActions.updatePrivateNotes(report.reportID, Number(route.params.accountID), editedNote); } // We want to delete saved private note draft after saving the note debouncedSavePrivateNote(''); Keyboard.dismiss(); - if (!_.some({...report.privateNotes, [route.params.accountID]: {note: editedNote}}, (item) => item.note)) { + if(({...report.privateNotes, [route.params.accountID]: {note: editedNote}} as Note).note) { ReportUtils.navigateToDetailsPage(report); } else { Navigation.goBack(ROUTES.PRIVATE_NOTES_LIST.getRoute(report.reportID)); @@ -133,16 +125,16 @@ function PrivateNotesEditPage({route, personalDetailsList, report}) { > {translate( - Str.extractEmailDomain(lodashGet(personalDetailsList, [route.params.accountID, 'login'], '')) === CONST.EMAIL.GUIDES_DOMAIN + Str.extractEmailDomain(personalDetailsList?.[route.params.accountID]?.login ?? '') === CONST.EMAIL.GUIDES_DOMAIN ? 'privateNotes.sharedNoteMessage' : 'privateNotes.personalNoteMessage', )} Report.clearPrivateNotesError(report.reportID, route.params.accountID)} + onClose={() => ReportActions.clearPrivateNotesError(report.reportID, Number(route.params.accountID))} style={[styles.mb3]} > { + onChangeText={(text: string) => { debouncedSavePrivateNote(text); setPrivateNote(text); }} @@ -177,15 +169,11 @@ function PrivateNotesEditPage({route, personalDetailsList, report}) { } PrivateNotesEditPage.displayName = 'PrivateNotesEditPage'; -PrivateNotesEditPage.propTypes = propTypes; -PrivateNotesEditPage.defaultProps = defaultProps; -export default compose( - withLocalize, - withReportAndPrivateNotesOrNotFound('privateNotes.title'), - withOnyx({ +export default withReportAndPrivateNotesOrNotFound('privateNotes.title')( + withOnyx({ personalDetailsList: { key: ONYXKEYS.PERSONAL_DETAILS_LIST, }, - }), -)(PrivateNotesEditPage); + })(PrivateNotesEditPage) +); \ No newline at end of file diff --git a/src/types/onyx/Report.ts b/src/types/onyx/Report.ts index 7cc3c508d926..22a60712597b 100644 --- a/src/types/onyx/Report.ts +++ b/src/types/onyx/Report.ts @@ -161,4 +161,4 @@ type Report = { export default Report; -export type {NotificationPreference, WriteCapability}; +export type {NotificationPreference, WriteCapability, Note}; From 1c13f5b86e49879b10a87b8de4c949c3de14fb18 Mon Sep 17 00:00:00 2001 From: Pujan Date: Tue, 16 Jan 2024 18:13:08 +0530 Subject: [PATCH 195/391] corrected the condition --- src/pages/PrivateNotes/PrivateNotesEditPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/PrivateNotes/PrivateNotesEditPage.tsx b/src/pages/PrivateNotes/PrivateNotesEditPage.tsx index b6b178049024..8dff3ffd54d6 100644 --- a/src/pages/PrivateNotes/PrivateNotesEditPage.tsx +++ b/src/pages/PrivateNotes/PrivateNotesEditPage.tsx @@ -97,7 +97,7 @@ function PrivateNotesEditPage({route, personalDetailsList, report}: PrivateNotes debouncedSavePrivateNote(''); Keyboard.dismiss(); - if(({...report.privateNotes, [route.params.accountID]: {note: editedNote}} as Note).note) { + if(!({...report.privateNotes, [route.params.accountID]: {note: editedNote}} as Note).note) { ReportUtils.navigateToDetailsPage(report); } else { Navigation.goBack(ROUTES.PRIVATE_NOTES_LIST.getRoute(report.reportID)); From 2babc2d778cd6c0d9213e72270cfe346855c958f Mon Sep 17 00:00:00 2001 From: Cong Pham Date: Thu, 11 Jan 2024 00:49:41 +0700 Subject: [PATCH 196/391] 34265 update emoji offset --- src/CONST.ts | 1 + src/components/EmojiPicker/EmojiPickerButton.js | 17 ++++++++++++++++- src/libs/calculateAnchorPosition.ts | 2 +- .../ReportActionCompose/ReportActionCompose.js | 8 ++++++++ src/styles/index.ts | 2 +- 5 files changed, 27 insertions(+), 3 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index fc4a3729bd01..881e520e0b12 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -965,6 +965,7 @@ const CONST = { SMALL_EMOJI_PICKER_SIZE: { WIDTH: '100%', }, + MENU_POSITION_REPORT_ACTION_COMPOSE_BOTTOM: 83, NON_NATIVE_EMOJI_PICKER_LIST_HEIGHT: 300, NON_NATIVE_EMOJI_PICKER_LIST_HEIGHT_WEB: 200, EMOJI_PICKER_ITEM_HEIGHT: 32, diff --git a/src/components/EmojiPicker/EmojiPickerButton.js b/src/components/EmojiPicker/EmojiPickerButton.js index e627119270dd..b056ccb22875 100644 --- a/src/components/EmojiPicker/EmojiPickerButton.js +++ b/src/components/EmojiPicker/EmojiPickerButton.js @@ -22,6 +22,9 @@ const propTypes = { /** Unique id for emoji picker */ emojiPickerID: PropTypes.string, + /** Emoji popup anchor offset shift vertical */ + shiftVertical: PropTypes.number, + ...withLocalizePropTypes, }; @@ -29,6 +32,7 @@ const defaultProps = { isDisabled: false, id: '', emojiPickerID: '', + shiftVertical: 0, }; function EmojiPickerButton(props) { @@ -49,7 +53,18 @@ function EmojiPickerButton(props) { return; } if (!EmojiPickerAction.emojiPickerRef.current.isEmojiPickerVisible) { - EmojiPickerAction.showEmojiPicker(props.onModalHide, props.onEmojiSelected, emojiPopoverAnchor, undefined, () => {}, props.emojiPickerID); + EmojiPickerAction.showEmojiPicker( + props.onModalHide, + props.onEmojiSelected, + emojiPopoverAnchor, + { + horizontal: 'right', + vertical: 'bottom', + shiftVertical: props.shiftVertical, + }, + () => {}, + props.emojiPickerID, + ); } else { EmojiPickerAction.emojiPickerRef.current.hideEmojiPicker(); } diff --git a/src/libs/calculateAnchorPosition.ts b/src/libs/calculateAnchorPosition.ts index 66966b7b504c..3b6617aa3ed0 100644 --- a/src/libs/calculateAnchorPosition.ts +++ b/src/libs/calculateAnchorPosition.ts @@ -22,7 +22,7 @@ export default function calculateAnchorPosition(anchorComponent: View, anchorOri if (anchorOrigin?.vertical === CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.TOP && anchorOrigin?.horizontal === CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.LEFT) { return resolve({horizontal: x, vertical: y + height + (anchorOrigin?.shiftVertical ?? 0)}); } - return resolve({horizontal: x + width, vertical: y}); + return resolve({horizontal: x + width, vertical: y + (anchorOrigin?.shiftVertical ?? 0)}); }); }); } diff --git a/src/pages/home/report/ReportActionCompose/ReportActionCompose.js b/src/pages/home/report/ReportActionCompose/ReportActionCompose.js index c072666920ae..c52b8ec6760a 100644 --- a/src/pages/home/report/ReportActionCompose/ReportActionCompose.js +++ b/src/pages/home/report/ReportActionCompose/ReportActionCompose.js @@ -354,6 +354,13 @@ function ReportActionCompose({ runOnJS(submitForm)(); }, [isSendDisabled, resetFullComposerSize, submitForm, animatedRef, isReportReadyForDisplay]); + const emojiShiftVertical = useMemo(() => { + const chatItemComposeSecondaryRowHeight = styles.chatItemComposeSecondaryRow.height + styles.chatItemComposeSecondaryRow.marginTop + styles.chatItemComposeSecondaryRow.marginBottom; + const reportActionComposeHeight = styles.chatItemComposeBox.minHeight + chatItemComposeSecondaryRowHeight; + const emojiOffsetWithComposeBox = (styles.chatItemComposeBox.minHeight - styles.chatItemEmojiButton.height) / 2; + return reportActionComposeHeight - emojiOffsetWithComposeBox - CONST.MENU_POSITION_REPORT_ACTION_COMPOSE_BOTTOM; + }, [styles]); + return ( @@ -453,6 +460,7 @@ function ReportActionCompose({ onModalHide={focus} onEmojiSelected={(...args) => composerRef.current.replaceSelectionWithText(...args)} emojiPickerID={report.reportID} + shiftVertical={emojiShiftVertical} /> )} createMenuPositionReportActionCompose: (windowHeight: number) => ({ horizontal: 18 + variables.sideBarWidth, - vertical: windowHeight - 83, + vertical: windowHeight - CONST.MENU_POSITION_REPORT_ACTION_COMPOSE_BOTTOM, } satisfies AnchorPosition), createMenuPositionRightSidepane: { From 089c626465bbc3a3280774717bf8b97f803b3679 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Wed, 17 Jan 2024 09:10:07 +0100 Subject: [PATCH 197/391] Use logical or since label can be an empty string --- src/components/StatePicker/index.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/StatePicker/index.tsx b/src/components/StatePicker/index.tsx index 09f3b1a02802..a03e4f15fba0 100644 --- a/src/components/StatePicker/index.tsx +++ b/src/components/StatePicker/index.tsx @@ -62,7 +62,9 @@ function StatePicker({value, onInputChange, label, onBlur, errorText = ''}: Stat ref={ref} shouldShowRightIcon title={title} - description={label ?? translate('common.state')} + // Label can be an empty string + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + description={label || translate('common.state')} descriptionTextStyle={descStyle} onPress={showPickerModal} /> From f8bb83d29eba0e059d54d31bd9cc627fd10be673 Mon Sep 17 00:00:00 2001 From: tienifr Date: Wed, 17 Jan 2024 15:37:25 +0700 Subject: [PATCH 198/391] remove any type --- src/components/ImageView/index.tsx | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/components/ImageView/index.tsx b/src/components/ImageView/index.tsx index 1c10c8116325..e8ee2ba3075a 100644 --- a/src/components/ImageView/index.tsx +++ b/src/components/ImageView/index.tsx @@ -1,4 +1,4 @@ -import React, {useCallback, useEffect, useRef, useState} from 'react'; +import React, {MouseEvent as ReactMouseEvent, useCallback, useEffect, useRef, useState} from 'react'; import type {GestureResponderEvent, LayoutChangeEvent, NativeSyntheticEvent} from 'react-native'; import {View} from 'react-native'; import FullscreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; @@ -112,8 +112,8 @@ function ImageView({isAuthTokenRequired = false, url, fileName, onError}: ImageV return {offsetX, offsetY}; }; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const onContainerPress = (e: any) => { + const onContainerPress = (mouseEvent?: GestureResponderEvent | KeyboardEvent) => { + const e = mouseEvent as unknown as ReactMouseEvent; if (!isZoomed && !isDragging) { if (e.nativeEvent) { const {offsetX, offsetY} = e.nativeEvent; @@ -138,10 +138,9 @@ function ImageView({isAuthTokenRequired = false, url, fileName, onError}: ImageV }; const trackPointerPosition = useCallback( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (e: any) => { + (e: MouseEvent) => { // Whether the pointer is released inside the ImageView - const isInsideImageView = scrollableRef.current?.contains(e.nativeEvent.target); + const isInsideImageView = scrollableRef.current?.contains(e.target as Node); if (!isInsideImageView && isZoomed && isDragging && isMouseDown) { setIsDragging(false); From e0c74d377dc06dc718e6a68e5fea927a54011843 Mon Sep 17 00:00:00 2001 From: tienifr Date: Wed, 17 Jan 2024 15:46:20 +0700 Subject: [PATCH 199/391] clean code --- src/components/ImageView/index.tsx | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/components/ImageView/index.tsx b/src/components/ImageView/index.tsx index e8ee2ba3075a..fb696fb03c64 100644 --- a/src/components/ImageView/index.tsx +++ b/src/components/ImageView/index.tsx @@ -112,11 +112,11 @@ function ImageView({isAuthTokenRequired = false, url, fileName, onError}: ImageV return {offsetX, offsetY}; }; - const onContainerPress = (mouseEvent?: GestureResponderEvent | KeyboardEvent) => { - const e = mouseEvent as unknown as ReactMouseEvent; + const onContainerPress = (e?: GestureResponderEvent | KeyboardEvent) => { + const mouseEvent = e as unknown as ReactMouseEvent; if (!isZoomed && !isDragging) { - if (e.nativeEvent) { - const {offsetX, offsetY} = e.nativeEvent; + if (mouseEvent.nativeEvent) { + const {offsetX, offsetY} = mouseEvent.nativeEvent; // Dividing clicked positions by the zoom scale to get coordinates // so that once we zoom we will scroll to the clicked location. @@ -139,8 +139,9 @@ function ImageView({isAuthTokenRequired = false, url, fileName, onError}: ImageV const trackPointerPosition = useCallback( (e: MouseEvent) => { + const mouseEvent = e as unknown as ReactMouseEvent; // Whether the pointer is released inside the ImageView - const isInsideImageView = scrollableRef.current?.contains(e.target as Node); + const isInsideImageView = scrollableRef.current?.contains(mouseEvent.nativeEvent.target as Node); if (!isInsideImageView && isZoomed && isDragging && isMouseDown) { setIsDragging(false); @@ -152,13 +153,14 @@ function ImageView({isAuthTokenRequired = false, url, fileName, onError}: ImageV const trackMovement = useCallback( (e: MouseEvent) => { + const mouseEvent = e as unknown as ReactMouseEvent; if (!isZoomed) { return; } if (isDragging && isMouseDown && scrollableRef.current) { - const x = e.x; - const y = e.y; + const x = mouseEvent.nativeEvent.x; + const y = mouseEvent.nativeEvent.y; const moveX = initialX - x; const moveY = initialY - y; scrollableRef.current.scrollLeft = initialScrollLeft + moveX; From 3d8c0ebdee04dfc9561050a4e7c79d3e1a20f08a Mon Sep 17 00:00:00 2001 From: Pujan Date: Wed, 17 Jan 2024 16:09:05 +0530 Subject: [PATCH 200/391] some method fix --- src/pages/PrivateNotes/PrivateNotesEditPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/PrivateNotes/PrivateNotesEditPage.tsx b/src/pages/PrivateNotes/PrivateNotesEditPage.tsx index 8dff3ffd54d6..6a3749d9bdc4 100644 --- a/src/pages/PrivateNotes/PrivateNotesEditPage.tsx +++ b/src/pages/PrivateNotes/PrivateNotesEditPage.tsx @@ -97,7 +97,7 @@ function PrivateNotesEditPage({route, personalDetailsList, report}: PrivateNotes debouncedSavePrivateNote(''); Keyboard.dismiss(); - if(!({...report.privateNotes, [route.params.accountID]: {note: editedNote}} as Note).note) { + if(!Object.values({...report.privateNotes, [route.params.accountID]: {note: editedNote}}).some((item) => item.note)) { ReportUtils.navigateToDetailsPage(report); } else { Navigation.goBack(ROUTES.PRIVATE_NOTES_LIST.getRoute(report.reportID)); From 8859d213427b1dae8f28d09ad15dd89cd29db80d Mon Sep 17 00:00:00 2001 From: brunovjk Date: Wed, 17 Jan 2024 10:10:18 -0300 Subject: [PATCH 201/391] Changes requested by reviewer --- src/components/SelectionList/BaseSelectionList.js | 2 +- ...neyTemporaryForRefactorRequestParticipantsSelector.js | 9 +-------- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/src/components/SelectionList/BaseSelectionList.js b/src/components/SelectionList/BaseSelectionList.js index 221436e1020e..2d209ef573c3 100644 --- a/src/components/SelectionList/BaseSelectionList.js +++ b/src/components/SelectionList/BaseSelectionList.js @@ -433,7 +433,7 @@ function BaseSelectionList({ /> )} - {Boolean(headerMessage) && ( + {!isLoadingNewOptions && Boolean(headerMessage) && ( {headerMessage} diff --git a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js index 01ef3d9bb697..2554b5933c6a 100644 --- a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js +++ b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js @@ -102,14 +102,7 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({ const [sections, newChatOptions] = useMemo(() => { const newSections = []; if (!didScreenTransitionEnd) { - return [ - newSections, - { - recentReports: {}, - personalDetails: {}, - userToInvite: {}, - } - ]; + return [newSections, {}]; } let indexOffset = 0; From 1b687411d3a190e2e892cda60d47dd6dccab1835 Mon Sep 17 00:00:00 2001 From: Pujan Date: Wed, 17 Jan 2024 19:02:20 +0530 Subject: [PATCH 202/391] private notes list ts migration changes --- ...esListPage.js => PrivateNotesListPage.tsx} | 94 +++++++------------ 1 file changed, 32 insertions(+), 62 deletions(-) rename src/pages/PrivateNotes/{PrivateNotesListPage.js => PrivateNotesListPage.tsx} (59%) diff --git a/src/pages/PrivateNotes/PrivateNotesListPage.js b/src/pages/PrivateNotes/PrivateNotesListPage.tsx similarity index 59% rename from src/pages/PrivateNotes/PrivateNotesListPage.js rename to src/pages/PrivateNotes/PrivateNotesListPage.tsx index 8e2f8c9f43e0..167a3523854c 100644 --- a/src/pages/PrivateNotes/PrivateNotesListPage.js +++ b/src/pages/PrivateNotes/PrivateNotesListPage.tsx @@ -1,68 +1,45 @@ import {useIsFocused} from '@react-navigation/native'; -import lodashGet from 'lodash/get'; -import PropTypes from 'prop-types'; import React, {useEffect, useMemo} from 'react'; import {ScrollView} from 'react-native'; import {withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; -import {withNetwork} from '@components/OnyxProvider'; import ScreenWrapper from '@components/ScreenWrapper'; import Text from '@components/Text'; -import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; -import compose from '@libs/compose'; import Navigation from '@libs/Navigation/Navigation'; import withReportAndPrivateNotesOrNotFound from '@pages/home/report/withReportAndPrivateNotesOrNotFound'; -import personalDetailsPropType from '@pages/personalDetailsPropType'; -import reportPropTypes from '@pages/reportPropTypes'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import type { PersonalDetails, Report, Session } from '@src/types/onyx'; +import type { OnyxCollection, OnyxEntry } from 'react-native-onyx'; -const propTypes = { - /** The report currently being looked at */ - report: reportPropTypes, - route: PropTypes.shape({ - /** Params from the URL path */ - params: PropTypes.shape({ - /** reportID and accountID passed via route: /r/:reportID/notes */ - reportID: PropTypes.string, - accountID: PropTypes.string, - }), - }).isRequired, - - /** Session info for the currently logged in user. */ - session: PropTypes.shape({ - /** Currently logged in user accountID */ - accountID: PropTypes.number, - }), +type PrivateNotesListPageOnyxProps = { + /* Onyx Props */ /** All of the personal details for everyone */ - personalDetailsList: PropTypes.objectOf(personalDetailsPropType), + personalDetailsList: OnyxCollection, - ...withLocalizePropTypes, -}; + /** Session info for the currently logged in user. */ + session: OnyxEntry; +} -const defaultProps = { - report: {}, - session: { - accountID: null, - }, - personalDetailsList: {}, -}; +type PrivateNotesListPageProps = PrivateNotesListPageOnyxProps & { + /** The report currently being looked at */ + report: Report; +} -function PrivateNotesListPage({report, personalDetailsList, session}) { +function PrivateNotesListPage({report, personalDetailsList, session}: PrivateNotesListPageProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); const isFocused = useIsFocused(); useEffect(() => { const navigateToEditPageTimeout = setTimeout(() => { - if (_.some(report.privateNotes, (item) => item.note) || !isFocused) { + if (Object.values(report.privateNotes ?? {}).some((item) => item.note) || !isFocused) { return; } Navigation.navigate(ROUTES.PRIVATE_NOTES_EDIT.getRoute(report.reportID, session.accountID)); @@ -75,12 +52,8 @@ function PrivateNotesListPage({report, personalDetailsList, session}) { /** * Gets the menu item for each workspace - * - * @param {Object} item - * @param {Number} index - * @returns {JSX} */ - function getMenuItem(item, index) { + function getMenuItem(item, index: number) { const keyTitle = item.translationKey ? translate(item.translationKey) : item.title; return ( { - const privateNoteBrickRoadIndicator = (accountID) => (!_.isEmpty(lodashGet(report, ['privateNotes', accountID, 'errors'], '')) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : ''); - return _.chain(lodashGet(report, 'privateNotes', {})) - .map((privateNote, accountID) => ({ - title: Number(lodashGet(session, 'accountID', null)) === Number(accountID) ? translate('privateNotes.myNote') : lodashGet(personalDetailsList, [accountID, 'login'], ''), - action: () => Navigation.navigate(ROUTES.PRIVATE_NOTES_EDIT.getRoute(report.reportID, accountID)), - brickRoadIndicator: privateNoteBrickRoadIndicator(accountID), - note: lodashGet(privateNote, 'note', ''), - disabled: Number(session.accountID) !== Number(accountID), - })) - .value(); + const privateNoteBrickRoadIndicator = (accountID: number) => report.privateNotes?.[accountID].errors ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : ''; + return Object.keys(report.privateNotes ?? {}) + .map((accountID: string) => { + const privateNote = report.privateNotes?.[Number(accountID)]; + return { + title: Number(session?.accountID) === Number(accountID) ? translate('privateNotes.myNote') : personalDetailsList?.[accountID]?.login ?? '', + action: () => Navigation.navigate(ROUTES.PRIVATE_NOTES_EDIT.getRoute(report.reportID, accountID)), + brickRoadIndicator: privateNoteBrickRoadIndicator(Number(accountID)), + note: privateNote?.note ?? '', + disabled: Number(session?.accountID) !== Number(accountID), + } + }) }, [report, personalDetailsList, session, translate]); return ( @@ -133,25 +108,20 @@ function PrivateNotesListPage({report, personalDetailsList, session}) { onCloseButtonPress={() => Navigation.dismissModal()} /> {translate('privateNotes.personalNoteMessage')} - {_.map(privateNotes, (item, index) => getMenuItem(item, index))} + {privateNotes.map((item, index) => getMenuItem(item, index))} ); } -PrivateNotesListPage.propTypes = propTypes; -PrivateNotesListPage.defaultProps = defaultProps; PrivateNotesListPage.displayName = 'PrivateNotesListPage'; -export default compose( - withLocalize, - withReportAndPrivateNotesOrNotFound('privateNotes.title'), - withOnyx({ +export default withReportAndPrivateNotesOrNotFound('privateNotes.title')( + withOnyx({ personalDetailsList: { key: ONYXKEYS.PERSONAL_DETAILS_LIST, }, session: { key: ONYXKEYS.SESSION, }, - }), - withNetwork(), -)(PrivateNotesListPage); + })(PrivateNotesListPage) +); From e5db70922171b0163282659ecd1260fb1032e35b Mon Sep 17 00:00:00 2001 From: Pujan Date: Wed, 17 Jan 2024 19:31:00 +0530 Subject: [PATCH 203/391] corrected back route --- src/pages/PrivateNotes/PrivateNotesEditPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/PrivateNotes/PrivateNotesEditPage.tsx b/src/pages/PrivateNotes/PrivateNotesEditPage.tsx index 6a3749d9bdc4..db7a1299bb5c 100644 --- a/src/pages/PrivateNotes/PrivateNotesEditPage.tsx +++ b/src/pages/PrivateNotes/PrivateNotesEditPage.tsx @@ -112,7 +112,7 @@ function PrivateNotesEditPage({route, personalDetailsList, report}: PrivateNotes > Navigation.goBack(ROUTES.PRIVATE_NOTES_VIEW.getRoute(report.reportID, route.params.accountID))} + onBackButtonPress={() => Navigation.goBack(ROUTES.PRIVATE_NOTES_LIST.getRoute(report.reportID))} shouldShowBackButton onCloseButtonPress={() => Navigation.dismissModal()} /> From 9298809b24b2d5a0675b0ba08cafd74e089bcc8e Mon Sep 17 00:00:00 2001 From: Kamil Owczarz Date: Wed, 17 Jan 2024 17:00:43 +0100 Subject: [PATCH 204/391] WIP --- src/ONYXKEYS.ts | 17 ++++---- src/components/AmountTextInput.tsx | 3 +- src/components/Composer/index.android.tsx | 3 +- src/components/Composer/index.ios.tsx | 5 ++- src/components/Composer/index.tsx | 6 ++- src/components/Form/FormProvider.tsx | 24 +++++++----- src/components/Form/InputWrapper.tsx | 14 ++++--- src/components/Form/types.ts | 39 +++++++++++-------- src/components/RNTextInput.tsx | 2 +- src/libs/ErrorUtils.ts | 2 +- src/libs/actions/FormActions.ts | 7 ++-- src/libs/actions/Plaid.ts | 2 +- src/libs/actions/Report.ts | 3 +- .../settings/Profile/DisplayNamePage.tsx | 8 ++-- src/types/onyx/Form.ts | 30 +++++++++----- src/types/onyx/ReimbursementAccount.ts | 4 +- src/types/onyx/index.ts | 5 +-- 17 files changed, 101 insertions(+), 73 deletions(-) diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 2915b7a4aa12..e5df472b5997 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -167,9 +167,6 @@ const ONYXKEYS = { /** Stores information about the active reimbursement account being set up */ REIMBURSEMENT_ACCOUNT: 'reimbursementAccount', - /** Stores draft information about the active reimbursement account being set up */ - REIMBURSEMENT_ACCOUNT_DRAFT: 'reimbursementAccountDraft', - /** Store preferred skintone for emoji */ PREFERRED_EMOJI_SKIN_TONE: 'preferredEmojiSkinTone', @@ -350,13 +347,15 @@ const ONYXKEYS = { REPORT_VIRTUAL_CARD_FRAUD_DRAFT: 'reportVirtualCardFraudFormDraft', GET_PHYSICAL_CARD_FORM: 'getPhysicalCardForm', GET_PHYSICAL_CARD_FORM_DRAFT: 'getPhysicalCardFormDraft', + REIMBURSEMENT_ACCOUNT_FORM: 'reimbursementAccount', + REIMBURSEMENT_ACCOUNT_FORM_DRAFT: 'reimbursementAccountDraft', }, } as const; type OnyxKeysMap = typeof ONYXKEYS; type OnyxCollectionKey = ValueOf; type OnyxKey = DeepValueOf>; -type OnyxFormKey = ValueOf | OnyxKeysMap['REIMBURSEMENT_ACCOUNT'] | OnyxKeysMap['REIMBURSEMENT_ACCOUNT_DRAFT']; +type OnyxFormKey = ValueOf; type OnyxValues = { [ONYXKEYS.ACCOUNT]: OnyxTypes.Account; @@ -408,8 +407,7 @@ type OnyxValues = { [ONYXKEYS.CARD_LIST]: Record; [ONYXKEYS.WALLET_STATEMENT]: OnyxTypes.WalletStatement; [ONYXKEYS.PERSONAL_BANK_ACCOUNT]: OnyxTypes.PersonalBankAccount; - [ONYXKEYS.REIMBURSEMENT_ACCOUNT]: OnyxTypes.ReimbursementAccountForm; - [ONYXKEYS.REIMBURSEMENT_ACCOUNT_DRAFT]: OnyxTypes.ReimbursementAccountFormDraft; + [ONYXKEYS.REIMBURSEMENT_ACCOUNT]: OnyxTypes.ReimbursementAccount; [ONYXKEYS.PREFERRED_EMOJI_SKIN_TONE]: string | number; [ONYXKEYS.FREQUENTLY_USED_EMOJIS]: OnyxTypes.FrequentlyUsedEmoji[]; [ONYXKEYS.REIMBURSEMENT_ACCOUNT_WORKSPACE_ID]: string; @@ -489,8 +487,8 @@ type OnyxValues = { [ONYXKEYS.FORMS.DATE_OF_BIRTH_FORM_DRAFT]: OnyxTypes.DateOfBirthForm; [ONYXKEYS.FORMS.HOME_ADDRESS_FORM]: OnyxTypes.Form; [ONYXKEYS.FORMS.HOME_ADDRESS_FORM_DRAFT]: OnyxTypes.Form; - [ONYXKEYS.FORMS.NEW_ROOM_FORM]: OnyxTypes.Form; - [ONYXKEYS.FORMS.NEW_ROOM_FORM_DRAFT]: OnyxTypes.Form; + [ONYXKEYS.FORMS.NEW_ROOM_FORM]: OnyxTypes.NewRoomForm; + [ONYXKEYS.FORMS.NEW_ROOM_FORM_DRAFT]: OnyxTypes.NewRoomForm; [ONYXKEYS.FORMS.ROOM_SETTINGS_FORM]: OnyxTypes.Form; [ONYXKEYS.FORMS.ROOM_SETTINGS_FORM_DRAFT]: OnyxTypes.Form; [ONYXKEYS.FORMS.NEW_TASK_FORM]: OnyxTypes.Form; @@ -527,6 +525,9 @@ type OnyxValues = { [ONYXKEYS.FORMS.REPORT_PHYSICAL_CARD_FORM_DRAFT]: OnyxTypes.Form; [ONYXKEYS.FORMS.GET_PHYSICAL_CARD_FORM]: OnyxTypes.Form; [ONYXKEYS.FORMS.GET_PHYSICAL_CARD_FORM_DRAFT]: OnyxTypes.Form; + // @ts-expect-error test + [ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM]: OnyxTypes.Form; + [ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM_DRAFT]: OnyxTypes.Form; }; type OnyxKeyValue = OnyxEntry; diff --git a/src/components/AmountTextInput.tsx b/src/components/AmountTextInput.tsx index 0f3416076cc0..05080fcdd21c 100644 --- a/src/components/AmountTextInput.tsx +++ b/src/components/AmountTextInput.tsx @@ -1,4 +1,5 @@ import React from 'react'; +import type {ForwardedRef} from 'react'; import type {StyleProp, TextStyle, ViewStyle} from 'react-native'; import useThemeStyles from '@hooks/useThemeStyles'; import CONST from '@src/CONST'; @@ -34,7 +35,7 @@ type AmountTextInputProps = { function AmountTextInput( {formattedAmount, onChangeAmount, placeholder, selection, onSelectionChange, style, touchableInputWrapperStyle, onKeyPress}: AmountTextInputProps, - ref: BaseTextInputRef, + ref: ForwardedRef, ) { const styles = useThemeStyles(); return ( diff --git a/src/components/Composer/index.android.tsx b/src/components/Composer/index.android.tsx index d60a41e0f263..8480636a25bd 100644 --- a/src/components/Composer/index.android.tsx +++ b/src/components/Composer/index.android.tsx @@ -2,6 +2,7 @@ import type {ForwardedRef} from 'react'; import React, {useCallback, useEffect, useMemo, useRef} from 'react'; import type {TextInput} from 'react-native'; import {StyleSheet} from 'react-native'; +import type {AnimatedTextInputRef} from '@components/RNTextInput'; import RNTextInput from '@components/RNTextInput'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -35,7 +36,7 @@ function Composer( /** * Set the TextInput Ref */ - const setTextInputRef = useCallback((el: TextInput) => { + const setTextInputRef = useCallback((el: AnimatedTextInputRef) => { textInput.current = el; if (typeof ref !== 'function' || textInput.current === null) { return; diff --git a/src/components/Composer/index.ios.tsx b/src/components/Composer/index.ios.tsx index b1357fef9a46..9fd03e3f7485 100644 --- a/src/components/Composer/index.ios.tsx +++ b/src/components/Composer/index.ios.tsx @@ -2,6 +2,7 @@ import type {ForwardedRef} from 'react'; import React, {useCallback, useEffect, useMemo, useRef} from 'react'; import type {TextInput} from 'react-native'; import {StyleSheet} from 'react-native'; +import type {AnimatedTextInputRef} from '@components/RNTextInput'; import RNTextInput from '@components/RNTextInput'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -27,7 +28,7 @@ function Composer( }: ComposerProps, ref: ForwardedRef, ) { - const textInput = useRef(null); + const textInput = useRef(null); const styles = useThemeStyles(); const theme = useTheme(); @@ -35,7 +36,7 @@ function Composer( /** * Set the TextInput Ref */ - const setTextInputRef = useCallback((el: TextInput) => { + const setTextInputRef = useCallback((el: AnimatedTextInputRef) => { textInput.current = el; if (typeof ref !== 'function' || textInput.current === null) { return; diff --git a/src/components/Composer/index.tsx b/src/components/Composer/index.tsx index 19b7bb6bb30a..3320ef5fb68d 100755 --- a/src/components/Composer/index.tsx +++ b/src/components/Composer/index.tsx @@ -6,8 +6,10 @@ import {flushSync} from 'react-dom'; import type {DimensionValue, NativeSyntheticEvent, Text as RNText, TextInput, TextInputKeyPressEventData, TextInputProps, TextInputSelectionChangeEventData} from 'react-native'; import {StyleSheet, View} from 'react-native'; import type {AnimatedProps} from 'react-native-reanimated'; +import type {AnimatedTextInputRef} from '@components/RNTextInput'; import RNTextInput from '@components/RNTextInput'; import Text from '@components/Text'; +import type {BaseTextInputRef} from '@components/TextInput/BaseTextInput/types'; import useIsScrollBarVisible from '@hooks/useIsScrollBarVisible'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; @@ -82,7 +84,7 @@ function Composer( const {windowWidth} = useWindowDimensions(); const navigation = useNavigation(); const textRef = useRef(null); - const textInput = useRef<(HTMLTextAreaElement & TextInput) | null>(null); + const textInput = useRef(null); const [numberOfLines, setNumberOfLines] = useState(numberOfLinesProp); const [selection, setSelection] = useState< | { @@ -358,7 +360,7 @@ function Composer( autoComplete="off" autoCorrect={!Browser.isMobileSafari()} placeholderTextColor={theme.placeholderText} - ref={(el: TextInput & HTMLTextAreaElement) => (textInput.current = el)} + ref={(el) => (textInput.current = el)} selection={selection} style={inputStyleMemo} value={value} diff --git a/src/components/Form/FormProvider.tsx b/src/components/Form/FormProvider.tsx index 26e045c6a0b9..b7aab46c94c4 100644 --- a/src/components/Form/FormProvider.tsx +++ b/src/components/Form/FormProvider.tsx @@ -10,11 +10,14 @@ import CONST from '@src/CONST'; import type {OnyxFormKey} from '@src/ONYXKEYS'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Form, Network} from '@src/types/onyx'; +import type {FormValueType} from '@src/types/onyx/Form'; import type {Errors} from '@src/types/onyx/OnyxCommon'; import {isEmptyObject, isNotEmptyObject} from '@src/types/utils/EmptyObject'; import FormContext from './FormContext'; import FormWrapper from './FormWrapper'; -import type {FormProps, InputRef, InputRefs, OnyxFormKeyWithoutDraft, OnyxFormValues, OnyxFormValuesFields, RegisterInput, ValueType} from './types'; +import type {BaseInputProps, FormProps, InputRef, InputRefs, OnyxFormKeyWithoutDraft, OnyxFormValues, OnyxFormValuesFields, RegisterInput, ValueTypeKey} from './types'; + +// In order to prevent Checkbox focus loss when the user are focusing a TextInput and proceeds to toggle a CheckBox in web and mobile web. // In order to prevent Checkbox focus loss when the user are focusing a TextInput and proceeds to toggle a CheckBox in web and mobile web. // 200ms delay was chosen as a result of empirical testing. @@ -23,7 +26,7 @@ const VALIDATE_DELAY = 200; type InitialDefaultValue = false | Date | ''; -function getInitialValueByType(valueType?: ValueType): InitialDefaultValue { +function getInitialValueByType(valueType?: ValueTypeKey): InitialDefaultValue { switch (valueType) { case 'string': return ''; @@ -151,7 +154,7 @@ function FormProvider( /** @param inputID - The inputID of the input being touched */ const setTouchedInput = useCallback( - (inputID: string) => { + (inputID: keyof Form) => { touchedInputs.current[inputID] = true; }, [touchedInputs], @@ -183,13 +186,13 @@ function FormProvider( }, [enabledWhenOffline, formState?.isLoading, inputValues, network?.isOffline, onSubmit, onValidate]); const resetForm = useCallback( - (optionalValue: Form) => { + (optionalValue: OnyxFormValuesFields) => { Object.keys(inputValues).forEach((inputID) => { setInputValues((prevState) => { const copyPrevState = {...prevState}; touchedInputs.current[inputID] = false; - copyPrevState[inputID] = optionalValue[inputID] || ''; + copyPrevState[inputID] = optionalValue[inputID as keyof OnyxFormValuesFields] || ''; return copyPrevState; }); @@ -202,8 +205,8 @@ function FormProvider( resetForm, })); - const registerInput: RegisterInput = useCallback( - (inputID, inputProps) => { + const registerInput = useCallback( + (inputID: keyof Form, inputProps: TInputProps): TInputProps => { const newRef: MutableRefObject = inputRefs.current[inputID] ?? inputProps.ref ?? createRef(); if (inputRefs.current[inputID] !== newRef) { inputRefs.current[inputID] = newRef; @@ -212,7 +215,7 @@ function FormProvider( inputValues[inputID] = inputProps.value; } else if (inputProps.shouldSaveDraft && draftValues?.[inputID] !== undefined && inputValues[inputID] === undefined) { inputValues[inputID] = draftValues[inputID]; - } else if (inputProps.shouldUseDefaultValue && inputValues[inputID] === undefined) { + } else if (inputProps.shouldUseDefaultValue && inputProps.defaultValue !== undefined && inputValues[inputID] === undefined) { // We force the form to set the input value from the defaultValue props if there is a saved valid value inputValues[inputID] = inputProps.defaultValue; } else if (inputValues[inputID] === undefined) { @@ -228,6 +231,7 @@ function FormProvider( .at(-1) ?? ''; const inputRef = inputProps.ref; + return { ...inputProps, ref: @@ -298,7 +302,7 @@ function FormProvider( } inputProps.onBlur?.(event); }, - onInputChange: (value: unknown, key?: string) => { + onInputChange: (value: FormValueType, key?: string) => { const inputKey = key ?? inputID; setInputValues((prevState) => { const newState = { @@ -309,7 +313,7 @@ function FormProvider( if (shouldValidateOnChange) { onValidate(newState); } - return newState; + return newState as Form; }); if (inputProps.shouldSaveDraft && !(formID as string).includes('Draft')) { diff --git a/src/components/Form/InputWrapper.tsx b/src/components/Form/InputWrapper.tsx index 8e824875c6d4..4313d800708d 100644 --- a/src/components/Form/InputWrapper.tsx +++ b/src/components/Form/InputWrapper.tsx @@ -1,13 +1,17 @@ -import type {ForwardedRef} from 'react'; +import type {ForwardedRef, FunctionComponent} from 'react'; import React, {forwardRef, useContext} from 'react'; import TextInput from '@components/TextInput'; import type {BaseTextInputRef} from '@components/TextInput/BaseTextInput/types'; import FormContext from './FormContext'; -import type {InputWrapperProps, ValidInput} from './types'; +import type {BaseInputProps, InputWrapperProps} from './types'; -function InputWrapper({InputComponent, inputID, valueType = 'string', ...rest}: InputWrapperProps, ref: ForwardedRef) { - const {registerInput} = useContext(FormContext); +type WrappableInputs = typeof TextInput; +function InputWrapper( + {InputComponent, inputID, valueType = 'string', ...rest}: InputWrapperProps, + ref: ForwardedRef, +) { + const {registerInput} = useContext(FormContext); // There are inputs that don't have onBlur methods, to simulate the behavior of onBlur in e.g. checkbox, we had to // use different methods like onPress. This introduced a problem that inputs that have the onBlur method were // calling some methods too early or twice, so we had to add this check to prevent that side effect. @@ -16,7 +20,7 @@ function InputWrapper({InputComponent, inputID, value // TODO: Sometimes we return too many props with register input, so we need to consider if it's better to make the returned type more general and disregard the issue, or we would like to omit the unused props somehow. // eslint-disable-next-line react/jsx-props-no-spreading - return ; + return ; } InputWrapper.displayName = 'InputWrapper'; diff --git a/src/components/Form/types.ts b/src/components/Form/types.ts index 0a9069ea596a..024d34d7e492 100644 --- a/src/components/Form/types.ts +++ b/src/components/Form/types.ts @@ -1,32 +1,39 @@ -import type {ComponentProps, ElementType, FocusEvent, MutableRefObject, ReactNode} from 'react'; -import type {StyleProp, ViewStyle} from 'react-native'; -import type {BaseTextInputRef} from '@components/TextInput/BaseTextInput/types'; +import type {ComponentProps, FocusEvent, ForwardedRef, FunctionComponent, Key, MutableRefObject, ReactNode, Ref, RefAttributes} from 'react'; +import {ComponentType} from 'react'; +import type {NativeSyntheticEvent, StyleProp, TextInputFocusEventData, ViewStyle} from 'react-native'; import type {OnyxFormKey, OnyxValues} from '@src/ONYXKEYS'; -import type {Form} from '@src/types/onyx'; +import type Form from '@src/types/onyx/Form'; +import type {BaseForm, FormValueType} from '@src/types/onyx/Form'; -type ValueType = 'string' | 'boolean' | 'date'; +type ValueTypeKey = 'string' | 'boolean' | 'date'; -type ValidInput = ElementType; - -type InputProps = ComponentProps & { +type BaseInputProps = { shouldSetTouchedOnBlurOnly?: boolean; onValueChange?: (value: unknown, key: string) => void; onTouched?: (event: unknown) => void; - valueType?: ValueType; - onBlur: (event: FocusEvent | Parameters['onBlur']>>[0]) => void; + valueType?: ValueTypeKey; + value?: FormValueType; + defaultValue?: FormValueType; + onBlur?: (event: FocusEvent | NativeSyntheticEvent) => void; + onPressOut?: (event: unknown) => void; + onPress?: (event: unknown) => void; + shouldSaveDraft?: boolean; + shouldUseDefaultValue?: boolean; + key?: Key | null | undefined; + ref?: Ref>; + isFocused?: boolean; }; -type InputWrapperProps = InputProps & { +type InputWrapperProps = TInputProps & { InputComponent: TInput; inputID: string; - valueType?: ValueType; }; type ExcludeDraft = T extends `${string}Draft` ? never : T; type OnyxFormKeyWithoutDraft = ExcludeDraft; type OnyxFormValues = OnyxValues[TOnyxKey]; -type OnyxFormValuesFields = Omit; +type OnyxFormValuesFields = Omit, keyof BaseForm>; type FormProps = { /** A unique Onyx key identifying the form */ @@ -57,9 +64,9 @@ type FormProps = { footerContent?: ReactNode; }; -type RegisterInput = (inputID: string, props: InputProps) => InputProps; +type RegisterInput = (inputID: keyof Form, inputProps: TInputProps) => TInputProps; -type InputRef = BaseTextInputRef; +type InputRef = FunctionComponent; type InputRefs = Record>; -export type {InputWrapperProps, ValidInput, FormProps, RegisterInput, ValueType, OnyxFormValues, OnyxFormValuesFields, InputProps, InputRef, InputRefs, OnyxFormKeyWithoutDraft}; +export type {InputWrapperProps, FormProps, RegisterInput, BaseInputProps, ValueTypeKey, OnyxFormValues, OnyxFormValuesFields, InputRef, InputRefs, OnyxFormKeyWithoutDraft}; diff --git a/src/components/RNTextInput.tsx b/src/components/RNTextInput.tsx index 526a5891df16..e21219e99730 100644 --- a/src/components/RNTextInput.tsx +++ b/src/components/RNTextInput.tsx @@ -9,7 +9,7 @@ import useTheme from '@hooks/useTheme'; // Convert the underlying TextInput into an Animated component so that we can take an animated ref and pass it to a worklet const AnimatedTextInput = Animated.createAnimatedComponent(TextInput); -type AnimatedTextInputRef = typeof AnimatedTextInput & TextInput; +type AnimatedTextInputRef = typeof AnimatedTextInput & TextInput & HTMLInputElement; function RNTextInputWithRef(props: TextInputProps, ref: ForwardedRef) { const theme = useTheme(); diff --git a/src/libs/ErrorUtils.ts b/src/libs/ErrorUtils.ts index 97d180408c8a..18aa262c2079 100644 --- a/src/libs/ErrorUtils.ts +++ b/src/libs/ErrorUtils.ts @@ -98,7 +98,7 @@ type ErrorsList = Record; /** * Method used to generate error message for given inputID - * @param errorList - An object containing current errors in the form + * @param errors - An object containing current errors in the form * @param message - Message to assign to the inputID errors */ function addErrorMessage(errors: ErrorsList, inputID?: string, message?: TKey) { diff --git a/src/libs/actions/FormActions.ts b/src/libs/actions/FormActions.ts index 779874d8b890..243fd062efeb 100644 --- a/src/libs/actions/FormActions.ts +++ b/src/libs/actions/FormActions.ts @@ -3,19 +3,18 @@ import type {KeyValueMapping, NullishDeep} from 'react-native-onyx/lib/types'; import type {OnyxFormKeyWithoutDraft} from '@components/Form/types'; import FormUtils from '@libs/FormUtils'; import type {OnyxFormKey} from '@src/ONYXKEYS'; -import type {Form} from '@src/types/onyx'; import type * as OnyxCommon from '@src/types/onyx/OnyxCommon'; function setIsLoading(formID: OnyxFormKey, isLoading: boolean) { - Onyx.merge(formID, {isLoading} satisfies Form); + Onyx.merge(formID, {isLoading}); } function setErrors(formID: OnyxFormKey, errors?: OnyxCommon.Errors | null) { - Onyx.merge(formID, {errors} satisfies Form); + Onyx.merge(formID, {errors}); } function setErrorFields(formID: OnyxFormKey, errorFields?: OnyxCommon.ErrorFields | null) { - Onyx.merge(formID, {errorFields} satisfies Form); + Onyx.merge(formID, {errorFields}); } function setDraftValues(formID: OnyxFormKeyWithoutDraft, draftValues: NullishDeep) { diff --git a/src/libs/actions/Plaid.ts b/src/libs/actions/Plaid.ts index ab828eefeece..8c35c391790a 100644 --- a/src/libs/actions/Plaid.ts +++ b/src/libs/actions/Plaid.ts @@ -28,7 +28,7 @@ function openPlaidBankLogin(allowDebit: boolean, bankAccountID: number) { }, { onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.REIMBURSEMENT_ACCOUNT_DRAFT, + key: ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM_DRAFT, value: { plaidAccountID: '', }, diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 4729feea736e..4aecc91c54e1 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -2583,7 +2583,8 @@ function updateLastVisitTime(reportID: string) { function clearNewRoomFormError() { Onyx.set(ONYXKEYS.FORMS.NEW_ROOM_FORM, { isLoading: false, - errorFields: {}, + errorFields: null, + errors: null, }); } diff --git a/src/pages/settings/Profile/DisplayNamePage.tsx b/src/pages/settings/Profile/DisplayNamePage.tsx index a481b9ccdbec..75fd2b8dbe3c 100644 --- a/src/pages/settings/Profile/DisplayNamePage.tsx +++ b/src/pages/settings/Profile/DisplayNamePage.tsx @@ -21,6 +21,7 @@ import * as PersonalDetails from '@userActions/PersonalDetails'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import type {Errors} from '@src/types/onyx/OnyxCommon'; const updateDisplayName = (values: any) => { PersonalDetails.updateDisplayName(values.firstName.trim(), values.lastName.trim()); @@ -38,13 +39,12 @@ function DisplayNamePage(props: any) { * @returns - An object containing the errors for each inputID */ const validate = (values: OnyxFormValuesFields) => { - const errors = {}; - + const errors: Errors = {}; // First we validate the first name field if (!ValidationUtils.isValidDisplayName(values.firstName)) { ErrorUtils.addErrorMessage(errors, 'firstName', 'personalDetails.error.hasInvalidCharacter'); } - if (ValidationUtils.doesContainReservedWord(values.firstName, CONST.DISPLAY_NAME.RESERVED_FIRST_NAMES as string[])) { + if (ValidationUtils.doesContainReservedWord(values.firstName, CONST.DISPLAY_NAME.RESERVED_FIRST_NAMES as unknown as string[])) { ErrorUtils.addErrorMessage(errors, 'firstName', 'personalDetails.error.containsReservedWord'); } @@ -117,7 +117,7 @@ export default compose( withCurrentUserPersonalDetails, withOnyx({ isLoadingApp: { - key: ONYXKEYS.IS_LOADING_APP, + key: ONYXKEYS.IS_LOADING_APP as any, }, }), )(DisplayNamePage); diff --git a/src/types/onyx/Form.ts b/src/types/onyx/Form.ts index 9306ab5736fc..8da34697fe5d 100644 --- a/src/types/onyx/Form.ts +++ b/src/types/onyx/Form.ts @@ -1,9 +1,9 @@ import type * as OnyxTypes from './index'; import type * as OnyxCommon from './OnyxCommon'; -type Form = { - [key: string]: unknown; +type FormValueType = string | boolean | Date; +type BaseForm = { /** Controls the loading state of the form */ isLoading?: boolean; @@ -14,21 +14,31 @@ type Form = { errorFields?: OnyxCommon.ErrorFields | null; }; -type AddDebitCardForm = Form & { - /** Whether or not the form has been submitted */ +type Form = Record> = TFormValues & BaseForm; + +type AddDebitCardForm = Form<{ + /** Whether the form has been submitted */ setupComplete: boolean; -}; +}>; -type DateOfBirthForm = Form & { +type DateOfBirthForm = Form<{ /** Date of birth */ dob?: string; -}; +}>; -type DisplayNameForm = OnyxTypes.Form & { +type DisplayNameForm = Form<{ firstName: string; lastName: string; -}; +}>; + +type NewRoomForm = Form<{ + roomName?: string; + welcomeMessage?: string; + policyID?: string; + writeCapability?: string; + visibility?: string; +}>; export default Form; -export type {AddDebitCardForm, DateOfBirthForm, DisplayNameForm}; +export type {AddDebitCardForm, DateOfBirthForm, DisplayNameForm, FormValueType, NewRoomForm, BaseForm}; diff --git a/src/types/onyx/ReimbursementAccount.ts b/src/types/onyx/ReimbursementAccount.ts index 4779b790eac0..fca43df9b06e 100644 --- a/src/types/onyx/ReimbursementAccount.ts +++ b/src/types/onyx/ReimbursementAccount.ts @@ -49,7 +49,5 @@ type ReimbursementAccount = { pendingAction?: OnyxCommon.PendingAction; }; -type ReimbursementAccountForm = ReimbursementAccount & OnyxTypes.Form; - export default ReimbursementAccount; -export type {BankAccountStep, BankAccountSubStep, ReimbursementAccountForm}; +export type {BankAccountStep, BankAccountSubStep}; diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts index 1436bb38e1e2..6fcc5ec03d58 100644 --- a/src/types/onyx/index.ts +++ b/src/types/onyx/index.ts @@ -9,7 +9,7 @@ import type Credentials from './Credentials'; import type Currency from './Currency'; import type CustomStatusDraft from './CustomStatusDraft'; import type Download from './Download'; -import type {AddDebitCardForm, DateOfBirthForm, DisplayNameForm} from './Form'; +import type {AddDebitCardForm, DateOfBirthForm, DisplayNameForm, NewRoomForm} from './Form'; import type Form from './Form'; import type FrequentlyUsedEmoji from './FrequentlyUsedEmoji'; import type {FundList} from './Fund'; @@ -38,7 +38,6 @@ import type RecentlyUsedReportFields from './RecentlyUsedReportFields'; import type RecentlyUsedTags from './RecentlyUsedTags'; import type RecentWaypoint from './RecentWaypoint'; import type ReimbursementAccount from './ReimbursementAccount'; -import type {ReimbursementAccountForm} from './ReimbursementAccount'; import type ReimbursementAccountDraft from './ReimbursementAccountDraft'; import type {ReimbursementAccountFormDraft} from './ReimbursementAccountDraft'; import type Report from './Report'; @@ -112,7 +111,6 @@ export type { RecentlyUsedCategories, RecentlyUsedTags, ReimbursementAccount, - ReimbursementAccountForm, ReimbursementAccountDraft, ReimbursementAccountFormDraft, Report, @@ -144,4 +142,5 @@ export type { ReportUserIsTyping, PolicyReportField, RecentlyUsedReportFields, + NewRoomForm, }; From bf7f887234f0e7f9714570d5bd911e1d613e3fd5 Mon Sep 17 00:00:00 2001 From: Pujan Date: Wed, 17 Jan 2024 21:34:37 +0530 Subject: [PATCH 205/391] removed notes view page --- src/ROUTES.ts | 4 - src/SCREENS.ts | 1 - .../AppNavigator/ModalStackNavigators.tsx | 1 - src/libs/Navigation/linkingConfig.ts | 1 - .../PrivateNotes/PrivateNotesViewPage.js | 112 ------------------ 5 files changed, 119 deletions(-) delete mode 100644 src/pages/PrivateNotes/PrivateNotesViewPage.js diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 37003a09a0cd..532516bf0f42 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -224,10 +224,6 @@ const ROUTES = { route: 'r/:reportID/assignee', getRoute: (reportID: string) => `r/${reportID}/assignee` as const, }, - PRIVATE_NOTES_VIEW: { - route: 'r/:reportID/notes/:accountID', - getRoute: (reportID: string, accountID: string | number) => `r/${reportID}/notes/${accountID}` as const, - }, PRIVATE_NOTES_LIST: { route: 'r/:reportID/notes', getRoute: (reportID: string) => `r/${reportID}/notes` as const, diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 703cb309d641..bf131078466b 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -180,7 +180,6 @@ const SCREENS = { }, PRIVATE_NOTES: { - VIEW: 'PrivateNotes_View', LIST: 'PrivateNotes_List', EDIT: 'PrivateNotes_Edit', }, diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx index b0f33af0ce2e..1d586c6f7378 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx @@ -269,7 +269,6 @@ const EditRequestStackNavigator = createModalStackNavigator({ - [SCREENS.PRIVATE_NOTES.VIEW]: () => require('../../../pages/PrivateNotes/PrivateNotesViewPage').default as React.ComponentType, [SCREENS.PRIVATE_NOTES.LIST]: () => require('../../../pages/PrivateNotes/PrivateNotesListPage').default as React.ComponentType, [SCREENS.PRIVATE_NOTES.EDIT]: () => require('../../../pages/PrivateNotes/PrivateNotesEditPage').default as React.ComponentType, }); diff --git a/src/libs/Navigation/linkingConfig.ts b/src/libs/Navigation/linkingConfig.ts index 1a495e92eb80..f0a031a88302 100644 --- a/src/libs/Navigation/linkingConfig.ts +++ b/src/libs/Navigation/linkingConfig.ts @@ -278,7 +278,6 @@ const linkingConfig: LinkingOptions = { }, [SCREENS.RIGHT_MODAL.PRIVATE_NOTES]: { screens: { - [SCREENS.PRIVATE_NOTES.VIEW]: ROUTES.PRIVATE_NOTES_VIEW.route, [SCREENS.PRIVATE_NOTES.LIST]: ROUTES.PRIVATE_NOTES_LIST.route, [SCREENS.PRIVATE_NOTES.EDIT]: ROUTES.PRIVATE_NOTES_EDIT.route, }, diff --git a/src/pages/PrivateNotes/PrivateNotesViewPage.js b/src/pages/PrivateNotes/PrivateNotesViewPage.js deleted file mode 100644 index f71259a2b685..000000000000 --- a/src/pages/PrivateNotes/PrivateNotesViewPage.js +++ /dev/null @@ -1,112 +0,0 @@ -import lodashGet from 'lodash/get'; -import PropTypes from 'prop-types'; -import React from 'react'; -import {ScrollView} from 'react-native'; -import {withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; -import HeaderWithBackButton from '@components/HeaderWithBackButton'; -import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; -import OfflineWithFeedback from '@components/OfflineWithFeedback'; -import ScreenWrapper from '@components/ScreenWrapper'; -import withLocalize from '@components/withLocalize'; -import useLocalize from '@hooks/useLocalize'; -import useThemeStyles from '@hooks/useThemeStyles'; -import compose from '@libs/compose'; -import Navigation from '@libs/Navigation/Navigation'; -import withReportAndPrivateNotesOrNotFound from '@pages/home/report/withReportAndPrivateNotesOrNotFound'; -import personalDetailsPropType from '@pages/personalDetailsPropType'; -import reportPropTypes from '@pages/reportPropTypes'; -import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; -import ROUTES from '@src/ROUTES'; - -const propTypes = { - /** All of the personal details for everyone */ - personalDetailsList: PropTypes.objectOf(personalDetailsPropType), - - /** The report currently being looked at */ - report: reportPropTypes, - route: PropTypes.shape({ - /** Params from the URL path */ - params: PropTypes.shape({ - /** reportID and accountID passed via route: /r/:reportID/notes */ - reportID: PropTypes.string, - accountID: PropTypes.string, - }), - }).isRequired, - - /** Session of currently logged in user */ - session: PropTypes.shape({ - /** Currently logged in user accountID */ - accountID: PropTypes.number, - }), -}; - -const defaultProps = { - report: {}, - session: { - accountID: null, - }, - personalDetailsList: {}, -}; - -function PrivateNotesViewPage({route, personalDetailsList, session, report}) { - const styles = useThemeStyles(); - const {translate} = useLocalize(); - const isCurrentUserNote = Number(session.accountID) === Number(route.params.accountID); - const privateNote = lodashGet(report, ['privateNotes', route.params.accountID, 'note'], ''); - - const getFallbackRoute = () => { - const privateNotes = lodashGet(report, 'privateNotes', {}); - - if (_.keys(privateNotes).length === 1) { - return ROUTES.HOME; - } - - return ROUTES.PRIVATE_NOTES_LIST.getRoute(report.reportID); - }; - - return ( - - Navigation.goBack(getFallbackRoute())} - subtitle={isCurrentUserNote ? translate('privateNotes.myNote') : `${lodashGet(personalDetailsList, [route.params.accountID, 'login'], '')} note`} - shouldShowBackButton - onCloseButtonPress={() => Navigation.dismissModal()} - /> - - - isCurrentUserNote && Navigation.navigate(ROUTES.PRIVATE_NOTES_EDIT.getRoute(report.reportID, route.params.accountID))} - shouldShowRightIcon={isCurrentUserNote} - numberOfLinesTitle={0} - shouldRenderAsHTML - brickRoadIndicator={!_.isEmpty(lodashGet(report, ['privateNotes', route.params.accountID, 'errors'], '')) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : ''} - disabled={!isCurrentUserNote} - shouldGreyOutWhenDisabled={false} - /> - - - - ); -} - -PrivateNotesViewPage.displayName = 'PrivateNotesViewPage'; -PrivateNotesViewPage.propTypes = propTypes; -PrivateNotesViewPage.defaultProps = defaultProps; - -export default compose( - withLocalize, - withReportAndPrivateNotesOrNotFound('privateNotes.title'), - withOnyx({ - personalDetailsList: { - key: ONYXKEYS.PERSONAL_DETAILS_LIST, - }, - }), -)(PrivateNotesViewPage); From 2baa8784975e99dcd3ba9dd4b2658fc46abd84ba Mon Sep 17 00:00:00 2001 From: Pujan Date: Wed, 17 Jan 2024 21:51:03 +0530 Subject: [PATCH 206/391] removed notes view for types --- src/libs/Navigation/types.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 8d227fa6f697..f87ed5094a82 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -331,10 +331,6 @@ type ProcessMoneyRequestHoldNavigatorParamList = { }; type PrivateNotesNavigatorParamList = { - [SCREENS.PRIVATE_NOTES.VIEW]: { - reportID: string; - accountID: string; - }; [SCREENS.PRIVATE_NOTES.LIST]: { reportID: string; accountID: string; From 64ead2bb48c1df23e1f972c834262980fbf21c37 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Wed, 17 Jan 2024 18:42:47 +0100 Subject: [PATCH 207/391] Rename REIMBURSEMENT_ACCOUNT_FORM_DRAFT --- src/libs/actions/ReimbursementAccount/index.js | 2 +- .../actions/ReimbursementAccount/resetFreePlanBankAccount.js | 2 +- src/pages/ReimbursementAccount/ACHContractStep.js | 2 +- src/pages/ReimbursementAccount/BankAccountManualStep.js | 2 +- src/pages/ReimbursementAccount/BankAccountPlaidStep.js | 2 +- src/pages/ReimbursementAccount/CompanyStep.js | 2 +- src/pages/ReimbursementAccount/ReimbursementAccountPage.js | 2 +- src/pages/ReimbursementAccount/RequestorStep.js | 2 +- src/pages/ReimbursementAccount/ValidationStep.js | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/libs/actions/ReimbursementAccount/index.js b/src/libs/actions/ReimbursementAccount/index.js index 0404115f086b..e23f80e61d12 100644 --- a/src/libs/actions/ReimbursementAccount/index.js +++ b/src/libs/actions/ReimbursementAccount/index.js @@ -30,7 +30,7 @@ function setWorkspaceIDForReimbursementAccount(workspaceID) { * @param {Object} bankAccountData */ function updateReimbursementAccountDraft(bankAccountData) { - Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT_DRAFT, bankAccountData); + Onyx.merge(ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM_DRAFT, bankAccountData); Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {draftStep: undefined}); } diff --git a/src/libs/actions/ReimbursementAccount/resetFreePlanBankAccount.js b/src/libs/actions/ReimbursementAccount/resetFreePlanBankAccount.js index 14c988033689..3110c059d2fc 100644 --- a/src/libs/actions/ReimbursementAccount/resetFreePlanBankAccount.js +++ b/src/libs/actions/ReimbursementAccount/resetFreePlanBankAccount.js @@ -60,7 +60,7 @@ function resetFreePlanBankAccount(bankAccountID, session) { }, { onyxMethod: Onyx.METHOD.SET, - key: ONYXKEYS.REIMBURSEMENT_ACCOUNT_DRAFT, + key: ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM_DRAFT, value: {}, }, ], diff --git a/src/pages/ReimbursementAccount/ACHContractStep.js b/src/pages/ReimbursementAccount/ACHContractStep.js index 806e438d0397..625a29ddc130 100644 --- a/src/pages/ReimbursementAccount/ACHContractStep.js +++ b/src/pages/ReimbursementAccount/ACHContractStep.js @@ -159,7 +159,7 @@ function ACHContractStep(props) { guidesCallTaskID={CONST.GUIDES_CALL_TASK_IDS.WORKSPACE_BANK_ACCOUNT} /> Date: Wed, 17 Jan 2024 18:53:49 +0100 Subject: [PATCH 208/391] Clean rest of form PR --- src/ONYXKEYS.ts | 2 +- src/components/Composer/index.tsx | 3 +-- src/components/Form/FormProvider.tsx | 4 ++-- src/components/Form/FormWrapper.tsx | 4 ++-- src/components/Form/InputWrapper.tsx | 16 +++++++++++----- src/components/Form/types.ts | 14 ++++++++------ src/types/onyx/Form.ts | 1 - src/types/onyx/ReimbursementAccount.ts | 1 - 8 files changed, 25 insertions(+), 20 deletions(-) diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index e5df472b5997..ee6c89b65cbd 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -525,7 +525,7 @@ type OnyxValues = { [ONYXKEYS.FORMS.REPORT_PHYSICAL_CARD_FORM_DRAFT]: OnyxTypes.Form; [ONYXKEYS.FORMS.GET_PHYSICAL_CARD_FORM]: OnyxTypes.Form; [ONYXKEYS.FORMS.GET_PHYSICAL_CARD_FORM_DRAFT]: OnyxTypes.Form; - // @ts-expect-error test + // @ts-expect-error Different values are defined under the same key: ReimbursementAccount and ReimbursementAccountForm [ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM]: OnyxTypes.Form; [ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM_DRAFT]: OnyxTypes.Form; }; diff --git a/src/components/Composer/index.tsx b/src/components/Composer/index.tsx index 3320ef5fb68d..71ce5e546b16 100755 --- a/src/components/Composer/index.tsx +++ b/src/components/Composer/index.tsx @@ -3,13 +3,12 @@ import ExpensiMark from 'expensify-common/lib/ExpensiMark'; import type {BaseSyntheticEvent, ForwardedRef} from 'react'; import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; import {flushSync} from 'react-dom'; -import type {DimensionValue, NativeSyntheticEvent, Text as RNText, TextInput, TextInputKeyPressEventData, TextInputProps, TextInputSelectionChangeEventData} from 'react-native'; +import type {DimensionValue, NativeSyntheticEvent, Text as RNText, TextInputKeyPressEventData, TextInputProps, TextInputSelectionChangeEventData} from 'react-native'; import {StyleSheet, View} from 'react-native'; import type {AnimatedProps} from 'react-native-reanimated'; import type {AnimatedTextInputRef} from '@components/RNTextInput'; import RNTextInput from '@components/RNTextInput'; import Text from '@components/Text'; -import type {BaseTextInputRef} from '@components/TextInput/BaseTextInput/types'; import useIsScrollBarVisible from '@hooks/useIsScrollBarVisible'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; diff --git a/src/components/Form/FormProvider.tsx b/src/components/Form/FormProvider.tsx index b7aab46c94c4..8fe35c989c62 100644 --- a/src/components/Form/FormProvider.tsx +++ b/src/components/Form/FormProvider.tsx @@ -15,7 +15,7 @@ import type {Errors} from '@src/types/onyx/OnyxCommon'; import {isEmptyObject, isNotEmptyObject} from '@src/types/utils/EmptyObject'; import FormContext from './FormContext'; import FormWrapper from './FormWrapper'; -import type {BaseInputProps, FormProps, InputRef, InputRefs, OnyxFormKeyWithoutDraft, OnyxFormValues, OnyxFormValuesFields, RegisterInput, ValueTypeKey} from './types'; +import type {BaseInputProps, FormProps, InputRefs, OnyxFormKeyWithoutDraft, OnyxFormValues, OnyxFormValuesFields, RegisterInput, ValueTypeKey} from './types'; // In order to prevent Checkbox focus loss when the user are focusing a TextInput and proceeds to toggle a CheckBox in web and mobile web. @@ -207,7 +207,7 @@ function FormProvider( const registerInput = useCallback( (inputID: keyof Form, inputProps: TInputProps): TInputProps => { - const newRef: MutableRefObject = inputRefs.current[inputID] ?? inputProps.ref ?? createRef(); + const newRef: MutableRefObject = inputRefs.current[inputID] ?? inputProps.ref ?? createRef(); if (inputRefs.current[inputID] !== newRef) { inputRefs.current[inputID] = newRef; } diff --git a/src/components/Form/FormWrapper.tsx b/src/components/Form/FormWrapper.tsx index a513b8fa0845..77b34cb551aa 100644 --- a/src/components/Form/FormWrapper.tsx +++ b/src/components/Form/FormWrapper.tsx @@ -85,7 +85,7 @@ function FormWrapper({ const focusInput = inputRef && 'current' in inputRef ? inputRef.current : undefined; // Dismiss the keyboard for non-text fields by checking if the component has the isFocused method, as only TextInput has this method. - if (typeof focusInput?.isFocused !== 'function') { + if (focusInput && typeof focusInput?.isFocused !== 'function') { Keyboard.dismiss(); } @@ -102,7 +102,7 @@ function FormWrapper({ } // Focus the input after scrolling, as on the Web it gives a slightly better visual result - focusInput?.focus(); + focusInput?.focus?.(); }} containerStyles={[styles.mh0, styles.mt5, styles.flex1, submitButtonStyles]} enabledWhenOffline={enabledWhenOffline} diff --git a/src/components/Form/InputWrapper.tsx b/src/components/Form/InputWrapper.tsx index 4313d800708d..e1f210b05ae9 100644 --- a/src/components/Form/InputWrapper.tsx +++ b/src/components/Form/InputWrapper.tsx @@ -1,13 +1,19 @@ -import type {ForwardedRef, FunctionComponent} from 'react'; +import type {ForwardedRef} from 'react'; import React, {forwardRef, useContext} from 'react'; +import type AmountTextInput from '@components/AmountTextInput'; +import type CheckboxWithLabel from '@components/CheckboxWithLabel'; +import type Picker from '@components/Picker'; +import type SingleChoiceQuestion from '@components/SingleChoiceQuestion'; import TextInput from '@components/TextInput'; import type {BaseTextInputRef} from '@components/TextInput/BaseTextInput/types'; import FormContext from './FormContext'; import type {BaseInputProps, InputWrapperProps} from './types'; -type WrappableInputs = typeof TextInput; +// TODO: Add remaining inputs here once these components are migrated to Typescript: +// AddressSearch | CountrySelector | StatePicker | DatePicker | EmojiPickerButtonDropdown | RoomNameInput | ValuePicker +type ValidInputs = typeof TextInput | typeof AmountTextInput | typeof SingleChoiceQuestion | typeof CheckboxWithLabel | typeof Picker; -function InputWrapper( +function InputWrapper( {InputComponent, inputID, valueType = 'string', ...rest}: InputWrapperProps, ref: ForwardedRef, ) { @@ -19,8 +25,8 @@ function InputWrapper; + // eslint-disable-next-line react/jsx-props-no-spreading, @typescript-eslint/no-explicit-any + return ; } InputWrapper.displayName = 'InputWrapper'; diff --git a/src/components/Form/types.ts b/src/components/Form/types.ts index 024d34d7e492..846322dd719b 100644 --- a/src/components/Form/types.ts +++ b/src/components/Form/types.ts @@ -1,5 +1,4 @@ -import type {ComponentProps, FocusEvent, ForwardedRef, FunctionComponent, Key, MutableRefObject, ReactNode, Ref, RefAttributes} from 'react'; -import {ComponentType} from 'react'; +import type {FocusEvent, Key, MutableRefObject, ReactNode, Ref} from 'react'; import type {NativeSyntheticEvent, StyleProp, TextInputFocusEventData, ViewStyle} from 'react-native'; import type {OnyxFormKey, OnyxValues} from '@src/ONYXKEYS'; import type Form from '@src/types/onyx/Form'; @@ -7,6 +6,8 @@ import type {BaseForm, FormValueType} from '@src/types/onyx/Form'; type ValueTypeKey = 'string' | 'boolean' | 'date'; +type MeasureLayoutOnSuccessCallback = (left: number, top: number, width: number, height: number) => void; + type BaseInputProps = { shouldSetTouchedOnBlurOnly?: boolean; onValueChange?: (value: unknown, key: string) => void; @@ -20,8 +21,10 @@ type BaseInputProps = { shouldSaveDraft?: boolean; shouldUseDefaultValue?: boolean; key?: Key | null | undefined; - ref?: Ref>; + ref?: Ref; isFocused?: boolean; + measureLayout?: (ref: unknown, callback: MeasureLayoutOnSuccessCallback) => void; + focus?: () => void; }; type InputWrapperProps = TInputProps & { @@ -66,7 +69,6 @@ type FormProps = { type RegisterInput = (inputID: keyof Form, inputProps: TInputProps) => TInputProps; -type InputRef = FunctionComponent; -type InputRefs = Record>; +type InputRefs = Record>; -export type {InputWrapperProps, FormProps, RegisterInput, BaseInputProps, ValueTypeKey, OnyxFormValues, OnyxFormValuesFields, InputRef, InputRefs, OnyxFormKeyWithoutDraft}; +export type {InputWrapperProps, FormProps, RegisterInput, BaseInputProps, ValueTypeKey, OnyxFormValues, OnyxFormValuesFields, InputRefs, OnyxFormKeyWithoutDraft}; diff --git a/src/types/onyx/Form.ts b/src/types/onyx/Form.ts index 8da34697fe5d..6ef0197495d5 100644 --- a/src/types/onyx/Form.ts +++ b/src/types/onyx/Form.ts @@ -1,4 +1,3 @@ -import type * as OnyxTypes from './index'; import type * as OnyxCommon from './OnyxCommon'; type FormValueType = string | boolean | Date; diff --git a/src/types/onyx/ReimbursementAccount.ts b/src/types/onyx/ReimbursementAccount.ts index fca43df9b06e..c0ade25e4d79 100644 --- a/src/types/onyx/ReimbursementAccount.ts +++ b/src/types/onyx/ReimbursementAccount.ts @@ -1,6 +1,5 @@ import type {ValueOf} from 'type-fest'; import type CONST from '@src/CONST'; -import type * as OnyxTypes from './index'; import type * as OnyxCommon from './OnyxCommon'; type BankAccountStep = ValueOf; From 11a225f4b1ec4265c27c097710fb00dd2a978874 Mon Sep 17 00:00:00 2001 From: Antony Kithinzi Date: Wed, 17 Jan 2024 22:47:27 +0300 Subject: [PATCH 209/391] Update src/pages/home/report/ReportActionItemCreated.tsx MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Fábio Henriques --- src/pages/home/report/ReportActionItemCreated.tsx | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/pages/home/report/ReportActionItemCreated.tsx b/src/pages/home/report/ReportActionItemCreated.tsx index 86b7eef2c8ae..edf8b1c9c48e 100644 --- a/src/pages/home/report/ReportActionItemCreated.tsx +++ b/src/pages/home/report/ReportActionItemCreated.tsx @@ -37,14 +37,6 @@ type ReportActionItemCreatedProps = OnyxProps & { // eslint-disable-next-line react/no-unused-prop-types policyID: string; - /** The policy object for the current route */ - policy?: { - /** The name of the policy */ - name?: string; - - /** The URL for the policy avatar */ - avatar?: string; - }; }; function ReportActionItemCreated(props: ReportActionItemCreatedProps) { const styles = useThemeStyles(); From 9bad7da3a1dd29734e7a79f226e7d1ac81cc1b44 Mon Sep 17 00:00:00 2001 From: Antony Kithinzi Date: Wed, 17 Jan 2024 22:48:50 +0300 Subject: [PATCH 210/391] Update src/pages/home/report/ReportActionItemCreated.tsx MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Fábio Henriques --- src/pages/home/report/ReportActionItemCreated.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/home/report/ReportActionItemCreated.tsx b/src/pages/home/report/ReportActionItemCreated.tsx index edf8b1c9c48e..bc423c72afc7 100644 --- a/src/pages/home/report/ReportActionItemCreated.tsx +++ b/src/pages/home/report/ReportActionItemCreated.tsx @@ -22,7 +22,7 @@ type OnyxProps = { /** The report currently being looked at */ report: OnyxEntry; - /** The policy being used */ + /** The policy object for the current route */ policy: OnyxEntry; /** Personal details of all the users */ From 0d802ffca22643faab94f5828ddc608ba8d54481 Mon Sep 17 00:00:00 2001 From: Antony Kithinzi Date: Wed, 17 Jan 2024 22:49:20 +0300 Subject: [PATCH 211/391] Update src/pages/home/report/ReportActionItemCreated.tsx MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Fábio Henriques --- src/pages/home/report/ReportActionItemCreated.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/home/report/ReportActionItemCreated.tsx b/src/pages/home/report/ReportActionItemCreated.tsx index bc423c72afc7..47dc71cf43cd 100644 --- a/src/pages/home/report/ReportActionItemCreated.tsx +++ b/src/pages/home/report/ReportActionItemCreated.tsx @@ -18,7 +18,7 @@ import ONYXKEYS from '@src/ONYXKEYS'; import type {PersonalDetailsList, Policy, Report} from '@src/types/onyx'; import AnimatedEmptyStateBackground from './AnimatedEmptyStateBackground'; -type OnyxProps = { +type ReportActionItemCreatedOnyxProps = { /** The report currently being looked at */ report: OnyxEntry; From 34636db8d9d3288d7fcdfddea8ce37e52d7dfeca Mon Sep 17 00:00:00 2001 From: Antony Kithinzi Date: Wed, 17 Jan 2024 22:53:37 +0300 Subject: [PATCH 212/391] Update src/pages/home/report/ReportActionItemCreated.tsx MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Fábio Henriques --- src/pages/home/report/ReportActionItemCreated.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/home/report/ReportActionItemCreated.tsx b/src/pages/home/report/ReportActionItemCreated.tsx index 47dc71cf43cd..5ca74647fe4e 100644 --- a/src/pages/home/report/ReportActionItemCreated.tsx +++ b/src/pages/home/report/ReportActionItemCreated.tsx @@ -54,8 +54,8 @@ function ReportActionItemCreated(props: ReportActionItemCreatedProps) { return ( navigateToConciergeChatAndDeleteReport(props.report?.reportID ?? props.reportID)} needsOffscreenAlphaCompositing From 3be468ac7e28bc2fa18a16c98c212449a565783d Mon Sep 17 00:00:00 2001 From: Antony Kithinzi Date: Wed, 17 Jan 2024 20:28:11 +0000 Subject: [PATCH 213/391] fixing typescript checks --- src/pages/home/report/ReportActionItemCreated.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/pages/home/report/ReportActionItemCreated.tsx b/src/pages/home/report/ReportActionItemCreated.tsx index 5ca74647fe4e..82c6bebd9ba1 100644 --- a/src/pages/home/report/ReportActionItemCreated.tsx +++ b/src/pages/home/report/ReportActionItemCreated.tsx @@ -29,14 +29,13 @@ type ReportActionItemCreatedOnyxProps = { personalDetails: OnyxEntry; }; -type ReportActionItemCreatedProps = OnyxProps & { +type ReportActionItemCreatedProps = ReportActionItemCreatedOnyxProps & { /** The id of the report */ reportID: string; /** The id of the policy */ // eslint-disable-next-line react/no-unused-prop-types policyID: string; - }; function ReportActionItemCreated(props: ReportActionItemCreatedProps) { const styles = useThemeStyles(); @@ -95,7 +94,7 @@ function ReportActionItemCreated(props: ReportActionItemCreatedProps) { ReportActionItemCreated.displayName = 'ReportActionItemCreated'; -export default withOnyx({ +export default withOnyx({ report: { key: ({reportID}) => `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, selector: reportWithoutHasDraftSelector, From 0258fb46b0a715ffcad5591d2a600e8e41f82889 Mon Sep 17 00:00:00 2001 From: Pujan Date: Thu, 18 Jan 2024 14:54:07 +0530 Subject: [PATCH 214/391] removed jsdoc type --- src/pages/PrivateNotes/PrivateNotesListPage.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pages/PrivateNotes/PrivateNotesListPage.tsx b/src/pages/PrivateNotes/PrivateNotesListPage.tsx index 167a3523854c..60ea21610c0b 100644 --- a/src/pages/PrivateNotes/PrivateNotesListPage.tsx +++ b/src/pages/PrivateNotes/PrivateNotesListPage.tsx @@ -80,7 +80,6 @@ function PrivateNotesListPage({report, personalDetailsList, session}: PrivateNot /** * Returns a list of private notes on the given chat report - * @returns {Array} the menu item list */ const privateNotes = useMemo(() => { const privateNoteBrickRoadIndicator = (accountID: number) => report.privateNotes?.[accountID].errors ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : ''; From 6d44c76847b5e80302f33b5693fd94460297cd98 Mon Sep 17 00:00:00 2001 From: Pujan Date: Thu, 18 Jan 2024 14:59:58 +0530 Subject: [PATCH 215/391] prettier --- .../PrivateNotes/PrivateNotesEditPage.tsx | 23 ++++++----- .../PrivateNotes/PrivateNotesListPage.tsx | 39 +++++++++---------- 2 files changed, 30 insertions(+), 32 deletions(-) diff --git a/src/pages/PrivateNotes/PrivateNotesEditPage.tsx b/src/pages/PrivateNotes/PrivateNotesEditPage.tsx index db7a1299bb5c..b78431601898 100644 --- a/src/pages/PrivateNotes/PrivateNotesEditPage.tsx +++ b/src/pages/PrivateNotes/PrivateNotesEditPage.tsx @@ -2,10 +2,10 @@ import {useFocusEffect} from '@react-navigation/native'; import type {RouteProp} from '@react-navigation/native'; import ExpensiMark from 'expensify-common/lib/ExpensiMark'; import Str from 'expensify-common/lib/str'; -import type {OnyxCollection} from 'react-native-onyx'; import lodashDebounce from 'lodash/debounce'; import React, {useCallback, useMemo, useRef, useState} from 'react'; import {Keyboard} from 'react-native'; +import type {OnyxCollection} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import FormProvider from '@components/Form/FormProvider'; import InputWrapper from '@components/Form/InputWrapper'; @@ -24,23 +24,22 @@ import * as ReportActions from '@userActions/Report'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import type { PersonalDetails, Report } from '@src/types/onyx'; -import type { Note } from '@src/types/onyx/Report'; +import type {PersonalDetails, Report} from '@src/types/onyx'; +import type {Note} from '@src/types/onyx/Report'; type PrivateNotesEditPageOnyxProps = { /* Onyx Props */ /** All of the personal details for everyone */ - personalDetailsList: OnyxCollection, -} + personalDetailsList: OnyxCollection; +}; type PrivateNotesEditPageProps = PrivateNotesEditPageOnyxProps & { - /** The report currently being looked at */ - report: Report, + report: Report; route: RouteProp<{params: {reportID: string; accountID: string}}>; -} +}; function PrivateNotesEditPage({route, personalDetailsList, report}: PrivateNotesEditPageProps) { const styles = useThemeStyles(); @@ -97,7 +96,7 @@ function PrivateNotesEditPage({route, personalDetailsList, report}: PrivateNotes debouncedSavePrivateNote(''); Keyboard.dismiss(); - if(!Object.values({...report.privateNotes, [route.params.accountID]: {note: editedNote}}).some((item) => item.note)) { + if (!Object.values({...report.privateNotes, [route.params.accountID]: {note: editedNote}}).some((item) => item.note)) { ReportUtils.navigateToDetailsPage(report); } else { Navigation.goBack(ROUTES.PRIVATE_NOTES_LIST.getRoute(report.reportID)); @@ -132,7 +131,7 @@ function PrivateNotesEditPage({route, personalDetailsList, report}: PrivateNotes ReportActions.clearPrivateNotesError(report.reportID, Number(route.params.accountID))} style={[styles.mb3]} @@ -175,5 +174,5 @@ export default withReportAndPrivateNotesOrNotFound('privateNotes.title')( personalDetailsList: { key: ONYXKEYS.PERSONAL_DETAILS_LIST, }, - })(PrivateNotesEditPage) -); \ No newline at end of file + })(PrivateNotesEditPage), +); diff --git a/src/pages/PrivateNotes/PrivateNotesListPage.tsx b/src/pages/PrivateNotes/PrivateNotesListPage.tsx index 60ea21610c0b..ef0d279e3de3 100644 --- a/src/pages/PrivateNotes/PrivateNotesListPage.tsx +++ b/src/pages/PrivateNotes/PrivateNotesListPage.tsx @@ -2,6 +2,7 @@ import {useIsFocused} from '@react-navigation/native'; import React, {useEffect, useMemo} from 'react'; import {ScrollView} from 'react-native'; import {withOnyx} from 'react-native-onyx'; +import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; @@ -14,23 +15,22 @@ import withReportAndPrivateNotesOrNotFound from '@pages/home/report/withReportAn import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import type { PersonalDetails, Report, Session } from '@src/types/onyx'; -import type { OnyxCollection, OnyxEntry } from 'react-native-onyx'; +import type {PersonalDetails, Report, Session} from '@src/types/onyx'; type PrivateNotesListPageOnyxProps = { /* Onyx Props */ /** All of the personal details for everyone */ - personalDetailsList: OnyxCollection, + personalDetailsList: OnyxCollection; /** Session info for the currently logged in user. */ session: OnyxEntry; -} +}; type PrivateNotesListPageProps = PrivateNotesListPageOnyxProps & { /** The report currently being looked at */ report: Report; -} +}; function PrivateNotesListPage({report, personalDetailsList, session}: PrivateNotesListPageProps) { const styles = useThemeStyles(); @@ -42,13 +42,13 @@ function PrivateNotesListPage({report, personalDetailsList, session}: PrivateNot if (Object.values(report.privateNotes ?? {}).some((item) => item.note) || !isFocused) { return; } - Navigation.navigate(ROUTES.PRIVATE_NOTES_EDIT.getRoute(report.reportID, session.accountID)); + Navigation.navigate(ROUTES.PRIVATE_NOTES_EDIT.getRoute(report.reportID, session?.accountID ?? '')); }, CONST.ANIMATED_TRANSITION); return () => { clearTimeout(navigateToEditPageTimeout); }; - }, [report.privateNotes, report.reportID, session.accountID, isFocused]); + }, [report.privateNotes, report.reportID, session?.accountID, isFocused]); /** * Gets the menu item for each workspace @@ -82,18 +82,17 @@ function PrivateNotesListPage({report, personalDetailsList, session}: PrivateNot * Returns a list of private notes on the given chat report */ const privateNotes = useMemo(() => { - const privateNoteBrickRoadIndicator = (accountID: number) => report.privateNotes?.[accountID].errors ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : ''; - return Object.keys(report.privateNotes ?? {}) - .map((accountID: string) => { - const privateNote = report.privateNotes?.[Number(accountID)]; - return { - title: Number(session?.accountID) === Number(accountID) ? translate('privateNotes.myNote') : personalDetailsList?.[accountID]?.login ?? '', - action: () => Navigation.navigate(ROUTES.PRIVATE_NOTES_EDIT.getRoute(report.reportID, accountID)), - brickRoadIndicator: privateNoteBrickRoadIndicator(Number(accountID)), - note: privateNote?.note ?? '', - disabled: Number(session?.accountID) !== Number(accountID), - } - }) + const privateNoteBrickRoadIndicator = (accountID: number) => (report.privateNotes?.[accountID].errors ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : ''); + return Object.keys(report.privateNotes ?? {}).map((accountID: string) => { + const privateNote = report.privateNotes?.[Number(accountID)]; + return { + title: Number(session?.accountID) === Number(accountID) ? translate('privateNotes.myNote') : personalDetailsList?.[accountID]?.login ?? '', + action: () => Navigation.navigate(ROUTES.PRIVATE_NOTES_EDIT.getRoute(report.reportID, accountID)), + brickRoadIndicator: privateNoteBrickRoadIndicator(Number(accountID)), + note: privateNote?.note ?? '', + disabled: Number(session?.accountID) !== Number(accountID), + }; + }); }, [report, personalDetailsList, session, translate]); return ( @@ -122,5 +121,5 @@ export default withReportAndPrivateNotesOrNotFound('privateNotes.title')( session: { key: ONYXKEYS.SESSION, }, - })(PrivateNotesListPage) + })(PrivateNotesListPage), ); From e744f155c905e37f808065b5a4f7b324b2b79fd0 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Thu, 18 Jan 2024 12:15:44 +0100 Subject: [PATCH 216/391] Move onFixTheErrorsLinkPressed to a function --- src/components/Form/FormWrapper.tsx | 69 +++++++++++++++-------------- 1 file changed, 35 insertions(+), 34 deletions(-) diff --git a/src/components/Form/FormWrapper.tsx b/src/components/Form/FormWrapper.tsx index 77b34cb551aa..660f53f1427f 100644 --- a/src/components/Form/FormWrapper.tsx +++ b/src/components/Form/FormWrapper.tsx @@ -52,10 +52,41 @@ function FormWrapper({ scrollContextEnabled = false, }: FormWrapperProps) { const styles = useThemeStyles(); - const formRef = useRef(null); - const formContentRef = useRef(null); + const formRef = useRef(null); + const formContentRef = useRef(null); const errorMessage = useMemo(() => (formState ? ErrorUtils.getLatestErrorMessage(formState) : undefined), [formState]); + const onFixTheErrorsLinkPressed = useCallback(() => { + const errorFields = !isEmptyObject(errors) ? errors : formState?.errorFields ?? {}; + const focusKey = Object.keys(inputRefs.current ?? {}).find((key) => Object.keys(errorFields).includes(key)); + + if (!focusKey) { + return; + } + + const focusInput = inputRefs.current?.[focusKey]?.current; + + // Dismiss the keyboard for non-text fields by checking if the component has the isFocused method, as only TextInput has this method. + if (typeof focusInput?.isFocused !== 'function') { + Keyboard.dismiss(); + } + + // We subtract 10 to scroll slightly above the input + if (formContentRef.current) { + // We measure relative to the content root, not the scroll view, as that gives + // consistent results across mobile and web + focusInput?.measureLayout?.(formContentRef.current, (X: number, y: number) => + formRef.current?.scrollTo({ + y: y - 10, + animated: false, + }), + ); + } + + // Focus the input after scrolling, as on the Web it gives a slightly better visual result + focusInput?.focus?.(); + }, [errors, formState?.errorFields, inputRefs]); + const scrollViewContent = useCallback( (safeAreaPaddingBottomStyle: SafeAreaChildrenProps['safeAreaPaddingBottomStyle']) => ( { - const errorFields = !isEmptyObject(errors) ? errors : formState?.errorFields ?? {}; - const focusKey = Object.keys(inputRefs.current ?? {}).find((key) => Object.keys(errorFields).includes(key)); - - if (!focusKey) { - return; - } - - const inputRef = inputRefs.current?.[focusKey]; - const focusInput = inputRef && 'current' in inputRef ? inputRef.current : undefined; - - // Dismiss the keyboard for non-text fields by checking if the component has the isFocused method, as only TextInput has this method. - if (focusInput && typeof focusInput?.isFocused !== 'function') { - Keyboard.dismiss(); - } - - // We subtract 10 to scroll slightly above the input - if (formContentRef.current) { - // We measure relative to the content root, not the scroll view, as that gives - // consistent results across mobile and web - focusInput?.measureLayout?.(formContentRef.current, (X: number, y: number) => - formRef.current?.scrollTo({ - y: y - 10, - animated: false, - }), - ); - } - - // Focus the input after scrolling, as on the Web it gives a slightly better visual result - focusInput?.focus?.(); - }} + onFixTheErrorsLinkPressed={onFixTheErrorsLinkPressed} containerStyles={[styles.mh0, styles.mt5, styles.flex1, submitButtonStyles]} enabledWhenOffline={enabledWhenOffline} isSubmitActionDangerous={isSubmitActionDangerous} @@ -121,7 +122,6 @@ function FormWrapper({ formID, formState?.errorFields, formState?.isLoading, - inputRefs, isSubmitActionDangerous, isSubmitButtonVisible, onSubmit, @@ -131,6 +131,7 @@ function FormWrapper({ styles.mt5, submitButtonStyles, submitButtonText, + onFixTheErrorsLinkPressed, ], ); From bba2f1a085f4af16146703f6415d21bd0630366c Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Thu, 18 Jan 2024 12:16:23 +0100 Subject: [PATCH 217/391] Bring back DisplayNamePage --- ...DisplayNamePage.tsx => DisplayNamePage.js} | 69 ++++++++++++------- 1 file changed, 44 insertions(+), 25 deletions(-) rename src/pages/settings/Profile/{DisplayNamePage.tsx => DisplayNamePage.js} (65%) diff --git a/src/pages/settings/Profile/DisplayNamePage.tsx b/src/pages/settings/Profile/DisplayNamePage.js similarity index 65% rename from src/pages/settings/Profile/DisplayNamePage.tsx rename to src/pages/settings/Profile/DisplayNamePage.js index 75fd2b8dbe3c..8ea471283004 100644 --- a/src/pages/settings/Profile/DisplayNamePage.tsx +++ b/src/pages/settings/Profile/DisplayNamePage.js @@ -1,17 +1,17 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ +import lodashGet from 'lodash/get'; +import PropTypes from 'prop-types'; import React from 'react'; import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import FormProvider from '@components/Form/FormProvider'; import InputWrapper from '@components/Form/InputWrapper'; -import type {OnyxFormValuesFields} from '@components/Form/types'; import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; import Text from '@components/Text'; import TextInput from '@components/TextInput'; -import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalDetails'; -import useLocalize from '@hooks/useLocalize'; +import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsDefaultProps, withCurrentUserPersonalDetailsPropTypes} from '@components/withCurrentUserPersonalDetails'; +import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import compose from '@libs/compose'; import * as ErrorUtils from '@libs/ErrorUtils'; @@ -21,30 +21,46 @@ import * as PersonalDetails from '@userActions/PersonalDetails'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import type {Errors} from '@src/types/onyx/OnyxCommon'; -const updateDisplayName = (values: any) => { +const propTypes = { + ...withLocalizePropTypes, + ...withCurrentUserPersonalDetailsPropTypes, + isLoadingApp: PropTypes.bool, +}; + +const defaultProps = { + ...withCurrentUserPersonalDetailsDefaultProps, + isLoadingApp: true, +}; + +/** + * Submit form to update user's first and last name (and display name) + * @param {Object} values + * @param {String} values.firstName + * @param {String} values.lastName + */ +const updateDisplayName = (values) => { PersonalDetails.updateDisplayName(values.firstName.trim(), values.lastName.trim()); }; -function DisplayNamePage(props: any) { +function DisplayNamePage(props) { const styles = useThemeStyles(); - const {translate} = useLocalize(); const currentUserDetails = props.currentUserPersonalDetails || {}; /** - * @param values - * @param values.firstName - * @param values.lastName - * @returns - An object containing the errors for each inputID + * @param {Object} values + * @param {String} values.firstName + * @param {String} values.lastName + * @returns {Object} - An object containing the errors for each inputID */ - const validate = (values: OnyxFormValuesFields) => { - const errors: Errors = {}; + const validate = (values) => { + const errors = {}; + // First we validate the first name field if (!ValidationUtils.isValidDisplayName(values.firstName)) { ErrorUtils.addErrorMessage(errors, 'firstName', 'personalDetails.error.hasInvalidCharacter'); } - if (ValidationUtils.doesContainReservedWord(values.firstName, CONST.DISPLAY_NAME.RESERVED_FIRST_NAMES as unknown as string[])) { + if (ValidationUtils.doesContainReservedWord(values.firstName, CONST.DISPLAY_NAME.RESERVED_FIRST_NAMES)) { ErrorUtils.addErrorMessage(errors, 'firstName', 'personalDetails.error.containsReservedWord'); } @@ -62,7 +78,7 @@ function DisplayNamePage(props: any) { testID={DisplayNamePage.displayName} > Navigation.goBack(ROUTES.SETTINGS_PROFILE)} /> {props.isLoadingApp ? ( @@ -73,21 +89,21 @@ function DisplayNamePage(props: any) { formID={ONYXKEYS.FORMS.DISPLAY_NAME_FORM} validate={validate} onSubmit={updateDisplayName} - submitButtonText={translate('common.save')} + submitButtonText={props.translate('common.save')} enabledWhenOffline shouldValidateOnBlur shouldValidateOnChange > - {translate('displayNamePage.isShownOnProfile')} + {props.translate('displayNamePage.isShownOnProfile')} @@ -97,10 +113,10 @@ function DisplayNamePage(props: any) { InputComponent={TextInput} inputID="lastName" name="lname" - label={translate('common.lastName')} - aria-label={translate('common.lastName')} + label={props.translate('common.lastName')} + aria-label={props.translate('common.lastName')} role={CONST.ROLE.PRESENTATION} - defaultValue={currentUserDetails?.lastName ?? ''} + defaultValue={lodashGet(currentUserDetails, 'lastName', '')} maxLength={CONST.DISPLAY_NAME.MAX_LENGTH} spellCheck={false} /> @@ -111,13 +127,16 @@ function DisplayNamePage(props: any) { ); } +DisplayNamePage.propTypes = propTypes; +DisplayNamePage.defaultProps = defaultProps; DisplayNamePage.displayName = 'DisplayNamePage'; export default compose( + withLocalize, withCurrentUserPersonalDetails, withOnyx({ isLoadingApp: { - key: ONYXKEYS.IS_LOADING_APP as any, + key: ONYXKEYS.IS_LOADING_APP, }, }), )(DisplayNamePage); From 48b0867d05486bd6a713453110915fe95765bb1b Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Thu, 18 Jan 2024 12:29:16 +0100 Subject: [PATCH 218/391] Adjust the PR after CK review --- src/components/Form/FormProvider.tsx | 8 ++++---- src/components/Form/FormWrapper.tsx | 2 +- src/components/Form/types.ts | 8 ++++---- src/components/FormAlertWithSubmitButton.tsx | 2 +- src/components/ScrollViewWithContext.tsx | 4 +++- src/libs/actions/FormActions.ts | 5 +---- 6 files changed, 14 insertions(+), 15 deletions(-) diff --git a/src/components/Form/FormProvider.tsx b/src/components/Form/FormProvider.tsx index 8fe35c989c62..379a13f21711 100644 --- a/src/components/Form/FormProvider.tsx +++ b/src/components/Form/FormProvider.tsx @@ -12,7 +12,7 @@ import ONYXKEYS from '@src/ONYXKEYS'; import type {Form, Network} from '@src/types/onyx'; import type {FormValueType} from '@src/types/onyx/Form'; import type {Errors} from '@src/types/onyx/OnyxCommon'; -import {isEmptyObject, isNotEmptyObject} from '@src/types/utils/EmptyObject'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; import FormContext from './FormContext'; import FormWrapper from './FormWrapper'; import type {BaseInputProps, FormProps, InputRefs, OnyxFormKeyWithoutDraft, OnyxFormValues, OnyxFormValuesFields, RegisterInput, ValueTypeKey} from './types'; @@ -173,7 +173,7 @@ function FormProvider( Object.keys(inputRefs.current).forEach((inputID) => (touchedInputs.current[inputID] = true)); // Validate form and return early if any errors are found - if (isNotEmptyObject(onValidate(trimmedStringValues))) { + if (!isEmptyObject(onValidate(trimmedStringValues))) { return; } @@ -280,7 +280,7 @@ function FormProvider( onBlur: (event) => { // Only run validation when user proactively blurs the input. if (Visibility.isVisible() && Visibility.hasFocus()) { - const relatedTarget = 'nativeEvent' in event && 'relatedTarget' in event.nativeEvent && event?.nativeEvent?.relatedTarget; + const relatedTarget = 'relatedTarget' in event.nativeEvent && event?.nativeEvent?.relatedTarget; const relatedTargetId = relatedTarget && 'id' in relatedTarget && typeof relatedTarget.id === 'string' && relatedTarget.id; // We delay the validation in order to prevent Checkbox loss of focus when // the user is focusing a TextInput and proceeds to toggle a CheckBox in @@ -316,7 +316,7 @@ function FormProvider( return newState as Form; }); - if (inputProps.shouldSaveDraft && !(formID as string).includes('Draft')) { + if (inputProps.shouldSaveDraft && !formID.includes('Draft')) { FormActions.setDraftValues(formID as OnyxFormKeyWithoutDraft, {[inputKey]: value}); } inputProps.onValueChange?.(value, inputKey); diff --git a/src/components/Form/FormWrapper.tsx b/src/components/Form/FormWrapper.tsx index 660f53f1427f..45b2edf0badd 100644 --- a/src/components/Form/FormWrapper.tsx +++ b/src/components/Form/FormWrapper.tsx @@ -42,7 +42,7 @@ function FormWrapper({ errors, inputRefs, submitButtonText, - footerContent = null, + footerContent, isSubmitButtonVisible = true, style, submitButtonStyles, diff --git a/src/components/Form/types.ts b/src/components/Form/types.ts index 846322dd719b..a5825e209147 100644 --- a/src/components/Form/types.ts +++ b/src/components/Form/types.ts @@ -1,5 +1,5 @@ import type {FocusEvent, Key, MutableRefObject, ReactNode, Ref} from 'react'; -import type {NativeSyntheticEvent, StyleProp, TextInputFocusEventData, ViewStyle} from 'react-native'; +import type {GestureResponderEvent, NativeSyntheticEvent, StyleProp, TextInputFocusEventData, ViewStyle} from 'react-native'; import type {OnyxFormKey, OnyxValues} from '@src/ONYXKEYS'; import type Form from '@src/types/onyx/Form'; import type {BaseForm, FormValueType} from '@src/types/onyx/Form'; @@ -11,13 +11,13 @@ type MeasureLayoutOnSuccessCallback = (left: number, top: number, width: number, type BaseInputProps = { shouldSetTouchedOnBlurOnly?: boolean; onValueChange?: (value: unknown, key: string) => void; - onTouched?: (event: unknown) => void; + onTouched?: (event: GestureResponderEvent) => void; valueType?: ValueTypeKey; value?: FormValueType; defaultValue?: FormValueType; onBlur?: (event: FocusEvent | NativeSyntheticEvent) => void; - onPressOut?: (event: unknown) => void; - onPress?: (event: unknown) => void; + onPressOut?: (event: GestureResponderEvent) => void; + onPress?: (event: GestureResponderEvent) => void; shouldSaveDraft?: boolean; shouldUseDefaultValue?: boolean; key?: Key | null | undefined; diff --git a/src/components/FormAlertWithSubmitButton.tsx b/src/components/FormAlertWithSubmitButton.tsx index 512d2063dc0f..ae96aa6c5359 100644 --- a/src/components/FormAlertWithSubmitButton.tsx +++ b/src/components/FormAlertWithSubmitButton.tsx @@ -65,7 +65,7 @@ function FormAlertWithSubmitButton({ enabledWhenOffline = false, disablePressOnEnter = false, isSubmitActionDangerous = false, - footerContent = null, + footerContent, buttonStyles, buttonText, isAlertVisible, diff --git a/src/components/ScrollViewWithContext.tsx b/src/components/ScrollViewWithContext.tsx index de32ac3591a8..d8d63ba61012 100644 --- a/src/components/ScrollViewWithContext.tsx +++ b/src/components/ScrollViewWithContext.tsx @@ -54,7 +54,9 @@ function ScrollViewWithContext({onScroll, scrollEventThrottle, children, ...rest {...restProps} ref={scrollViewRef} onScroll={setContextScrollPosition} - scrollEventThrottle={scrollEventThrottle ?? MIN_SMOOTH_SCROLL_EVENT_THROTTLE} + // It's possible for scrollEventThrottle to be 0, so we must use "||" to fallback to MIN_SMOOTH_SCROLL_EVENT_THROTTLE. + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + scrollEventThrottle={scrollEventThrottle || MIN_SMOOTH_SCROLL_EVENT_THROTTLE} > {children} diff --git a/src/libs/actions/FormActions.ts b/src/libs/actions/FormActions.ts index 243fd062efeb..9daaa4fef20c 100644 --- a/src/libs/actions/FormActions.ts +++ b/src/libs/actions/FormActions.ts @@ -21,11 +21,8 @@ function setDraftValues(formID: OnyxFormKeyWithoutDraft, draftValues: NullishDee Onyx.merge(FormUtils.getDraftKey(formID), draftValues); } -/** - * @param formID - */ function clearDraftValues(formID: OnyxFormKeyWithoutDraft) { - Onyx.merge(FormUtils.getDraftKey(formID), {}); + Onyx.set(FormUtils.getDraftKey(formID), {}); } export {setDraftValues, setErrorFields, setErrors, setIsLoading, clearDraftValues}; From e8bbd93a94d57cbb31ba2ce95716ea6a4d8b50f5 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Thu, 18 Jan 2024 12:39:50 +0100 Subject: [PATCH 219/391] Add AddressSearch to valid inputs --- src/components/Form/InputWrapper.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/Form/InputWrapper.tsx b/src/components/Form/InputWrapper.tsx index e1f210b05ae9..559166aa5056 100644 --- a/src/components/Form/InputWrapper.tsx +++ b/src/components/Form/InputWrapper.tsx @@ -1,5 +1,6 @@ import type {ForwardedRef} from 'react'; import React, {forwardRef, useContext} from 'react'; +import type AddressSearch from '@components/AddressSearch'; import type AmountTextInput from '@components/AmountTextInput'; import type CheckboxWithLabel from '@components/CheckboxWithLabel'; import type Picker from '@components/Picker'; @@ -10,8 +11,8 @@ import FormContext from './FormContext'; import type {BaseInputProps, InputWrapperProps} from './types'; // TODO: Add remaining inputs here once these components are migrated to Typescript: -// AddressSearch | CountrySelector | StatePicker | DatePicker | EmojiPickerButtonDropdown | RoomNameInput | ValuePicker -type ValidInputs = typeof TextInput | typeof AmountTextInput | typeof SingleChoiceQuestion | typeof CheckboxWithLabel | typeof Picker; +// CountrySelector | StatePicker | DatePicker | EmojiPickerButtonDropdown | RoomNameInput | ValuePicker +type ValidInputs = typeof TextInput | typeof AmountTextInput | typeof SingleChoiceQuestion | typeof CheckboxWithLabel | typeof Picker | typeof AddressSearch; function InputWrapper( {InputComponent, inputID, valueType = 'string', ...rest}: InputWrapperProps, From 6c8201a9e763c66a473c6771e919ae8ae2f861de Mon Sep 17 00:00:00 2001 From: Pujan Date: Thu, 18 Jan 2024 18:07:18 +0530 Subject: [PATCH 220/391] ref type fixes --- src/libs/updateMultilineInputRange/types.ts | 2 +- .../PrivateNotes/PrivateNotesEditPage.tsx | 8 ++-- .../PrivateNotes/PrivateNotesListPage.tsx | 47 +++++++++---------- 3 files changed, 29 insertions(+), 28 deletions(-) diff --git a/src/libs/updateMultilineInputRange/types.ts b/src/libs/updateMultilineInputRange/types.ts index d1b134b09a99..ce8f553c51f8 100644 --- a/src/libs/updateMultilineInputRange/types.ts +++ b/src/libs/updateMultilineInputRange/types.ts @@ -1,5 +1,5 @@ import type {TextInput} from 'react-native'; -type UpdateMultilineInputRange = (input: HTMLInputElement | TextInput, shouldAutoFocus?: boolean) => void; +type UpdateMultilineInputRange = (input: HTMLInputElement | TextInput | null, shouldAutoFocus?: boolean) => void; export default UpdateMultilineInputRange; diff --git a/src/pages/PrivateNotes/PrivateNotesEditPage.tsx b/src/pages/PrivateNotes/PrivateNotesEditPage.tsx index b78431601898..c6095a318029 100644 --- a/src/pages/PrivateNotes/PrivateNotesEditPage.tsx +++ b/src/pages/PrivateNotes/PrivateNotesEditPage.tsx @@ -5,6 +5,7 @@ import Str from 'expensify-common/lib/str'; import lodashDebounce from 'lodash/debounce'; import React, {useCallback, useMemo, useRef, useState} from 'react'; import {Keyboard} from 'react-native'; +import type {TextInput as TextInputRN} from 'react-native'; import type {OnyxCollection} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import FormProvider from '@components/Form/FormProvider'; @@ -54,7 +55,6 @@ function PrivateNotesEditPage({route, personalDetailsList, report}: PrivateNotes /** * Save the draft of the private note. This debounced so that we're not ceaselessly saving your edit. Saving the draft * allows one to navigate somewhere else and come back to the private note and still have it in edit mode. - * @param {String} newDraft */ const debouncedSavePrivateNote = useMemo( () => @@ -65,8 +65,8 @@ function PrivateNotesEditPage({route, personalDetailsList, report}: PrivateNotes ); // To focus on the input field when the page loads - const privateNotesInput = useRef(null); - const focusTimeoutRef = useRef(null); + const privateNotesInput = useRef(null); + const focusTimeoutRef = useRef(null); useFocusEffect( useCallback(() => { @@ -115,6 +115,7 @@ function PrivateNotesEditPage({route, personalDetailsList, report}: PrivateNotes shouldShowBackButton onCloseButtonPress={() => Navigation.dismissModal()} /> + {/* @ts-expect-error TODO: Remove this once FormProvider (https://github.com/Expensify/App/issues/31972) is migrated to TypeScript. */} void; + brickRoadIndicator: ValueOf | undefined; + note: string; + disabled: boolean; +}; + function PrivateNotesListPage({report, personalDetailsList, session}: PrivateNotesListPageProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); @@ -53,28 +61,19 @@ function PrivateNotesListPage({report, personalDetailsList, session}: PrivateNot /** * Gets the menu item for each workspace */ - function getMenuItem(item, index: number) { - const keyTitle = item.translationKey ? translate(item.translationKey) : item.title; + function getMenuItem(item: NoteListItem) { return ( - - - + ); } @@ -82,7 +81,7 @@ function PrivateNotesListPage({report, personalDetailsList, session}: PrivateNot * Returns a list of private notes on the given chat report */ const privateNotes = useMemo(() => { - const privateNoteBrickRoadIndicator = (accountID: number) => (report.privateNotes?.[accountID].errors ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : ''); + const privateNoteBrickRoadIndicator = (accountID: number) => (report.privateNotes?.[accountID].errors ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined); return Object.keys(report.privateNotes ?? {}).map((accountID: string) => { const privateNote = report.privateNotes?.[Number(accountID)]; return { @@ -106,7 +105,7 @@ function PrivateNotesListPage({report, personalDetailsList, session}: PrivateNot onCloseButtonPress={() => Navigation.dismissModal()} /> {translate('privateNotes.personalNoteMessage')} - {privateNotes.map((item, index) => getMenuItem(item, index))} + {privateNotes.map((item) => getMenuItem(item))} ); } From 5d52ddad9d98d51c294cf80811e4b5c23d669d35 Mon Sep 17 00:00:00 2001 From: Pujan Date: Thu, 18 Jan 2024 18:29:54 +0530 Subject: [PATCH 221/391] added key --- src/pages/PrivateNotes/PrivateNotesListPage.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pages/PrivateNotes/PrivateNotesListPage.tsx b/src/pages/PrivateNotes/PrivateNotesListPage.tsx index 2cc958e730c1..550234a0707e 100644 --- a/src/pages/PrivateNotes/PrivateNotesListPage.tsx +++ b/src/pages/PrivateNotes/PrivateNotesListPage.tsx @@ -64,6 +64,7 @@ function PrivateNotesListPage({report, personalDetailsList, session}: PrivateNot function getMenuItem(item: NoteListItem) { return ( Date: Thu, 18 Jan 2024 14:47:04 +0100 Subject: [PATCH 222/391] Improve comments and InputWrapper props --- src/components/Form/FormProvider.tsx | 4 +--- src/components/Form/FormWrapper.tsx | 2 +- src/components/Form/InputWrapper.tsx | 4 ++-- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/components/Form/FormProvider.tsx b/src/components/Form/FormProvider.tsx index 379a13f21711..db9ea2e16d5a 100644 --- a/src/components/Form/FormProvider.tsx +++ b/src/components/Form/FormProvider.tsx @@ -17,8 +17,6 @@ import FormContext from './FormContext'; import FormWrapper from './FormWrapper'; import type {BaseInputProps, FormProps, InputRefs, OnyxFormKeyWithoutDraft, OnyxFormValues, OnyxFormValuesFields, RegisterInput, ValueTypeKey} from './types'; -// In order to prevent Checkbox focus loss when the user are focusing a TextInput and proceeds to toggle a CheckBox in web and mobile web. - // In order to prevent Checkbox focus loss when the user are focusing a TextInput and proceeds to toggle a CheckBox in web and mobile web. // 200ms delay was chosen as a result of empirical testing. // More details: https://github.com/Expensify/App/pull/16444#issuecomment-1482983426 @@ -350,7 +348,7 @@ export default withOnyx({ network: { key: ONYXKEYS.NETWORK, }, - // withOnyx typings are not able to handle such generic cases like this one, since it's a generic component we had to cast the keys to any + // withOnyx typings are not able to handle such generic cases like this one, since it's a generic component we need to cast the keys to any formState: { // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-explicit-any key: ({formID}) => formID as any, diff --git a/src/components/Form/FormWrapper.tsx b/src/components/Form/FormWrapper.tsx index dc0c6d5221a8..c12c9d1b5a44 100644 --- a/src/components/Form/FormWrapper.tsx +++ b/src/components/Form/FormWrapper.tsx @@ -168,7 +168,7 @@ FormWrapper.displayName = 'FormWrapper'; export default withOnyx({ formState: { - // withOnyx typings are not able to handle such generic cases like this one, since it's a generic component we had to cast the keys to any + // withOnyx typings are not able to handle such generic cases like this one, since it's a generic component we need to cast the keys to any // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-explicit-any key: (props) => props.formID as any, }, diff --git a/src/components/Form/InputWrapper.tsx b/src/components/Form/InputWrapper.tsx index 559166aa5056..b4cc5aab2d94 100644 --- a/src/components/Form/InputWrapper.tsx +++ b/src/components/Form/InputWrapper.tsx @@ -1,4 +1,4 @@ -import type {ForwardedRef} from 'react'; +import type {ComponentProps, ForwardedRef} from 'react'; import React, {forwardRef, useContext} from 'react'; import type AddressSearch from '@components/AddressSearch'; import type AmountTextInput from '@components/AmountTextInput'; @@ -15,7 +15,7 @@ import type {BaseInputProps, InputWrapperProps} from './types'; type ValidInputs = typeof TextInput | typeof AmountTextInput | typeof SingleChoiceQuestion | typeof CheckboxWithLabel | typeof Picker | typeof AddressSearch; function InputWrapper( - {InputComponent, inputID, valueType = 'string', ...rest}: InputWrapperProps, + {InputComponent, inputID, valueType = 'string', ...rest}: InputWrapperProps>, ref: ForwardedRef, ) { const {registerInput} = useContext(FormContext); From 05fa761f58159140793e4ccc70e0cc2ef5848ae8 Mon Sep 17 00:00:00 2001 From: greg-schroeder Date: Thu, 18 Jan 2024 06:10:15 -0800 Subject: [PATCH 223/391] Update Budgets.md --- .../expensify-classic/workspace-and-domain-settings/Budgets.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/articles/expensify-classic/workspace-and-domain-settings/Budgets.md b/docs/articles/expensify-classic/workspace-and-domain-settings/Budgets.md index 30adac589dc0..2b95bfab31d6 100644 --- a/docs/articles/expensify-classic/workspace-and-domain-settings/Budgets.md +++ b/docs/articles/expensify-classic/workspace-and-domain-settings/Budgets.md @@ -44,7 +44,7 @@ Expensify’s Budgets feature allows you to: {% include faq-begin.md %} ## Can I import budgets as a CSV? -At this time, you cannot import budgets via CSV since we don’t import categories or tags from direct accounting integrations. +At this time, you can't import budgets via CSV. ## When will I be notified as a budget is hit? Notifications are sent twice: From 008807d7c5baf480ccecbdbe899a0f8691e816fd Mon Sep 17 00:00:00 2001 From: greg-schroeder Date: Thu, 18 Jan 2024 06:30:04 -0800 Subject: [PATCH 224/391] Update Budgets.md --- .../expensify-classic/workspace-and-domain-settings/Budgets.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/articles/expensify-classic/workspace-and-domain-settings/Budgets.md b/docs/articles/expensify-classic/workspace-and-domain-settings/Budgets.md index 2b95bfab31d6..b3f0ad3c6f6f 100644 --- a/docs/articles/expensify-classic/workspace-and-domain-settings/Budgets.md +++ b/docs/articles/expensify-classic/workspace-and-domain-settings/Budgets.md @@ -44,7 +44,7 @@ Expensify’s Budgets feature allows you to: {% include faq-begin.md %} ## Can I import budgets as a CSV? -At this time, you can't import budgets via CSV. +At this time, you cannot import budgets via CSV. ## When will I be notified as a budget is hit? Notifications are sent twice: From eb8bb85fe2b8b8a04a5106308fe8739b54ecb145 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Thu, 18 Jan 2024 15:52:30 +0100 Subject: [PATCH 225/391] Final touches to InputWrapper --- src/components/Form/InputWrapper.tsx | 18 +++--------------- src/components/Form/types.ts | 28 ++++++++++++++++++++++------ 2 files changed, 25 insertions(+), 21 deletions(-) diff --git a/src/components/Form/InputWrapper.tsx b/src/components/Form/InputWrapper.tsx index b4cc5aab2d94..68dd7219f96a 100644 --- a/src/components/Form/InputWrapper.tsx +++ b/src/components/Form/InputWrapper.tsx @@ -1,23 +1,11 @@ -import type {ComponentProps, ForwardedRef} from 'react'; +import type {ForwardedRef} from 'react'; import React, {forwardRef, useContext} from 'react'; -import type AddressSearch from '@components/AddressSearch'; -import type AmountTextInput from '@components/AmountTextInput'; -import type CheckboxWithLabel from '@components/CheckboxWithLabel'; -import type Picker from '@components/Picker'; -import type SingleChoiceQuestion from '@components/SingleChoiceQuestion'; import TextInput from '@components/TextInput'; import type {BaseTextInputRef} from '@components/TextInput/BaseTextInput/types'; import FormContext from './FormContext'; -import type {BaseInputProps, InputWrapperProps} from './types'; +import type {InputWrapperProps, ValidInputs} from './types'; -// TODO: Add remaining inputs here once these components are migrated to Typescript: -// CountrySelector | StatePicker | DatePicker | EmojiPickerButtonDropdown | RoomNameInput | ValuePicker -type ValidInputs = typeof TextInput | typeof AmountTextInput | typeof SingleChoiceQuestion | typeof CheckboxWithLabel | typeof Picker | typeof AddressSearch; - -function InputWrapper( - {InputComponent, inputID, valueType = 'string', ...rest}: InputWrapperProps>, - ref: ForwardedRef, -) { +function InputWrapper({InputComponent, inputID, valueType = 'string', ...rest}: InputWrapperProps, ref: ForwardedRef) { const {registerInput} = useContext(FormContext); // There are inputs that don't have onBlur methods, to simulate the behavior of onBlur in e.g. checkbox, we had to // use different methods like onPress. This introduced a problem that inputs that have the onBlur method were diff --git a/src/components/Form/types.ts b/src/components/Form/types.ts index ecbb6a90d458..1418c900c022 100644 --- a/src/components/Form/types.ts +++ b/src/components/Form/types.ts @@ -1,9 +1,24 @@ -import type {FocusEvent, Key, MutableRefObject, ReactNode, Ref} from 'react'; +import type {ComponentProps, FocusEvent, Key, MutableRefObject, ReactNode, Ref} from 'react'; import type {GestureResponderEvent, NativeSyntheticEvent, StyleProp, TextInputFocusEventData, ViewStyle} from 'react-native'; +import type AddressSearch from '@components/AddressSearch'; +import type AmountTextInput from '@components/AmountTextInput'; +import type CheckboxWithLabel from '@components/CheckboxWithLabel'; +import type Picker from '@components/Picker'; +import type SingleChoiceQuestion from '@components/SingleChoiceQuestion'; +import type TextInput from '@components/TextInput'; import type {OnyxFormKey, OnyxValues} from '@src/ONYXKEYS'; import type Form from '@src/types/onyx/Form'; import type {BaseForm, FormValueType} from '@src/types/onyx/Form'; +/** + * This type specifies all the inputs that can be used with `InputWrapper` component. Make sure to update it + * when adding new inputs or removing old ones. + * + * TODO: Add remaining inputs here once these components are migrated to Typescript: + * CountrySelector | StatePicker | DatePicker | EmojiPickerButtonDropdown | RoomNameInput | ValuePicker + */ +type ValidInputs = typeof TextInput | typeof AmountTextInput | typeof SingleChoiceQuestion | typeof CheckboxWithLabel | typeof Picker | typeof AddressSearch; + type ValueTypeKey = 'string' | 'boolean' | 'date'; type MeasureLayoutOnSuccessCallback = (left: number, top: number, width: number, height: number) => void; @@ -27,10 +42,11 @@ type BaseInputProps = { focus?: () => void; }; -type InputWrapperProps = TInputProps & { - InputComponent: TInput; - inputID: string; -}; +type InputWrapperProps = BaseInputProps & + ComponentProps & { + InputComponent: TInput; + inputID: string; + }; type ExcludeDraft = T extends `${string}Draft` ? never : T; type OnyxFormKeyWithoutDraft = ExcludeDraft; @@ -74,4 +90,4 @@ type RegisterInput = (inputID: keyof Form, i type InputRefs = Record>; -export type {InputWrapperProps, FormProps, RegisterInput, BaseInputProps, ValueTypeKey, OnyxFormValues, OnyxFormValuesFields, InputRefs, OnyxFormKeyWithoutDraft}; +export type {InputWrapperProps, FormProps, RegisterInput, ValidInputs, BaseInputProps, ValueTypeKey, OnyxFormValues, OnyxFormValuesFields, InputRefs, OnyxFormKeyWithoutDraft}; From 1a94efad3cdfecf94266dea234835cabc6e4b7ae Mon Sep 17 00:00:00 2001 From: brunovjk Date: Thu, 18 Jan 2024 12:07:53 -0300 Subject: [PATCH 226/391] Resolve conflict over Migrate 'SelectionList' --- src/components/SelectionList/BaseSelectionList.js | 4 +++- src/components/SelectionList/selectionListPropTypes.js | 3 +++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/components/SelectionList/BaseSelectionList.js b/src/components/SelectionList/BaseSelectionList.js index ea9ee3a0012c..0ad7d3621660 100644 --- a/src/components/SelectionList/BaseSelectionList.js +++ b/src/components/SelectionList/BaseSelectionList.js @@ -45,6 +45,7 @@ function BaseSelectionList({ inputMode = CONST.INPUT_MODE.TEXT, onChangeText, initiallyFocusedOptionKey = '', + isLoadingNewOptions = false, onScroll, onScrollBeginDrag, headerMessage = '', @@ -428,10 +429,11 @@ function BaseSelectionList({ spellCheck={false} onSubmitEditing={selectFocusedOption} blurOnSubmit={Boolean(flattenedSections.allOptions.length)} + isLoading={isLoadingNewOptions} /> )} - {Boolean(headerMessage) && ( + {!isLoadingNewOptions && !!headerMessage && ( {headerMessage} diff --git a/src/components/SelectionList/selectionListPropTypes.js b/src/components/SelectionList/selectionListPropTypes.js index 7914ecc8572c..254f3e22b684 100644 --- a/src/components/SelectionList/selectionListPropTypes.js +++ b/src/components/SelectionList/selectionListPropTypes.js @@ -151,6 +151,9 @@ const propTypes = { /** Item `keyForList` to focus initially */ initiallyFocusedOptionKey: PropTypes.string, + /** Whether we are loading new options */ + isLoadingNewOptions: PropTypes.bool, + /** Callback to fire when the list is scrolled */ onScroll: PropTypes.func, From f2fc0de7c68ac5e222cc408d0c3582c00dfdfc75 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Thu, 18 Jan 2024 16:09:49 +0100 Subject: [PATCH 227/391] revert rows prop change --- src/components/Composer/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Composer/index.tsx b/src/components/Composer/index.tsx index f3a471335f0d..3c2caf020ef7 100755 --- a/src/components/Composer/index.tsx +++ b/src/components/Composer/index.tsx @@ -368,7 +368,7 @@ function Composer( /* eslint-disable-next-line react/jsx-props-no-spreading */ {...props} onSelectionChange={addCursorPositionToSelectionChange} - numberOfLines={numberOfLines} + rows={numberOfLines} disabled={isDisabled} onKeyPress={handleKeyPress} onFocus={(e) => { From 0d6b7a5aca021eea4be0e8aa1385db6f67d1ec74 Mon Sep 17 00:00:00 2001 From: brunovjk Date: Thu, 18 Jan 2024 12:12:48 -0300 Subject: [PATCH 228/391] Resolve conflict over Migrate 'SelectionList' --- src/components/SelectionList/BaseSelectionList.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/SelectionList/BaseSelectionList.js b/src/components/SelectionList/BaseSelectionList.js index 0ad7d3621660..36d56813e917 100644 --- a/src/components/SelectionList/BaseSelectionList.js +++ b/src/components/SelectionList/BaseSelectionList.js @@ -433,7 +433,7 @@ function BaseSelectionList({ /> )} - {!isLoadingNewOptions && !!headerMessage && ( + {!isLoadingNewOptions && Boolean(headerMessage) && ( {headerMessage} From bc997b31897023cea6714e4aab62a451e7e01699 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Thu, 18 Jan 2024 17:17:13 +0100 Subject: [PATCH 229/391] Fix reimbursment form bug --- src/components/Form/FormProvider.tsx | 2 +- src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/Form/FormProvider.tsx b/src/components/Form/FormProvider.tsx index db9ea2e16d5a..10e4952a7896 100644 --- a/src/components/Form/FormProvider.tsx +++ b/src/components/Form/FormProvider.tsx @@ -278,7 +278,7 @@ function FormProvider( onBlur: (event) => { // Only run validation when user proactively blurs the input. if (Visibility.isVisible() && Visibility.hasFocus()) { - const relatedTarget = 'relatedTarget' in event.nativeEvent && event?.nativeEvent?.relatedTarget; + const relatedTarget = event && 'relatedTarget' in event.nativeEvent && event?.nativeEvent?.relatedTarget; const relatedTargetId = relatedTarget && 'id' in relatedTarget && typeof relatedTarget.id === 'string' && relatedTarget.id; // We delay the validation in order to prevent Checkbox loss of focus when // the user is focusing a TextInput and proceeds to toggle a CheckBox in diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx index b0f33af0ce2e..7a9f92ec7996 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx @@ -64,7 +64,7 @@ function createModalStackNavigator(screens: getComponent={(screens as Required)[name as Screen]} /> ))} - + ); } From e1aa79263df57e6c677d6c5d50200883e29bf3fe Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Thu, 18 Jan 2024 17:27:20 +0100 Subject: [PATCH 230/391] Fix prettier --- src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx index 7a9f92ec7996..b0f33af0ce2e 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx @@ -64,7 +64,7 @@ function createModalStackNavigator(screens: getComponent={(screens as Required)[name as Screen]} /> ))} - + ); } From 8c0eee24ebe61eaffc271b680d944b2d35b401ac Mon Sep 17 00:00:00 2001 From: brunovjk Date: Thu, 18 Jan 2024 13:28:39 -0300 Subject: [PATCH 231/391] Run prettier --- src/components/SelectionList/BaseListItem.js | 2 +- .../SelectionList/BaseSelectionList.js | 2 +- src/components/SelectionList/RadioListItem.js | 2 +- src/components/SelectionList/UserListItem.js | 2 +- src/components/SelectionList/index.android.js | 2 +- src/components/SelectionList/index.ios.js | 2 +- src/components/SelectionList/index.js | 2 +- .../SelectionList/selectionListPropTypes.js | 2 +- ...yForRefactorRequestParticipantsSelector.js | 4 +- .../step/IOURequestStepParticipants.js | 2 +- .../MoneyRequestParticipantsPage.js | 3 +- .../MoneyRequestParticipantsSelector.js | 43 ++++++------------- 12 files changed, 24 insertions(+), 44 deletions(-) diff --git a/src/components/SelectionList/BaseListItem.js b/src/components/SelectionList/BaseListItem.js index ad19e7dcad76..6a067ea0fe3d 100644 --- a/src/components/SelectionList/BaseListItem.js +++ b/src/components/SelectionList/BaseListItem.js @@ -142,4 +142,4 @@ function BaseListItem({ BaseListItem.displayName = 'BaseListItem'; BaseListItem.propTypes = baseListItemPropTypes; -export default BaseListItem; \ No newline at end of file +export default BaseListItem; diff --git a/src/components/SelectionList/BaseSelectionList.js b/src/components/SelectionList/BaseSelectionList.js index 36d56813e917..2d209ef573c3 100644 --- a/src/components/SelectionList/BaseSelectionList.js +++ b/src/components/SelectionList/BaseSelectionList.js @@ -513,4 +513,4 @@ function BaseSelectionList({ BaseSelectionList.displayName = 'BaseSelectionList'; BaseSelectionList.propTypes = propTypes; -export default withKeyboardState(BaseSelectionList); \ No newline at end of file +export default withKeyboardState(BaseSelectionList); diff --git a/src/components/SelectionList/RadioListItem.js b/src/components/SelectionList/RadioListItem.js index 5b465f9efe58..2de0c96932ea 100644 --- a/src/components/SelectionList/RadioListItem.js +++ b/src/components/SelectionList/RadioListItem.js @@ -41,4 +41,4 @@ function RadioListItem({item, showTooltip, textStyles, alternateTextStyles}) { RadioListItem.displayName = 'RadioListItem'; RadioListItem.propTypes = radioListItemPropTypes; -export default RadioListItem; \ No newline at end of file +export default RadioListItem; diff --git a/src/components/SelectionList/UserListItem.js b/src/components/SelectionList/UserListItem.js index 1513f2601b9f..a842f19858f2 100644 --- a/src/components/SelectionList/UserListItem.js +++ b/src/components/SelectionList/UserListItem.js @@ -54,4 +54,4 @@ function UserListItem({item, textStyles, alternateTextStyles, showTooltip, style UserListItem.displayName = 'UserListItem'; UserListItem.propTypes = userListItemPropTypes; -export default UserListItem; \ No newline at end of file +export default UserListItem; diff --git a/src/components/SelectionList/index.android.js b/src/components/SelectionList/index.android.js index 5c98df733c02..53d5b6bbce06 100644 --- a/src/components/SelectionList/index.android.js +++ b/src/components/SelectionList/index.android.js @@ -14,4 +14,4 @@ const SelectionList = forwardRef((props, ref) => ( SelectionList.displayName = 'SelectionList'; -export default SelectionList; \ No newline at end of file +export default SelectionList; diff --git a/src/components/SelectionList/index.ios.js b/src/components/SelectionList/index.ios.js index b03aae215f9d..7f2a282aeb89 100644 --- a/src/components/SelectionList/index.ios.js +++ b/src/components/SelectionList/index.ios.js @@ -13,4 +13,4 @@ const SelectionList = forwardRef((props, ref) => ( SelectionList.displayName = 'SelectionList'; -export default SelectionList; \ No newline at end of file +export default SelectionList; diff --git a/src/components/SelectionList/index.js b/src/components/SelectionList/index.js index f0fe27acbe2d..24ea60d29be5 100644 --- a/src/components/SelectionList/index.js +++ b/src/components/SelectionList/index.js @@ -43,4 +43,4 @@ const SelectionList = forwardRef((props, ref) => { SelectionList.displayName = 'SelectionList'; -export default SelectionList; \ No newline at end of file +export default SelectionList; diff --git a/src/components/SelectionList/selectionListPropTypes.js b/src/components/SelectionList/selectionListPropTypes.js index 254f3e22b684..b0c5dd37867e 100644 --- a/src/components/SelectionList/selectionListPropTypes.js +++ b/src/components/SelectionList/selectionListPropTypes.js @@ -203,4 +203,4 @@ const propTypes = { rightHandSideComponent: PropTypes.oneOfType([PropTypes.func, PropTypes.node]), }; -export {propTypes, baseListItemPropTypes, radioListItemPropTypes, userListItemPropTypes}; \ No newline at end of file +export {propTypes, baseListItemPropTypes, radioListItemPropTypes, userListItemPropTypes}; diff --git a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js index 2554b5933c6a..6da8524934d3 100644 --- a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js +++ b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js @@ -1,6 +1,6 @@ import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; -import React, {useCallback, useEffect,useMemo} from 'react'; +import React, {useCallback, useEffect, useMemo} from 'react'; import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; @@ -369,4 +369,4 @@ export default withOnyx({ key: ONYXKEYS.IS_SEARCHING_FOR_REPORTS, initWithStoredValues: false, }, -})(MoneyTemporaryForRefactorRequestParticipantsSelector); \ No newline at end of file +})(MoneyTemporaryForRefactorRequestParticipantsSelector); diff --git a/src/pages/iou/request/step/IOURequestStepParticipants.js b/src/pages/iou/request/step/IOURequestStepParticipants.js index aad85307b3e4..4846c3c4c8a4 100644 --- a/src/pages/iou/request/step/IOURequestStepParticipants.js +++ b/src/pages/iou/request/step/IOURequestStepParticipants.js @@ -105,4 +105,4 @@ IOURequestStepParticipants.displayName = 'IOURequestStepParticipants'; IOURequestStepParticipants.propTypes = propTypes; IOURequestStepParticipants.defaultProps = defaultProps; -export default compose(withWritableReportOrNotFound, withFullTransactionOrNotFound)(IOURequestStepParticipants); \ No newline at end of file +export default compose(withWritableReportOrNotFound, withFullTransactionOrNotFound)(IOURequestStepParticipants); diff --git a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsPage.js b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsPage.js index 76b7b80c6306..216154be9cd4 100644 --- a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsPage.js +++ b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsPage.js @@ -130,7 +130,7 @@ function MoneyRequestParticipantsPage({iou, selectedTab, route, transaction}) { shouldEnableMaxHeight={DeviceCapabilities.canUseTouchScreen()} testID={MoneyRequestParticipantsPage.displayName} > - {({didScreenTransitionEnd, safeAreaPaddingBottomStyle}) => ( + {({safeAreaPaddingBottomStyle}) => ( )} diff --git a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js index 76e97b140506..9567b17ecdf5 100755 --- a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js +++ b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js @@ -1,6 +1,6 @@ import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; -import React, {useCallback, useEffect,useMemo} from 'react'; +import React, {useCallback, useMemo, useState} from 'react'; import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; @@ -11,18 +11,15 @@ import {PressableWithFeedback} from '@components/Pressable'; import ReferralProgramCTA from '@components/ReferralProgramCTA'; import SelectCircle from '@components/SelectCircle'; import SelectionList from '@components/SelectionList'; -import useDebouncedState from '@hooks/useDebouncedState'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useThemeStyles from '@hooks/useThemeStyles'; import * as Report from '@libs/actions/Report'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import * as OptionsListUtils from '@libs/OptionsListUtils'; -import * as ReportUtils from '@libs/ReportUtils'; import reportPropTypes from '@pages/reportPropTypes'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import {isNotEmptyObject} from '@src/types/utils/EmptyObject'; const propTypes = { /** Beta features list */ @@ -62,9 +59,6 @@ const propTypes = { /** Whether we are searching for reports in the server */ isSearchingForReports: PropTypes.bool, - - /** Whether the parent screen transition has ended */ - didScreenTransitionEnd: PropTypes.bool, }; const defaultProps = { @@ -74,7 +68,6 @@ const defaultProps = { betas: [], isDistanceRequest: false, isSearchingForReports: false, - didScreenTransitionEnd: false, }; function MoneyRequestParticipantsSelector({ @@ -88,24 +81,16 @@ function MoneyRequestParticipantsSelector({ iouType, isDistanceRequest, isSearchingForReports, - didScreenTransitionEnd, }) { const {translate} = useLocalize(); const styles = useThemeStyles(); - const [searchTerm, debouncedSearchTerm, setSearchTerm] = useDebouncedState(''); + const [searchTerm, setSearchTerm] = useState(''); const {isOffline} = useNetwork(); const personalDetails = usePersonalDetails(); const offlineMessage = isOffline ? `${translate('common.youAppearToBeOffline')} ${translate('search.resultsAreLimited')}` : ''; const newChatOptions = useMemo(() => { - if (!didScreenTransitionEnd) { - return { - recentReports: {}, - personalDetails: {}, - userToInvite: {}, - }; - } const chatOptions = OptionsListUtils.getFilteredOptions( reports, personalDetails, @@ -136,7 +121,7 @@ function MoneyRequestParticipantsSelector({ personalDetails: chatOptions.personalDetails, userToInvite: chatOptions.userToInvite, }; - }, [betas, didScreenTransitionEnd, reports, participants, personalDetails, searchTerm, iouType, isDistanceRequest]); + }, [betas, reports, participants, personalDetails, searchTerm, iouType, isDistanceRequest]); const maxParticipantsReached = participants.length === CONST.REPORT.MAXIMUM_PARTICIPANTS; @@ -181,7 +166,7 @@ function MoneyRequestParticipantsSelector({ }); indexOffset += newChatOptions.personalDetails.length; - if (isNotEmptyObject(newChatOptions.userToInvite) && !OptionsListUtils.isCurrentUser(newChatOptions.userToInvite)) { + if (newChatOptions.userToInvite && !OptionsListUtils.isCurrentUser(newChatOptions.userToInvite)) { newSections.push({ title: undefined, data: _.map([newChatOptions.userToInvite], (participant) => { @@ -273,12 +258,11 @@ function MoneyRequestParticipantsSelector({ [maxParticipantsReached, newChatOptions.personalDetails.length, newChatOptions.recentReports.length, newChatOptions.userToInvite, participants, searchTerm], ); - useEffect(() => { - if (!debouncedSearchTerm.length) { - return; - } - Report.searchInServer(debouncedSearchTerm); - }, [debouncedSearchTerm]); + // When search term updates we will fetch any reports + const setSearchTermAndSearchInServer = useCallback((text = '') => { + Report.searchInServer(text); + setSearchTerm(text); + }, []); // Right now you can't split a request with a workspace and other additional participants // This is getting properly fixed in https://github.com/Expensify/App/issues/27508, but as a stop-gap to prevent @@ -357,24 +341,21 @@ function MoneyRequestParticipantsSelector({ [addParticipantToSelection, isAllowedToSplit, styles, translate], ); - const isOptionsDataReady = useMemo(() => ReportUtils.isReportDataReady() && OptionsListUtils.isPersonalDetailsReady(personalDetails), [personalDetails]); - return ( 0 ? safeAreaPaddingBottomStyle : {}]}> ); From 9194fd136a405e4d5532d33729f89a44d6345295 Mon Sep 17 00:00:00 2001 From: Fitsum Abebe Date: Thu, 18 Jan 2024 19:30:41 +0300 Subject: [PATCH 232/391] implemented useCallback Ref --- src/hooks/useCallbackRef.ts | 13 +++++++++++++ src/pages/home/report/ReportActionsList.js | 16 +++++++--------- 2 files changed, 20 insertions(+), 9 deletions(-) create mode 100644 src/hooks/useCallbackRef.ts diff --git a/src/hooks/useCallbackRef.ts b/src/hooks/useCallbackRef.ts new file mode 100644 index 000000000000..075cbe08bbbc --- /dev/null +++ b/src/hooks/useCallbackRef.ts @@ -0,0 +1,13 @@ +import {useEffect, useMemo, useRef} from 'react'; + +const useCallbackRef = unknown>(callback: T): T => { + const calbackRef = useRef(callback); + + useEffect(() => { + calbackRef.current = callback; + }); + + return useMemo(() => ((...args) => calbackRef.current(...args)) as T, []); +}; + +export default useCallbackRef; diff --git a/src/pages/home/report/ReportActionsList.js b/src/pages/home/report/ReportActionsList.js index 61e2e1ce14bb..a4fe94183695 100644 --- a/src/pages/home/report/ReportActionsList.js +++ b/src/pages/home/report/ReportActionsList.js @@ -9,6 +9,7 @@ import InvertedFlatList from '@components/InvertedFlatList'; import {withPersonalDetails} from '@components/OnyxProvider'; import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsDefaultProps, withCurrentUserPersonalDetailsPropTypes} from '@components/withCurrentUserPersonalDetails'; import withWindowDimensions, {windowDimensionsPropTypes} from '@components/withWindowDimensions'; +import useCallbackRef from '@hooks/useCallbackRef'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useReportScrollManager from '@hooks/useReportScrollManager'; @@ -167,7 +168,6 @@ function ReportActionsList({ const reportActionSize = useRef(sortedVisibleReportActions.length); const previousLastIndex = useRef(lastActionIndex); - const visibilityCallback = useRef(() => {}); const linkedReportActionID = lodashGet(route, 'params.reportActionID', ''); @@ -387,7 +387,7 @@ function ReportActionsList({ [currentUnreadMarker, sortedVisibleReportActions, report.reportID, messageManuallyMarkedUnread], ); - const calculateUnreadMarker = () => { + const calculateUnreadMarker = useCallback(() => { // Iterate through the report actions and set appropriate unread marker. // This is to avoid a warning of: // Cannot update a component (ReportActionsList) while rendering a different component (CellRenderer). @@ -405,15 +405,13 @@ function ReportActionsList({ if (!markerFound) { setCurrentUnreadMarker(null); } - }; + }, [sortedVisibleReportActions, shouldDisplayNewMarker, currentUnreadMarker, report.reportID]); useEffect(() => { calculateUnreadMarker(); + }, [calculateUnreadMarker, report.lastReadTime, messageManuallyMarkedUnread]); - /* eslint-disable-next-line react-hooks/exhaustive-deps */ - }, [sortedVisibleReportActions, report.lastReadTime, report.reportID, messageManuallyMarkedUnread, shouldDisplayNewMarker, currentUnreadMarker]); - - visibilityCallback.current = () => { + const visibilityCallback = useCallbackRef(() => { if (!Visibility.isVisible() || scrollingVerticalOffset.current >= MSG_VISIBLE_THRESHOLD || !ReportUtils.isUnread(report) || messageManuallyMarkedUnread) { return; } @@ -423,10 +421,10 @@ function ReportActionsList({ setCurrentUnreadMarker(null); cacheUnreadMarkers.delete(report.reportID); calculateUnreadMarker(); - }; + }); useEffect(() => { - const unsubscribeVisibilityListener = Visibility.onVisibilityChange(() => visibilityCallback.current()); + const unsubscribeVisibilityListener = Visibility.onVisibilityChange(visibilityCallback); return unsubscribeVisibilityListener; From fbff2d3ed5774a6a2ddccd303051bab31c638e72 Mon Sep 17 00:00:00 2001 From: rayane-djouah <77965000+rayane-djouah@users.noreply.github.com> Date: Thu, 18 Jan 2024 17:31:51 +0100 Subject: [PATCH 233/391] use a better approach for useResponsiveLayout hook --- src/hooks/useResponsiveLayout.ts | 24 +++++++++--------------- 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/src/hooks/useResponsiveLayout.ts b/src/hooks/useResponsiveLayout.ts index dd782a9dbba5..10f1bccf15bd 100644 --- a/src/hooks/useResponsiveLayout.ts +++ b/src/hooks/useResponsiveLayout.ts @@ -1,25 +1,19 @@ -import type {ParamListBase, RouteProp} from '@react-navigation/native'; -import {useRoute} from '@react-navigation/native'; +import {navigationRef} from '@libs/Navigation/Navigation'; +import NAVIGATORS from '@src/NAVIGATORS'; import useWindowDimensions from './useWindowDimensions'; -type RouteParams = ParamListBase & { - params: {isInRHP?: boolean}; -}; type ResponsiveLayoutResult = { shouldUseNarrowLayout: boolean; }; /** - * Hook to determine if we are on mobile devices or in the RHP + * Hook to determine if we are on mobile devices or in the Modal Navigator */ export default function useResponsiveLayout(): ResponsiveLayoutResult { const {isSmallScreenWidth} = useWindowDimensions(); - try { - // eslint-disable-next-line react-hooks/rules-of-hooks - const {params} = useRoute>(); - return {shouldUseNarrowLayout: isSmallScreenWidth || (params?.isInRHP ?? false)}; - } catch (error) { - return { - shouldUseNarrowLayout: isSmallScreenWidth, - }; - } + const state = navigationRef.getRootState(); + const lastRoute = state.routes.at(-1); + const lastRouteName = lastRoute?.name; + const isInModal = lastRouteName === NAVIGATORS.LEFT_MODAL_NAVIGATOR || lastRouteName === NAVIGATORS.RIGHT_MODAL_NAVIGATOR; + const shouldUseNarrowLayout = isSmallScreenWidth || isInModal; + return {shouldUseNarrowLayout}; } From 0d6384d50abe777aac837a89343c35437671abc1 Mon Sep 17 00:00:00 2001 From: hurali97 Date: Fri, 19 Jan 2024 08:42:37 +0500 Subject: [PATCH 234/391] feat: add dontwarn flag to in proguard rules to allow build to succeed --- android/app/proguard-rules.pro | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/android/app/proguard-rules.pro b/android/app/proguard-rules.pro index 57650844b780..e553222dd682 100644 --- a/android/app/proguard-rules.pro +++ b/android/app/proguard-rules.pro @@ -9,4 +9,30 @@ # Add any project specific keep options here: -keep class com.expensify.chat.BuildConfig { *; } --keep, allowoptimization, allowobfuscation class expo.modules.** { *; } \ No newline at end of file +-keep, allowoptimization, allowobfuscation class expo.modules.** { *; } + +# Added from auto-generated missingrules.txt to allow build to succeed +-dontwarn com.onfido.javax.inject.Inject +-dontwarn javax.lang.model.element.Element +-dontwarn javax.lang.model.type.TypeMirror +-dontwarn javax.lang.model.type.TypeVisitor +-dontwarn javax.lang.model.util.SimpleTypeVisitor7 +-dontwarn net.sf.scuba.data.Gender +-dontwarn net.sf.scuba.smartcards.CardFileInputStream +-dontwarn net.sf.scuba.smartcards.CardService +-dontwarn net.sf.scuba.smartcards.CardServiceException +-dontwarn org.jmrtd.AccessKeySpec +-dontwarn org.jmrtd.BACKey +-dontwarn org.jmrtd.BACKeySpec +-dontwarn org.jmrtd.PACEKeySpec +-dontwarn org.jmrtd.PassportService +-dontwarn org.jmrtd.lds.CardAccessFile +-dontwarn org.jmrtd.lds.PACEInfo +-dontwarn org.jmrtd.lds.SecurityInfo +-dontwarn org.jmrtd.lds.icao.DG15File +-dontwarn org.jmrtd.lds.icao.DG1File +-dontwarn org.jmrtd.lds.icao.MRZInfo +-dontwarn org.jmrtd.protocol.AAResult +-dontwarn org.jmrtd.protocol.BACResult +-dontwarn org.jmrtd.protocol.PACEResult +-dontwarn org.spongycastle.jce.provider.BouncyCastleProvider \ No newline at end of file From bd4d978f94884911335142ff990235dc57410153 Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Fri, 19 Jan 2024 14:57:27 +0700 Subject: [PATCH 235/391] Revert "fallback string instead of number" This reverts commit ea5c3a2b6c180a94601b4d2408614db0e44dd5d4. --- src/components/ReportActionItem/MoneyRequestAction.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ReportActionItem/MoneyRequestAction.js b/src/components/ReportActionItem/MoneyRequestAction.js index 241b3e32447a..d242dbe23e86 100644 --- a/src/components/ReportActionItem/MoneyRequestAction.js +++ b/src/components/ReportActionItem/MoneyRequestAction.js @@ -116,7 +116,7 @@ function MoneyRequestAction({ const isDeletedParentAction = ReportActionsUtils.isDeletedParentAction(action); const isReversedTransaction = ReportActionsUtils.isReversedTransaction(action); if (!_.isEmpty(iouReport) && !_.isEmpty(reportActions) && chatReport.iouReportID && action.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD && network.isOffline) { - shouldShowPendingConversionMessage = IOUUtils.isTransactionPendingCurrencyConversion(iouReport, (action && action.originalMessage && action.originalMessage.IOUTransactionID) || '0'); + shouldShowPendingConversionMessage = IOUUtils.isTransactionPendingCurrencyConversion(iouReport, (action && action.originalMessage && action.originalMessage.IOUTransactionID) || 0); } return isDeletedParentAction || isReversedTransaction ? ( From 4cfb299c7e87b596678843c6630b4a93afa1b95b Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Fri, 19 Jan 2024 14:57:44 +0700 Subject: [PATCH 236/391] Revert "only display pending message for foreign currency transaction" This reverts commit 43c38ba40b27b6f8450c4280f9cf6cd720640067. --- src/components/ReportActionItem/MoneyRequestAction.js | 2 +- src/libs/IOUUtils.ts | 10 ++++------ tests/unit/IOUUtilsTest.js | 6 +++--- 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/src/components/ReportActionItem/MoneyRequestAction.js b/src/components/ReportActionItem/MoneyRequestAction.js index d242dbe23e86..46226969636e 100644 --- a/src/components/ReportActionItem/MoneyRequestAction.js +++ b/src/components/ReportActionItem/MoneyRequestAction.js @@ -116,7 +116,7 @@ function MoneyRequestAction({ const isDeletedParentAction = ReportActionsUtils.isDeletedParentAction(action); const isReversedTransaction = ReportActionsUtils.isReversedTransaction(action); if (!_.isEmpty(iouReport) && !_.isEmpty(reportActions) && chatReport.iouReportID && action.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD && network.isOffline) { - shouldShowPendingConversionMessage = IOUUtils.isTransactionPendingCurrencyConversion(iouReport, (action && action.originalMessage && action.originalMessage.IOUTransactionID) || 0); + shouldShowPendingConversionMessage = IOUUtils.isIOUReportPendingCurrencyConversion(iouReport); } return isDeletedParentAction || isReversedTransaction ? ( diff --git a/src/libs/IOUUtils.ts b/src/libs/IOUUtils.ts index aab9aac2391d..11dd0f5badda 100644 --- a/src/libs/IOUUtils.ts +++ b/src/libs/IOUUtils.ts @@ -110,14 +110,12 @@ function updateIOUOwnerAndTotal(iouReport: OnyxEntry, actorAccountID: nu } /** - * Returns whether or not a transaction of IOU report contains money requests in a different currency + * Returns whether or not an IOU report contains money requests in a different currency * that are either created or cancelled offline, and thus haven't been converted to the report's currency yet */ -function isTransactionPendingCurrencyConversion(iouReport: Report, transactionID: string): boolean { +function isIOUReportPendingCurrencyConversion(iouReport: Report): boolean { const reportTransactions: Transaction[] = TransactionUtils.getAllReportTransactions(iouReport.reportID); - const pendingRequestsInDifferentCurrency = reportTransactions.filter( - (transaction) => transaction.pendingAction && transaction.transactionID === transactionID && TransactionUtils.getCurrency(transaction) !== iouReport.currency, - ); + const pendingRequestsInDifferentCurrency = reportTransactions.filter((transaction) => transaction.pendingAction && TransactionUtils.getCurrency(transaction) !== iouReport.currency); return pendingRequestsInDifferentCurrency.length > 0; } @@ -129,4 +127,4 @@ function isValidMoneyRequestType(iouType: string): boolean { return moneyRequestType.includes(iouType); } -export {calculateAmount, updateIOUOwnerAndTotal, isTransactionPendingCurrencyConversion, isValidMoneyRequestType, navigateToStartMoneyRequestStep, navigateToStartStepIfScanFileCannotBeRead}; +export {calculateAmount, updateIOUOwnerAndTotal, isIOUReportPendingCurrencyConversion, isValidMoneyRequestType, navigateToStartMoneyRequestStep, navigateToStartStepIfScanFileCannotBeRead}; diff --git a/tests/unit/IOUUtilsTest.js b/tests/unit/IOUUtilsTest.js index 7f239d9bb576..ac04b74a0ca5 100644 --- a/tests/unit/IOUUtilsTest.js +++ b/tests/unit/IOUUtilsTest.js @@ -17,7 +17,7 @@ function initCurrencyList() { } describe('IOUUtils', () => { - describe('isTransactionPendingCurrencyConversion', () => { + describe('isIOUReportPendingCurrencyConversion', () => { beforeAll(() => { Onyx.init({ keys: ONYXKEYS, @@ -34,7 +34,7 @@ describe('IOUUtils', () => { [`${ONYXKEYS.COLLECTION.TRANSACTION}${aedPendingTransaction.transactionID}`]: aedPendingTransaction, }).then(() => { // We requested money offline in a different currency, we don't know the total of the iouReport until we're back online - expect(IOUUtils.isTransactionPendingCurrencyConversion(iouReport, aedPendingTransaction.transactionID)).toBe(true); + expect(IOUUtils.isIOUReportPendingCurrencyConversion(iouReport)).toBe(true); }); }); @@ -54,7 +54,7 @@ describe('IOUUtils', () => { }, }).then(() => { // We requested money online in a different currency, we know the iouReport total and there's no need to show the pending conversion message - expect(IOUUtils.isTransactionPendingCurrencyConversion(iouReport, aedPendingTransaction.transactionID)).toBe(false); + expect(IOUUtils.isIOUReportPendingCurrencyConversion(iouReport)).toBe(false); }); }); }); From db3e96cc82c6467ef16d54c748a5dbb154b847c6 Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Fri, 19 Jan 2024 14:57:57 +0700 Subject: [PATCH 237/391] Revert "remove most recent iou report action id" This reverts commit 964763547f578d20b5bb3d21120e0189b33b37ba. --- .../ReportActionItem/MoneyRequestAction.js | 8 +++++++- src/pages/home/report/ReportActionItem.js | 5 +++++ .../home/report/ReportActionItemParentAction.js | 1 + src/pages/home/report/ReportActionsList.js | 8 +++++++- .../home/report/ReportActionsListItemRenderer.js | 16 +++++++++++++++- src/pages/home/report/ReportActionsView.js | 4 ++++ 6 files changed, 39 insertions(+), 3 deletions(-) diff --git a/src/components/ReportActionItem/MoneyRequestAction.js b/src/components/ReportActionItem/MoneyRequestAction.js index 46226969636e..d159998b2d57 100644 --- a/src/components/ReportActionItem/MoneyRequestAction.js +++ b/src/components/ReportActionItem/MoneyRequestAction.js @@ -115,7 +115,13 @@ function MoneyRequestAction({ let shouldShowPendingConversionMessage = false; const isDeletedParentAction = ReportActionsUtils.isDeletedParentAction(action); const isReversedTransaction = ReportActionsUtils.isReversedTransaction(action); - if (!_.isEmpty(iouReport) && !_.isEmpty(reportActions) && chatReport.iouReportID && action.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD && network.isOffline) { + if ( + !_.isEmpty(iouReport) && + !_.isEmpty(reportActions) && + chatReport.iouReportID && + action.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD && + network.isOffline + ) { shouldShowPendingConversionMessage = IOUUtils.isIOUReportPendingCurrencyConversion(iouReport); } diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.js index 2f8e86de5cdb..55b294936f49 100644 --- a/src/pages/home/report/ReportActionItem.js +++ b/src/pages/home/report/ReportActionItem.js @@ -89,6 +89,9 @@ const propTypes = { /** Should the comment have the appearance of being grouped with the previous comment? */ displayAsGroup: PropTypes.bool.isRequired, + /** Is this the most recent IOU Action? */ + isMostRecentIOUReportAction: PropTypes.bool.isRequired, + /** Should we display the new marker on top of the comment? */ shouldDisplayNewMarker: PropTypes.bool.isRequired, @@ -346,6 +349,7 @@ function ReportActionItem(props) { chatReportID={originalMessage.IOUReportID ? props.report.chatReportID : props.report.reportID} requestReportID={iouReportID} action={props.action} + isMostRecentIOUReportAction={props.isMostRecentIOUReportAction} isHovered={hovered} contextMenuAnchor={popoverAnchorRef} checkIfContextMenuActive={toggleContextMenuFromActiveReportAction} @@ -819,6 +823,7 @@ export default compose( (prevProps, nextProps) => prevProps.displayAsGroup === nextProps.displayAsGroup && prevProps.draftMessage === nextProps.draftMessage && + prevProps.isMostRecentIOUReportAction === nextProps.isMostRecentIOUReportAction && prevProps.shouldDisplayNewMarker === nextProps.shouldDisplayNewMarker && _.isEqual(prevProps.emojiReactions, nextProps.emojiReactions) && _.isEqual(prevProps.action, nextProps.action) && diff --git a/src/pages/home/report/ReportActionItemParentAction.js b/src/pages/home/report/ReportActionItemParentAction.js index 7c9f08b35ce7..d1a294881eb9 100644 --- a/src/pages/home/report/ReportActionItemParentAction.js +++ b/src/pages/home/report/ReportActionItemParentAction.js @@ -73,6 +73,7 @@ function ReportActionItemParentAction(props) { report={props.report} action={parentReportAction} displayAsGroup={false} + isMostRecentIOUReportAction={false} shouldDisplayNewMarker={props.shouldDisplayNewMarker} index={props.index} /> diff --git a/src/pages/home/report/ReportActionsList.js b/src/pages/home/report/ReportActionsList.js index 635466219f2b..8d79e7af8dd4 100644 --- a/src/pages/home/report/ReportActionsList.js +++ b/src/pages/home/report/ReportActionsList.js @@ -34,6 +34,9 @@ const propTypes = { /** 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, + /** The report metadata loading states */ isLoadingInitialReportActions: PropTypes.bool, @@ -70,6 +73,7 @@ const propTypes = { const defaultProps = { onScroll: () => {}, + mostRecentIOUReportActionID: '', isLoadingInitialReportActions: false, isLoadingOlderReportActions: false, isLoadingNewerReportActions: false, @@ -124,6 +128,7 @@ function ReportActionsList({ sortedReportActions, windowHeight, onScroll, + mostRecentIOUReportActionID, isSmallScreenWidth, personalDetailsList, currentUserPersonalDetails, @@ -410,11 +415,12 @@ function ReportActionsList({ report={report} linkedReportActionID={linkedReportActionID} displayAsGroup={ReportActionsUtils.isConsecutiveActionMadeByPreviousActor(sortedReportActions, index)} + mostRecentIOUReportActionID={mostRecentIOUReportActionID} shouldHideThreadDividerLine={shouldHideThreadDividerLine} shouldDisplayNewMarker={shouldDisplayNewMarker(reportAction, index)} /> ), - [report, linkedReportActionID, sortedReportActions, shouldHideThreadDividerLine, shouldDisplayNewMarker], + [report, linkedReportActionID, sortedReportActions, mostRecentIOUReportActionID, shouldHideThreadDividerLine, shouldDisplayNewMarker], ); // Native mobile does not render updates flatlist the changes even though component did update called. diff --git a/src/pages/home/report/ReportActionsListItemRenderer.js b/src/pages/home/report/ReportActionsListItemRenderer.js index 791a3f78c67b..a9ae2b4c73b9 100644 --- a/src/pages/home/report/ReportActionsListItemRenderer.js +++ b/src/pages/home/report/ReportActionsListItemRenderer.js @@ -22,6 +22,9 @@ const propTypes = { /** Should the comment have the appearance of being grouped with the previous comment? */ displayAsGroup: PropTypes.bool.isRequired, + /** The ID of the most recent IOU report action connected with the shown report */ + mostRecentIOUReportActionID: PropTypes.string, + /** If the thread divider line should be hidden */ shouldHideThreadDividerLine: PropTypes.bool.isRequired, @@ -33,10 +36,20 @@ const propTypes = { }; const defaultProps = { + mostRecentIOUReportActionID: '', linkedReportActionID: '', }; -function ReportActionsListItemRenderer({reportAction, index, report, displayAsGroup, shouldHideThreadDividerLine, shouldDisplayNewMarker, linkedReportActionID}) { +function ReportActionsListItemRenderer({ + reportAction, + index, + report, + displayAsGroup, + mostRecentIOUReportActionID, + shouldHideThreadDividerLine, + shouldDisplayNewMarker, + linkedReportActionID, +}) { const shouldDisplayParentAction = reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED && ReportUtils.isChatThread(report) && @@ -65,6 +78,7 @@ function ReportActionsListItemRenderer({reportAction, index, report, displayAsGr reportAction.actionName, ) } + isMostRecentIOUReportAction={reportAction.reportActionID === mostRecentIOUReportActionID} index={index} /> ); diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index ddcea7894251..2758437a3962 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -14,6 +14,7 @@ import usePrevious from '@hooks/usePrevious'; import compose from '@libs/compose'; import getIsReportFullyVisible from '@libs/getIsReportFullyVisible'; import Performance from '@libs/Performance'; +import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import {isUserCreatedPolicyRoom} from '@libs/ReportUtils'; import {didUserLogInDuringSession} from '@libs/SessionUtils'; import {ReactionListContext} from '@pages/home/ReportScreenContext'; @@ -86,6 +87,8 @@ function ReportActionsView(props) { const didSubscribeToReportTypingEvents = useRef(false); const isFirstRender = useRef(true); const hasCachedActions = useInitialValue(() => _.size(props.reportActions) > 0); + const mostRecentIOUReportActionID = useInitialValue(() => ReportActionsUtils.getMostRecentIOURequestActionID(props.reportActions)); + const prevNetworkRef = useRef(props.network); const prevAuthTokenType = usePrevious(props.session.authTokenType); @@ -254,6 +257,7 @@ function ReportActionsView(props) { report={props.report} onLayout={recordTimeToMeasureItemLayout} sortedReportActions={props.reportActions} + mostRecentIOUReportActionID={mostRecentIOUReportActionID} loadOlderChats={loadOlderChats} loadNewerChats={loadNewerChats} isLoadingInitialReportActions={props.isLoadingInitialReportActions} From 86a41083ee018b2409aefe02d87b3901e479be24 Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Fri, 19 Jan 2024 14:58:05 +0700 Subject: [PATCH 238/391] Revert "fix: 33073" This reverts commit ad03aca8426f0ad13f65dc3b0fd75165a2a2a214. --- src/components/ReportActionItem/MoneyRequestAction.js | 5 +++++ src/pages/home/report/ReportActionItem.js | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/components/ReportActionItem/MoneyRequestAction.js b/src/components/ReportActionItem/MoneyRequestAction.js index d159998b2d57..e0a3152a41b4 100644 --- a/src/components/ReportActionItem/MoneyRequestAction.js +++ b/src/components/ReportActionItem/MoneyRequestAction.js @@ -34,6 +34,9 @@ const propTypes = { /** The ID of the associated request report */ requestReportID: PropTypes.string.isRequired, + /** Is this IOUACTION the most recent? */ + isMostRecentIOUReportAction: PropTypes.bool.isRequired, + /** Popover context menu anchor, used for showing context menu */ contextMenuAnchor: refPropTypes, @@ -78,6 +81,7 @@ function MoneyRequestAction({ action, chatReportID, requestReportID, + isMostRecentIOUReportAction, contextMenuAnchor, checkIfContextMenuActive, chatReport, @@ -119,6 +123,7 @@ function MoneyRequestAction({ !_.isEmpty(iouReport) && !_.isEmpty(reportActions) && chatReport.iouReportID && + isMostRecentIOUReportAction && action.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD && network.isOffline ) { diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.js index 55b294936f49..00ce20c19a85 100644 --- a/src/pages/home/report/ReportActionItem.js +++ b/src/pages/home/report/ReportActionItem.js @@ -346,7 +346,7 @@ function ReportActionItem(props) { const iouReportID = originalMessage.IOUReportID ? originalMessage.IOUReportID.toString() : '0'; children = ( Date: Fri, 19 Jan 2024 15:15:18 +0700 Subject: [PATCH 239/391] fix 33073 --- src/pages/home/report/ReportActionItem.js | 2 +- src/pages/home/report/ReportActionsView.js | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.js index 00ce20c19a85..55b294936f49 100644 --- a/src/pages/home/report/ReportActionItem.js +++ b/src/pages/home/report/ReportActionItem.js @@ -346,7 +346,7 @@ function ReportActionItem(props) { const iouReportID = originalMessage.IOUReportID ? originalMessage.IOUReportID.toString() : '0'; children = ( _.size(props.reportActions) > 0); - const mostRecentIOUReportActionID = useInitialValue(() => ReportActionsUtils.getMostRecentIOURequestActionID(props.reportActions)); - + const mostRecentIOUReportActionID = useMemo(() => ReportActionsUtils.getMostRecentIOURequestActionID(props.reportActions), [props.reportActions]); const prevNetworkRef = useRef(props.network); const prevAuthTokenType = usePrevious(props.session.authTokenType); From 3e4c2fd231f7b378ce5707ea9b3a9a2ab7fd17d0 Mon Sep 17 00:00:00 2001 From: tienifr Date: Fri, 19 Jan 2024 16:21:41 +0700 Subject: [PATCH 240/391] remove nativeEvent --- src/components/ImageView/index.tsx | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/components/ImageView/index.tsx b/src/components/ImageView/index.tsx index fb696fb03c64..13a9033e2d4b 100644 --- a/src/components/ImageView/index.tsx +++ b/src/components/ImageView/index.tsx @@ -1,4 +1,5 @@ -import React, {MouseEvent as ReactMouseEvent, useCallback, useEffect, useRef, useState} from 'react'; +import type {MouseEvent as ReactMouseEvent} from 'react'; +import React, { useCallback, useEffect, useRef, useState} from 'react'; import type {GestureResponderEvent, LayoutChangeEvent, NativeSyntheticEvent} from 'react-native'; import {View} from 'react-native'; import FullscreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; @@ -138,10 +139,9 @@ function ImageView({isAuthTokenRequired = false, url, fileName, onError}: ImageV }; const trackPointerPosition = useCallback( - (e: MouseEvent) => { - const mouseEvent = e as unknown as ReactMouseEvent; + (event: MouseEvent) => { // Whether the pointer is released inside the ImageView - const isInsideImageView = scrollableRef.current?.contains(mouseEvent.nativeEvent.target as Node); + const isInsideImageView = scrollableRef.current?.contains(event.target as Node); if (!isInsideImageView && isZoomed && isDragging && isMouseDown) { setIsDragging(false); @@ -152,15 +152,14 @@ function ImageView({isAuthTokenRequired = false, url, fileName, onError}: ImageV ); const trackMovement = useCallback( - (e: MouseEvent) => { - const mouseEvent = e as unknown as ReactMouseEvent; + (event: MouseEvent) => { if (!isZoomed) { return; } if (isDragging && isMouseDown && scrollableRef.current) { - const x = mouseEvent.nativeEvent.x; - const y = mouseEvent.nativeEvent.y; + const x = event.x; + const y = event.y; const moveX = initialX - x; const moveY = initialY - y; scrollableRef.current.scrollLeft = initialScrollLeft + moveX; From df1502d54b41d5040d0a044165070d033c4e4e37 Mon Sep 17 00:00:00 2001 From: tienifr Date: Fri, 19 Jan 2024 16:25:17 +0700 Subject: [PATCH 241/391] nativeEvent checker --- src/components/ImageView/index.tsx | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/components/ImageView/index.tsx b/src/components/ImageView/index.tsx index 13a9033e2d4b..ed6cfd6413a3 100644 --- a/src/components/ImageView/index.tsx +++ b/src/components/ImageView/index.tsx @@ -1,4 +1,4 @@ -import type {MouseEvent as ReactMouseEvent} from 'react'; +import type {MouseEvent as ReactMouseEvent, SyntheticEvent} from 'react'; import React, { useCallback, useEffect, useRef, useState} from 'react'; import type {GestureResponderEvent, LayoutChangeEvent, NativeSyntheticEvent} from 'react-native'; import {View} from 'react-native'; @@ -113,11 +113,10 @@ function ImageView({isAuthTokenRequired = false, url, fileName, onError}: ImageV return {offsetX, offsetY}; }; - const onContainerPress = (e?: GestureResponderEvent | KeyboardEvent) => { - const mouseEvent = e as unknown as ReactMouseEvent; + const onContainerPress = (e?: GestureResponderEvent | KeyboardEvent | SyntheticEvent) => { if (!isZoomed && !isDragging) { - if (mouseEvent.nativeEvent) { - const {offsetX, offsetY} = mouseEvent.nativeEvent; + if (e && 'nativeEvent' in e && 'offsetX' in e.nativeEvent) { + const {offsetX, offsetY} = e.nativeEvent; // Dividing clicked positions by the zoom scale to get coordinates // so that once we zoom we will scroll to the clicked location. From dbadc31306a6e4fc53df78ab8031c70b2fbcef87 Mon Sep 17 00:00:00 2001 From: tienifr Date: Fri, 19 Jan 2024 16:28:12 +0700 Subject: [PATCH 242/391] lint fix --- src/components/ImageView/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/ImageView/index.tsx b/src/components/ImageView/index.tsx index ed6cfd6413a3..3677111c09df 100644 --- a/src/components/ImageView/index.tsx +++ b/src/components/ImageView/index.tsx @@ -1,5 +1,5 @@ -import type {MouseEvent as ReactMouseEvent, SyntheticEvent} from 'react'; -import React, { useCallback, useEffect, useRef, useState} from 'react'; +import type {SyntheticEvent} from 'react'; +import React, {useCallback, useEffect, useRef, useState} from 'react'; import type {GestureResponderEvent, LayoutChangeEvent, NativeSyntheticEvent} from 'react-native'; import {View} from 'react-native'; import FullscreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; From 8f68bc31f71b8627950f6f80cf8d793604e3d016 Mon Sep 17 00:00:00 2001 From: tienifr Date: Fri, 19 Jan 2024 18:28:23 +0700 Subject: [PATCH 243/391] clone tnode --- .../HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.js b/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.js index e7712a2dcde1..189aa528cb35 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.js +++ b/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.js @@ -19,6 +19,7 @@ import CONST from '@src/CONST'; import * as LoginUtils from '@src/libs/LoginUtils'; import ROUTES from '@src/ROUTES'; import htmlRendererPropTypes from './htmlRendererPropTypes'; +import { cloneDeep } from 'lodash'; const propTypes = { ...htmlRendererPropTypes, @@ -38,8 +39,7 @@ function MentionUserRenderer(props) { let accountID; let displayNameOrLogin; let navigationRoute; - const tnode = props.tnode; - + const tnode = cloneDeep(props.tnode); const getMentionDisplayText = (displayText, accountId, userLogin = '') => { if (accountId && userLogin !== displayText) { return displayText; From a3e41096bf9b179060da0e752b47d3307629a683 Mon Sep 17 00:00:00 2001 From: tienifr Date: Fri, 19 Jan 2024 18:32:57 +0700 Subject: [PATCH 244/391] lint fix --- .../HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.js b/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.js index 189aa528cb35..ca316c5aa9eb 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.js +++ b/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.js @@ -1,3 +1,4 @@ +import {cloneDeep} from 'lodash'; import lodashGet from 'lodash/get'; import React from 'react'; import {TNodeChildrenRenderer} from 'react-native-render-html'; @@ -19,7 +20,6 @@ import CONST from '@src/CONST'; import * as LoginUtils from '@src/libs/LoginUtils'; import ROUTES from '@src/ROUTES'; import htmlRendererPropTypes from './htmlRendererPropTypes'; -import { cloneDeep } from 'lodash'; const propTypes = { ...htmlRendererPropTypes, From 55408387417e016eb45bb908df9ade8b373c3a68 Mon Sep 17 00:00:00 2001 From: brunovjk Date: Fri, 19 Jan 2024 09:51:36 -0300 Subject: [PATCH 245/391] Isolate reuse 'didScreenTransitionEnd' to address the screen transition skeleton issue --- .../SelectionList/BaseSelectionList.js | 4 +--- .../SelectionList/selectionListPropTypes.js | 3 --- ...ryForRefactorRequestParticipantsSelector.js | 18 +++++++++--------- 3 files changed, 10 insertions(+), 15 deletions(-) diff --git a/src/components/SelectionList/BaseSelectionList.js b/src/components/SelectionList/BaseSelectionList.js index 2d209ef573c3..960618808fd9 100644 --- a/src/components/SelectionList/BaseSelectionList.js +++ b/src/components/SelectionList/BaseSelectionList.js @@ -45,7 +45,6 @@ function BaseSelectionList({ inputMode = CONST.INPUT_MODE.TEXT, onChangeText, initiallyFocusedOptionKey = '', - isLoadingNewOptions = false, onScroll, onScrollBeginDrag, headerMessage = '', @@ -429,11 +428,10 @@ function BaseSelectionList({ spellCheck={false} onSubmitEditing={selectFocusedOption} blurOnSubmit={Boolean(flattenedSections.allOptions.length)} - isLoading={isLoadingNewOptions} />
)} - {!isLoadingNewOptions && Boolean(headerMessage) && ( + {Boolean(headerMessage) && ( {headerMessage} diff --git a/src/components/SelectionList/selectionListPropTypes.js b/src/components/SelectionList/selectionListPropTypes.js index b0c5dd37867e..f5178112a4c3 100644 --- a/src/components/SelectionList/selectionListPropTypes.js +++ b/src/components/SelectionList/selectionListPropTypes.js @@ -151,9 +151,6 @@ const propTypes = { /** Item `keyForList` to focus initially */ initiallyFocusedOptionKey: PropTypes.string, - /** Whether we are loading new options */ - isLoadingNewOptions: PropTypes.bool, - /** Callback to fire when the list is scrolled */ onScroll: PropTypes.func, diff --git a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js index 6da8524934d3..65b51da1d72d 100644 --- a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js +++ b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js @@ -1,6 +1,6 @@ import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; -import React, {useCallback, useEffect, useMemo} from 'react'; +import React, {useCallback, useMemo, useState} from 'react'; import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; @@ -11,7 +11,6 @@ import {PressableWithFeedback} from '@components/Pressable'; import ReferralProgramCTA from '@components/ReferralProgramCTA'; import SelectCircle from '@components/SelectCircle'; import SelectionList from '@components/SelectionList'; -import useDebouncedState from '@hooks/useDebouncedState'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -86,7 +85,7 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({ }) { const {translate} = useLocalize(); const styles = useThemeStyles(); - const [searchTerm, debouncedSearchTerm, setSearchTerm] = useDebouncedState(''); + const [searchTerm, setSearchTerm] = useState(''); const {isOffline} = useNetwork(); const personalDetails = usePersonalDetails(); @@ -248,12 +247,13 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({ [maxParticipantsReached, newChatOptions, participants, searchTerm], ); - useEffect(() => { - if (!debouncedSearchTerm.length) { - return; + // When search term updates we will fetch any reports + const setSearchTermAndSearchInServer = useCallback((text = '') => { + if (text.length) { + Report.searchInServer(text); } - Report.searchInServer(debouncedSearchTerm); - }, [debouncedSearchTerm]); + setSearchTerm(text); + }, []); // Right now you can't split a request with a workspace and other additional participants // This is getting properly fixed in https://github.com/Expensify/App/issues/27508, but as a stop-gap to prevent @@ -341,7 +341,7 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({ textInputValue={searchTerm} textInputLabel={translate('optionsSelector.nameEmailOrPhoneNumber')} textInputHint={offlineMessage} - onChangeText={setSearchTerm} + onChangeText={setSearchTermAndSearchInServer} shouldPreventDefaultFocusOnSelectRow={!DeviceCapabilities.canUseTouchScreen()} onSelectRow={addSingleParticipant} footerContent={footerContent} From 9e0c27acd2a6484d7094a30f1a465635211fdbea Mon Sep 17 00:00:00 2001 From: Aldo Canepa Garay <87341702+aldo-expensify@users.noreply.github.com> Date: Fri, 19 Jan 2024 11:42:42 -0300 Subject: [PATCH 246/391] Fix comment --- src/libs/actions/IOU.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index 722d9198751e..0cf427cb2f0c 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -1169,7 +1169,7 @@ function updateMoneyRequestMerchant(transactionID, transactionThreadReportID, va } /** - * Updates the tag date of a money request + * Updates the tag of a money request * * @param {String} transactionID * @param {Number} transactionThreadReportID From 8f5f879da515b998472f5cbd4ffdc33428e9fadb Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Fri, 19 Jan 2024 16:19:08 +0100 Subject: [PATCH 247/391] fix: prop --- src/components/Composer/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Composer/index.tsx b/src/components/Composer/index.tsx index 3c2caf020ef7..f3a471335f0d 100755 --- a/src/components/Composer/index.tsx +++ b/src/components/Composer/index.tsx @@ -368,7 +368,7 @@ function Composer( /* eslint-disable-next-line react/jsx-props-no-spreading */ {...props} onSelectionChange={addCursorPositionToSelectionChange} - rows={numberOfLines} + numberOfLines={numberOfLines} disabled={isDisabled} onKeyPress={handleKeyPress} onFocus={(e) => { From 3bd59508671e60b0496f754cb935a95484ecc96e Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Fri, 19 Jan 2024 22:43:23 +0700 Subject: [PATCH 248/391] add comment --- src/pages/home/report/ReportActionItem.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.js index 55b294936f49..60503424f663 100644 --- a/src/pages/home/report/ReportActionItem.js +++ b/src/pages/home/report/ReportActionItem.js @@ -346,6 +346,8 @@ function ReportActionItem(props) { const iouReportID = originalMessage.IOUReportID ? originalMessage.IOUReportID.toString() : '0'; children = ( Date: Fri, 19 Jan 2024 21:25:13 +0530 Subject: [PATCH 249/391] added requested changes --- src/pages/home/report/comment/TextCommentFragment.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/home/report/comment/TextCommentFragment.tsx b/src/pages/home/report/comment/TextCommentFragment.tsx index 7450dc14e6bf..998ed9f6616f 100644 --- a/src/pages/home/report/comment/TextCommentFragment.tsx +++ b/src/pages/home/report/comment/TextCommentFragment.tsx @@ -84,7 +84,7 @@ function TextCommentFragment({fragment, styleAsDeleted, source, style, displayAs > {convertToLTR(message)} - {!!fragment.isEdited && ( + {fragment.isEdited && ( <> Date: Fri, 19 Jan 2024 23:47:38 +0700 Subject: [PATCH 250/391] update constant value --- src/components/EmojiPicker/EmojiPickerButton.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/EmojiPicker/EmojiPickerButton.js b/src/components/EmojiPicker/EmojiPickerButton.js index b056ccb22875..832715e3214c 100644 --- a/src/components/EmojiPicker/EmojiPickerButton.js +++ b/src/components/EmojiPicker/EmojiPickerButton.js @@ -11,6 +11,7 @@ import useThemeStyles from '@hooks/useThemeStyles'; import compose from '@libs/compose'; import getButtonState from '@libs/getButtonState'; import * as EmojiPickerAction from '@userActions/EmojiPickerAction'; +import CONST from '@src/CONST'; const propTypes = { /** Flag to disable the emoji picker button */ @@ -58,8 +59,8 @@ function EmojiPickerButton(props) { props.onEmojiSelected, emojiPopoverAnchor, { - horizontal: 'right', - vertical: 'bottom', + horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.RIGHT, + vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.BOTTOM, shiftVertical: props.shiftVertical, }, () => {}, From c235a069b0630394813b8bb18cf4b1fc94d61e53 Mon Sep 17 00:00:00 2001 From: brunovjk Date: Fri, 19 Jan 2024 13:53:25 -0300 Subject: [PATCH 251/391] remove redundance --- .../MoneyTemporaryForRefactorRequestParticipantsSelector.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js index 65b51da1d72d..699284645162 100644 --- a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js +++ b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js @@ -348,7 +348,6 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({ headerMessage={headerMessage} showLoadingPlaceholder={!(didScreenTransitionEnd && isOptionsDataReady)} rightHandSideComponent={itemRightSideComponent} - isLoadingNewOptions={isSearchingForReports} />
); From 10b518f42ff588243009076b83d0e0cea8c6d46d Mon Sep 17 00:00:00 2001 From: Fitsum Abebe Date: Fri, 19 Jan 2024 20:00:32 +0300 Subject: [PATCH 252/391] minor fix --- src/hooks/useCallbackRef.ts | 13 ------------- src/pages/home/report/ReportActionsList.js | 11 ++++------- 2 files changed, 4 insertions(+), 20 deletions(-) delete mode 100644 src/hooks/useCallbackRef.ts diff --git a/src/hooks/useCallbackRef.ts b/src/hooks/useCallbackRef.ts deleted file mode 100644 index 075cbe08bbbc..000000000000 --- a/src/hooks/useCallbackRef.ts +++ /dev/null @@ -1,13 +0,0 @@ -import {useEffect, useMemo, useRef} from 'react'; - -const useCallbackRef = unknown>(callback: T): T => { - const calbackRef = useRef(callback); - - useEffect(() => { - calbackRef.current = callback; - }); - - return useMemo(() => ((...args) => calbackRef.current(...args)) as T, []); -}; - -export default useCallbackRef; diff --git a/src/pages/home/report/ReportActionsList.js b/src/pages/home/report/ReportActionsList.js index 30888f47beba..2c3032a4986b 100644 --- a/src/pages/home/report/ReportActionsList.js +++ b/src/pages/home/report/ReportActionsList.js @@ -9,7 +9,6 @@ import InvertedFlatList from '@components/InvertedFlatList'; import {withPersonalDetails} from '@components/OnyxProvider'; import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsDefaultProps, withCurrentUserPersonalDetailsPropTypes} from '@components/withCurrentUserPersonalDetails'; import withWindowDimensions, {windowDimensionsPropTypes} from '@components/withWindowDimensions'; -import useCallbackRef from '@hooks/useCallbackRef'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useReportScrollManager from '@hooks/useReportScrollManager'; @@ -412,7 +411,7 @@ function ReportActionsList({ calculateUnreadMarker(); }, [calculateUnreadMarker, report.lastReadTime, messageManuallyMarkedUnread]); - const visibilityCallback = useCallbackRef(() => { + const onVisibilityChange = useCallback(() => { if (!Visibility.isVisible() || scrollingVerticalOffset.current >= MSG_VISIBLE_THRESHOLD || !ReportUtils.isUnread(report) || messageManuallyMarkedUnread) { return; } @@ -422,15 +421,13 @@ function ReportActionsList({ setCurrentUnreadMarker(null); cacheUnreadMarkers.delete(report.reportID); calculateUnreadMarker(); - }); + }, [calculateUnreadMarker, messageManuallyMarkedUnread, report]); useEffect(() => { - const unsubscribeVisibilityListener = Visibility.onVisibilityChange(visibilityCallback); + const unsubscribeVisibilityListener = Visibility.onVisibilityChange(onVisibilityChange); return unsubscribeVisibilityListener; - - /* eslint-disable-next-line react-hooks/exhaustive-deps */ - }, [report.reportID]); + }, [onVisibilityChange]); const renderItem = useCallback( ({item: reportAction, index}) => ( From d49c487c24e839fe65f47a17078a30c688d9a351 Mon Sep 17 00:00:00 2001 From: Fitsum Abebe Date: Fri, 19 Jan 2024 20:12:19 +0300 Subject: [PATCH 253/391] minor change --- src/pages/home/report/ReportActionsList.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/home/report/ReportActionsList.js b/src/pages/home/report/ReportActionsList.js index 2c3032a4986b..5cee5a77ea46 100644 --- a/src/pages/home/report/ReportActionsList.js +++ b/src/pages/home/report/ReportActionsList.js @@ -412,7 +412,7 @@ function ReportActionsList({ }, [calculateUnreadMarker, report.lastReadTime, messageManuallyMarkedUnread]); const onVisibilityChange = useCallback(() => { - if (!Visibility.isVisible() || scrollingVerticalOffset.current >= MSG_VISIBLE_THRESHOLD || !ReportUtils.isUnread(report) || messageManuallyMarkedUnread) { + if (!Visibility.isVisible() || scrollingVerticalOffset.current >= MSG_VISIBLE_THRESHOLD || !ReportUtils.isUnread(report)) { return; } @@ -421,7 +421,7 @@ function ReportActionsList({ setCurrentUnreadMarker(null); cacheUnreadMarkers.delete(report.reportID); calculateUnreadMarker(); - }, [calculateUnreadMarker, messageManuallyMarkedUnread, report]); + }, [calculateUnreadMarker, report]); useEffect(() => { const unsubscribeVisibilityListener = Visibility.onVisibilityChange(onVisibilityChange); From 5f81b8d58b435bcbe8de9daf995ea893eb37116b Mon Sep 17 00:00:00 2001 From: rory Date: Fri, 19 Jan 2024 11:28:46 -0800 Subject: [PATCH 254/391] Include isUnreadWithMention in selector --- src/pages/home/sidebar/SidebarLinksData.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/pages/home/sidebar/SidebarLinksData.js b/src/pages/home/sidebar/SidebarLinksData.js index d29420f182f5..4d98e865f4cd 100644 --- a/src/pages/home/sidebar/SidebarLinksData.js +++ b/src/pages/home/sidebar/SidebarLinksData.js @@ -14,6 +14,7 @@ import useLocalize from '@hooks/useLocalize'; import usePrevious from '@hooks/usePrevious'; import useThemeStyles from '@hooks/useThemeStyles'; import compose from '@libs/compose'; +import * as ReportUtils from '@libs/ReportUtils'; import SidebarUtils from '@libs/SidebarUtils'; import reportPropTypes from '@pages/reportPropTypes'; import CONST from '@src/CONST'; @@ -202,6 +203,7 @@ const chatReportSelector = (report) => parentReportActionID: report.parentReportActionID, parentReportID: report.parentReportID, isDeletedParentAction: report.isDeletedParentAction, + isUnreadWithMention: ReportUtils.isUnreadWithMention(report), }; /** From 0d2693264e64b6fc0f69d1f837052d7b948f0066 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Sun, 21 Jan 2024 14:20:46 +0100 Subject: [PATCH 255/391] wip: use partner authentication for e2e tests --- src/libs/E2E/actions/e2eLogin.ts | 50 +++++++++++++++--------- src/libs/E2E/reactNativeLaunchingTest.ts | 1 + 2 files changed, 33 insertions(+), 18 deletions(-) diff --git a/src/libs/E2E/actions/e2eLogin.ts b/src/libs/E2E/actions/e2eLogin.ts index 41f9de6c6501..6f8672bd0b2d 100644 --- a/src/libs/E2E/actions/e2eLogin.ts +++ b/src/libs/E2E/actions/e2eLogin.ts @@ -1,9 +1,26 @@ /* eslint-disable rulesdir/prefer-onyx-connect-in-libs */ +import Config from 'react-native-config'; import Onyx from 'react-native-onyx'; -import E2EClient from '@libs/E2E/client'; -import * as Session from '@userActions/Session'; +import {Authenticate} from '@libs/Authentication'; +import CONFIG from '@src/CONFIG'; import ONYXKEYS from '@src/ONYXKEYS'; +function getConfigValueOrThrow(key: string): string { + const value = Config[key]; + if (value == null) { + throw new Error(`Missing config value for ${key}`); + } + return value; +} + +const e2eUserCredentials = { + email: getConfigValueOrThrow('EXPENSIFY_PARTNER_PASSWORD_EMAIL'), + partnerUserID: getConfigValueOrThrow('EXPENSIFY_PARTNER_USER_ID'), + partnerUserSecret: getConfigValueOrThrow('EXPENSIFY_PARTNER_USER_SECRET'), + partnerName: CONFIG.EXPENSIFY.PARTNER_NAME, + partnerPassword: CONFIG.EXPENSIFY.PARTNER_PASSWORD, +}; + /** * Command for e2e test to automatically sign in a user. * If the user is already logged in the function will simply @@ -11,7 +28,7 @@ import ONYXKEYS from '@src/ONYXKEYS'; * * @return Resolved true when the user was actually signed in. Returns false if the user was already logged in. */ -export default function (email = 'expensify.testuser@trashmail.de'): Promise { +export default function (): Promise { const waitForBeginSignInToFinish = (): Promise => new Promise((resolve) => { const id = Onyx.connect({ @@ -31,7 +48,7 @@ export default function (email = 'expensify.testuser@trashmail.de'): Promise { + return new Promise((resolve, reject) => { const connectionId = Onyx.connect({ key: ONYXKEYS.SESSION, callback: (session) => { @@ -40,22 +57,19 @@ export default function (email = 'expensify.testuser@trashmail.de'): Promise { - // Get OTP code - console.debug('[E2E] Waiting for OTP…'); - E2EClient.getOTPCode().then((otp) => { - // Complete sign in - console.debug('[E2E] Completing sign in with otp code', otp); - Session.signIn(otp); + Authenticate(e2eUserCredentials) + .then(() => { + console.debug('[E2E] Signed in finished!'); + return waitForBeginSignInToFinish(); + }) + .catch((error) => { + console.error('[E2E] Error while signing in', error); + reject(error); }); - }); - } else { - // signal that auth was completed - resolve(neededLogin); - Onyx.disconnect(connectionId); } + // signal that auth was completed + resolve(neededLogin); + Onyx.disconnect(connectionId); }, }); }); diff --git a/src/libs/E2E/reactNativeLaunchingTest.ts b/src/libs/E2E/reactNativeLaunchingTest.ts index dc687c61eb0b..b2b19ed865e5 100644 --- a/src/libs/E2E/reactNativeLaunchingTest.ts +++ b/src/libs/E2E/reactNativeLaunchingTest.ts @@ -45,6 +45,7 @@ E2EClient.getTestConfig() .then((config): Promise | undefined => { const test = tests[config.name]; if (!test) { + console.error(`[E2E] Test '${config.name}' not found`); // instead of throwing, report the error to the server, which is better for DX return E2EClient.submitTestResults({ name: config.name, From 645fef1c71cde4895684bf783c3dce94228afa05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Sun, 21 Jan 2024 14:59:15 +0100 Subject: [PATCH 256/391] fix: enabled e2e login by merging data into onyx --- src/libs/E2E/actions/e2eLogin.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/libs/E2E/actions/e2eLogin.ts b/src/libs/E2E/actions/e2eLogin.ts index 6f8672bd0b2d..741146837a16 100644 --- a/src/libs/E2E/actions/e2eLogin.ts +++ b/src/libs/E2E/actions/e2eLogin.ts @@ -1,3 +1,5 @@ +/* eslint-disable rulesdir/prefer-actions-set-data */ + /* eslint-disable rulesdir/prefer-onyx-connect-in-libs */ import Config from 'react-native-config'; import Onyx from 'react-native-onyx'; @@ -58,7 +60,11 @@ export default function (): Promise { // authenticate with a predefined user console.debug('[E2E] Signing in…'); Authenticate(e2eUserCredentials) - .then(() => { + .then((response) => { + Onyx.merge(ONYXKEYS.SESSION, { + authToken: response.authToken, + email: e2eUserCredentials.email, + }); console.debug('[E2E] Signed in finished!'); return waitForBeginSignInToFinish(); }) From fea9d1bc1222e6df11e698cf43476e5ddb7afc04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Sun, 21 Jan 2024 15:49:06 +0100 Subject: [PATCH 257/391] wip: NetworkInterceptor --- package-lock.json | 40 +------ package.json | 3 +- src/libs/E2E/NetworkInterceptor.ts | 130 +++++++++++++++++++++++ src/libs/E2E/client.ts | 93 +++++++++------- src/libs/E2E/reactNativeLaunchingTest.ts | 16 +++ src/libs/E2E/types.ts | 11 +- tests/e2e/server/index.js | 82 ++++++-------- tests/e2e/server/routes.js | 6 +- 8 files changed, 248 insertions(+), 133 deletions(-) create mode 100644 src/libs/E2E/NetworkInterceptor.ts diff --git a/package-lock.json b/package-lock.json index 0c97fc9f3426..75d3a78776fe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -123,8 +123,7 @@ "save": "^2.4.0", "semver": "^7.5.2", "shim-keyboard-event-key": "^1.0.3", - "underscore": "^1.13.1", - "xml2js": "^0.6.2" + "underscore": "^1.13.1" }, "devDependencies": { "@actions/core": "1.10.0", @@ -53334,27 +53333,6 @@ "uuid": "dist/bin/uuid" } }, - "node_modules/xml2js": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", - "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==", - "dependencies": { - "sax": ">=0.6.0", - "xmlbuilder": "~11.0.0" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/xml2js/node_modules/xmlbuilder": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", - "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", - "license": "MIT", - "engines": { - "node": ">=4.0" - } - }, "node_modules/xmlbuilder": { "version": "14.0.0", "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-14.0.0.tgz", @@ -91844,22 +91822,6 @@ } } }, - "xml2js": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", - "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==", - "requires": { - "sax": ">=0.6.0", - "xmlbuilder": "~11.0.0" - }, - "dependencies": { - "xmlbuilder": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", - "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==" - } - } - }, "xmlbuilder": { "version": "14.0.0", "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-14.0.0.tgz", diff --git a/package.json b/package.json index 30c2f4985fd9..f51450a8dea4 100644 --- a/package.json +++ b/package.json @@ -171,8 +171,7 @@ "save": "^2.4.0", "semver": "^7.5.2", "shim-keyboard-event-key": "^1.0.3", - "underscore": "^1.13.1", - "xml2js": "^0.6.2" + "underscore": "^1.13.1" }, "devDependencies": { "@actions/core": "1.10.0", diff --git a/src/libs/E2E/NetworkInterceptor.ts b/src/libs/E2E/NetworkInterceptor.ts new file mode 100644 index 000000000000..f907bfd1aee0 --- /dev/null +++ b/src/libs/E2E/NetworkInterceptor.ts @@ -0,0 +1,130 @@ +/* eslint-disable @lwc/lwc/no-async-await */ +import type {NetworkCacheMap} from './types'; + +const LOG_TAG = `[E2E][NetworkInterceptor]`; +// Requests with these headers will be ignored: +const IGNORE_REQUEST_HEADERS = ['X-E2E-Server-Request']; + +let globalResolveIsNetworkInterceptorInstalled: () => void; +let globalRejectIsNetworkInterceptorInstalled: (error: Error) => void; +const globalIsNetworkInterceptorInstalledPromise = new Promise((resolve, reject) => { + globalResolveIsNetworkInterceptorInstalled = resolve; + globalRejectIsNetworkInterceptorInstalled = reject; +}); +let networkCache: NetworkCacheMap | null = null; + +/** + * This function hashes the arguments of fetch. + */ +function hashFetchArgs(args: Parameters) { + const [url, options] = args; + return JSON.stringify({url, options}); +} + +/** + * The headers of a fetch request can be passed as an array of tuples or as an object. + * This function converts the headers to an object. + */ +function getFetchRequestHeadersAsObject(fetchRequest: RequestInit): Record { + const headers: Record = {}; + if (Array.isArray(fetchRequest.headers)) { + fetchRequest.headers.forEach(([key, value]) => { + headers[key] = value; + }); + } else if (typeof fetchRequest.headers === 'object') { + Object.entries(fetchRequest.headers).forEach(([key, value]) => { + headers[key] = value; + }); + } + return headers; +} + +/** + * This function extracts the RequestInit from the arguments of fetch. + * It is needed because the arguments can be passed in different ways. + */ +function fetchArgsGetRequestInit(args: Parameters): RequestInit { + const [firstArg, secondArg] = args; + if (typeof firstArg === 'string' || (typeof firstArg === 'object' && firstArg instanceof URL)) { + if (secondArg == null) { + return {}; + } + return secondArg; + } + return firstArg; +} + +function fetchArgsGetUrl(args: Parameters): string { + const [firstArg] = args; + if (typeof firstArg === 'string') { + return firstArg; + } + if (typeof firstArg === 'object' && firstArg instanceof URL) { + return firstArg.href; + } + if (typeof firstArg === 'object' && firstArg instanceof Request) { + return firstArg.url; + } + throw new Error('Could not get url from fetch args'); +} + +export default function installNetworkInterceptor( + getNetworkCache: () => Promise, + updateNetworkCache: (networkCache: NetworkCacheMap) => Promise, + shouldReturnRecordedResponse: boolean, +) { + console.debug(LOG_TAG, 'installing with shouldReturnRecordedResponse:', shouldReturnRecordedResponse); + const originalFetch = global.fetch; + + if (networkCache == null && shouldReturnRecordedResponse) { + console.debug(LOG_TAG, 'fetching network cache …'); + getNetworkCache().then((newCache) => { + networkCache = newCache; + globalResolveIsNetworkInterceptorInstalled(); + console.debug(LOG_TAG, 'network cache fetched!'); + }, globalRejectIsNetworkInterceptorInstalled); + } else { + networkCache = {}; + globalResolveIsNetworkInterceptorInstalled(); + } + + // @ts-expect-error Fetch global types weirdly include URL + global.fetch = async (...args: Parameters) => { + const headers = getFetchRequestHeadersAsObject(fetchArgsGetRequestInit(args)); + const url = fetchArgsGetUrl(args); + // Check if headers contain any of the ignored headers, or if react native metro server: + if (IGNORE_REQUEST_HEADERS.some((header) => headers[header] != null) || url.includes('8081')) { + return originalFetch(...args); + } + + await globalIsNetworkInterceptorInstalledPromise; + + const hash = hashFetchArgs(args); + if (shouldReturnRecordedResponse && networkCache?.[hash] != null) { + console.debug(LOG_TAG, 'Returning recorded response for hash:', hash); + const {response} = networkCache[hash]; + return Promise.resolve(response); + } + + return originalFetch(...args) + .then((res) => { + if (networkCache != null) { + console.debug(LOG_TAG, 'Updating network cache for hash:'); + networkCache[hash] = { + // @ts-expect-error TODO: The user could pass these differently, add better handling + url: args[0], + // @ts-expect-error TODO: The user could pass these differently, add better handling + options: args[1], + response: res, + }; + // Send the network cache to the test server: + return updateNetworkCache(networkCache).then(() => res); + } + return res; + }) + .then((res) => { + console.debug(LOG_TAG, 'Network cache updated!'); + return res; + }); + }; +} diff --git a/src/libs/E2E/client.ts b/src/libs/E2E/client.ts index 74f293be2839..30822063b558 100644 --- a/src/libs/E2E/client.ts +++ b/src/libs/E2E/client.ts @@ -1,5 +1,6 @@ import Config from '../../../tests/e2e/config'; import Routes from '../../../tests/e2e/server/routes'; +import type {NetworkCacheMap} from './types'; type TestResult = { name: string; @@ -24,26 +25,31 @@ type NativeCommand = { const SERVER_ADDRESS = `http://localhost:${Config.SERVER_PORT}`; -/** - * Submits a test result to the server. - * Note: a test can have multiple test results. - */ -const submitTestResults = (testResult: TestResult): Promise => { - console.debug(`[E2E] Submitting test result '${testResult.name}'…`); - return fetch(`${SERVER_ADDRESS}${Routes.testResults}`, { +const defaultHeaders = { + // eslint-disable-next-line @typescript-eslint/naming-convention + 'X-E2E-Server-Request': 'true', +}; + +const defaultRequestInit: RequestInit = { + headers: defaultHeaders, +}; + +const sendRequest = (url: string, data: Record): Promise => + fetch(url, { method: 'POST', headers: { // eslint-disable-next-line @typescript-eslint/naming-convention 'Content-Type': 'application/json', + ...defaultHeaders, }, - body: JSON.stringify(testResult), + body: JSON.stringify(data), }).then((res) => { if (res.status === 200) { - console.debug(`[E2E] Test result '${testResult.name}' submitted successfully`); - return; + return res; } - const errorMsg = `Test result submission failed with status code ${res.status}`; - res.json() + const errorMsg = `[E2E] Client failed to send request to "${url}". Returned status: ${res.status}`; + return res + .json() .then((responseText) => { throw new Error(`${errorMsg}: ${responseText}`); }) @@ -51,14 +57,24 @@ const submitTestResults = (testResult: TestResult): Promise => { throw new Error(errorMsg); }); }); + +/** + * Submits a test result to the server. + * Note: a test can have multiple test results. + */ +const submitTestResults = (testResult: TestResult): Promise => { + console.debug(`[E2E] Submitting test result '${testResult.name}'…`); + return sendRequest(`${SERVER_ADDRESS}${Routes.testResults}`, testResult).then(() => { + console.debug(`[E2E] Test result '${testResult.name}' submitted successfully`); + }); }; -const submitTestDone = () => fetch(`${SERVER_ADDRESS}${Routes.testDone}`); +const submitTestDone = () => fetch(`${SERVER_ADDRESS}${Routes.testDone}`, defaultRequestInit); let currentActiveTestConfig: TestConfig | null = null; const getTestConfig = (): Promise => - fetch(`${SERVER_ADDRESS}${Routes.testConfig}`) + fetch(`${SERVER_ADDRESS}${Routes.testConfig}`, defaultRequestInit) .then((res: Response): Promise => res.json()) .then((config: TestConfig) => { currentActiveTestConfig = config; @@ -67,32 +83,30 @@ const getTestConfig = (): Promise => const getCurrentActiveTestConfig = () => currentActiveTestConfig; -const sendNativeCommand = (payload: NativeCommand) => - fetch(`${SERVER_ADDRESS}${Routes.testNativeCommand}`, { - method: 'POST', - headers: { - // eslint-disable-next-line @typescript-eslint/naming-convention - 'Content-Type': 'application/json', - }, - body: JSON.stringify(payload), - }).then((res) => { - if (res.status === 200) { - return true; - } - const errorMsg = `Sending native command failed with status code ${res.status}`; - res.json() - .then((responseText) => { - throw new Error(`${errorMsg}: ${responseText}`); - }) - .catch(() => { - throw new Error(errorMsg); - }); +const sendNativeCommand = (payload: NativeCommand) => { + console.debug(`[E2E] Sending native command '${payload.actionName}'…`); + return sendRequest(`${SERVER_ADDRESS}${Routes.testNativeCommand}`, payload).then(() => { + console.debug(`[E2E] Native command '${payload.actionName}' sent successfully`); }); +}; -const getOTPCode = (): Promise => - fetch(`${SERVER_ADDRESS}${Routes.getOtpCode}`) - .then((res: Response): Promise => res.json()) - .then((otp: string) => otp); +const updateNetworkCache = (appInstanceId: string, networkCache: NetworkCacheMap) => { + console.debug('[E2E] Updating network cache…'); + return sendRequest(`${SERVER_ADDRESS}${Routes.testUpdateNetworkCache}`, { + appInstanceId, + cache: networkCache, + }).then(() => { + console.debug('[E2E] Network cache updated successfully'); + }); +}; + +const getNetworkCache = (appInstanceId: string): Promise => + sendRequest(`${SERVER_ADDRESS}${Routes.testGetNetworkCache}`, {appInstanceId}) + .then((res): Promise => res.json()) + .then((networkCache: NetworkCacheMap) => { + console.debug('[E2E] Network cache fetched successfully'); + return networkCache; + }); export default { submitTestResults, @@ -100,5 +114,6 @@ export default { getTestConfig, getCurrentActiveTestConfig, sendNativeCommand, - getOTPCode, + updateNetworkCache, + getNetworkCache, }; diff --git a/src/libs/E2E/reactNativeLaunchingTest.ts b/src/libs/E2E/reactNativeLaunchingTest.ts index b2b19ed865e5..937f69114918 100644 --- a/src/libs/E2E/reactNativeLaunchingTest.ts +++ b/src/libs/E2E/reactNativeLaunchingTest.ts @@ -8,8 +8,10 @@ import type {ValueOf} from 'type-fest'; import * as Metrics from '@libs/Metrics'; import Performance from '@libs/Performance'; +import Config from 'react-native-config'; import E2EConfig from '../../../tests/e2e/config'; import E2EClient from './client'; +import installNetworkInterceptor from './NetworkInterceptor'; type Tests = Record, () => void>; @@ -22,6 +24,12 @@ if (!Metrics.canCapturePerformanceMetrics()) { throw new Error('Performance module not available! Please set CAPTURE_METRICS=true in your environment file!'); } +const appInstanceId = Config.E2E_BRANCH +if (!appInstanceId) { + throw new Error('E2E_BRANCH not set in environment file!'); +} + + // import your test here, define its name and config first in e2e/config.js const tests: Tests = { [E2EConfig.TEST_NAMES.AppStartTime]: require('./tests/appStartTimeTest.e2e').default, @@ -41,6 +49,14 @@ const appReady = new Promise((resolve) => { }); }); +// Install the network interceptor +installNetworkInterceptor( + () => E2EClient.getNetworkCache(appInstanceId), + (networkCache) => E2EClient.updateNetworkCache(appInstanceId, networkCache), + // TODO: this needs to be set my the launch args, which we aren't using yet … + false, +) + E2EClient.getTestConfig() .then((config): Promise | undefined => { const test = tests[config.name]; diff --git a/src/libs/E2E/types.ts b/src/libs/E2E/types.ts index fcdfa01d7132..8a9529bd62d6 100644 --- a/src/libs/E2E/types.ts +++ b/src/libs/E2E/types.ts @@ -4,4 +4,13 @@ type SigninParams = { type IsE2ETestSession = () => boolean; -export type {SigninParams, IsE2ETestSession}; +type NetworkCacheMap = Record< + string, // hash + { + url: string; + options: RequestInit; + response: Response; + } +>; + +export type {SigninParams, IsE2ETestSession, NetworkCacheMap}; diff --git a/tests/e2e/server/index.js b/tests/e2e/server/index.js index b2c2f1853320..c2365f259bb7 100644 --- a/tests/e2e/server/index.js +++ b/tests/e2e/server/index.js @@ -54,39 +54,6 @@ const createListenerState = () => { return [listeners, addListener]; }; -const https = require('https'); - -function simpleHttpRequest(url, method = 'GET') { - return new Promise((resolve, reject) => { - const req = https.request(url, {method}, (res) => { - let data = ''; - res.on('data', (chunk) => { - data += chunk; - }); - res.on('end', () => { - resolve(data); - }); - }); - req.on('error', reject); - req.end(); - }); -} - -const parseString = require('xml2js').parseString; - -function simpleXMLToJSON(xml) { - // using xml2js - return new Promise((resolve, reject) => { - parseString(xml, (err, result) => { - if (err) { - reject(err); - return; - } - resolve(result); - }); - }); -} - /** * The test result object that a client might submit to the server. * @typedef TestResult @@ -117,6 +84,7 @@ const createServerInstance = () => { const [testDoneListeners, addTestDoneListener] = createListenerState(); let activeTestConfig; + const networkCache = {}; /** * @param {TestConfig} testConfig @@ -179,24 +147,36 @@ const createServerInstance = () => { break; } - case Routes.getOtpCode: { - // Wait 10 seconds for the email to arrive - setTimeout(() => { - simpleHttpRequest('https://www.trashmail.de/inbox-api.php?name=expensify.testuser') - .then(simpleXMLToJSON) - .then(({feed}) => { - const firstEmailID = feed.entry[0].id; - // Get email content: - return simpleHttpRequest(`https://www.trashmail.de/mail-api.php?name=expensify.testuser&id=${firstEmailID}`).then(simpleXMLToJSON); - }) - .then(({feed}) => { - const content = feed.entry[0].content[0]; - // content is a string, find code using regex based on text "Use 259463 to sign" - const otpCode = content.match(/Use (\d+) to sign/)[1]; - console.debug('otpCode', otpCode); - res.end(otpCode); - }); - }, 10000); + case Routes.testGetNetworkCache: { + getPostJSONRequestData(req, res).then((data) => { + const appInstanceId = data && data.appInstanceId; + if (!appInstanceId) { + res.statusCode = 400; + res.end('Invalid request missing appInstanceId'); + return; + } + + const cachedData = networkCache[appInstanceId] || {}; + res.end(JSON.stringify(cachedData)); + }); + + break; + } + + case Routes.testUpdateNetworkCache: { + getPostJSONRequestData(req, res).then((data) => { + const appInstanceId = data && data.appInstanceId; + const cache = data && data.cache; + if (!appInstanceId || !cache) { + res.statusCode = 400; + res.end('Invalid request missing appInstanceId or cache'); + return; + } + + networkCache[appInstanceId] = cache; + res.end('ok'); + }); + break; } diff --git a/tests/e2e/server/routes.js b/tests/e2e/server/routes.js index 1128b5b0f8dc..0d23866ec808 100644 --- a/tests/e2e/server/routes.js +++ b/tests/e2e/server/routes.js @@ -11,5 +11,9 @@ module.exports = { // Commands to execute from the host machine (there are pre-defined types like scroll or type) testNativeCommand: '/test_native_command', - getOtpCode: '/get_otp_code', + // Updates the network cache + testUpdateNetworkCache: '/test_update_network_cache', + + // Gets the network cache + testGetNetworkCache: '/test_get_network_cache', }; From e5e517977129f909a801edc70003889416733290 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Sun, 21 Jan 2024 16:06:22 +0100 Subject: [PATCH 258/391] e2e; finish network interceptor implementation --- package-lock.json | 16 ++++++++++++++++ package.json | 1 + src/App.js | 4 +++- src/libs/E2E/reactNativeLaunchingTest.ts | 6 +++--- src/libs/E2E/utils/LaunchArgs.ts | 8 ++++++++ src/libs/E2E/{ => utils}/NetworkInterceptor.ts | 2 +- tests/e2e/config.js | 4 ++++ tests/e2e/testRunner.js | 8 ++++++-- tests/e2e/utils/launchApp.js | 10 +++++++--- 9 files changed, 49 insertions(+), 10 deletions(-) create mode 100644 src/libs/E2E/utils/LaunchArgs.ts rename src/libs/E2E/{ => utils}/NetworkInterceptor.ts (98%) diff --git a/package-lock.json b/package-lock.json index 75d3a78776fe..0c667a41ae1e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -91,6 +91,7 @@ "react-native-image-picker": "^5.1.0", "react-native-image-size": "git+https://github.com/Expensify/react-native-image-size#8393b7e58df6ff65fd41f60aee8ece8822c91e2b", "react-native-key-command": "^1.0.6", + "react-native-launch-arguments": "^4.0.2", "react-native-linear-gradient": "^2.8.1", "react-native-localize": "^2.2.6", "react-native-modal": "^13.0.0", @@ -45116,6 +45117,15 @@ "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==" }, + "node_modules/react-native-launch-arguments": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/react-native-launch-arguments/-/react-native-launch-arguments-4.0.2.tgz", + "integrity": "sha512-OaXXOG9jVrmb66cTV8wPdhKxaSVivOBKuYr8wgKCM5uAHkY5B6SkvydgJ3B/x8uGoWqtr87scSYYDm4MMU4rSg==", + "peerDependencies": { + "react": ">=16.8.1", + "react-native": ">=0.60.0-rc.0 <1.0.x" + } + }, "node_modules/react-native-linear-gradient": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/react-native-linear-gradient/-/react-native-linear-gradient-2.8.1.tgz", @@ -86024,6 +86034,12 @@ } } }, + "react-native-launch-arguments": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/react-native-launch-arguments/-/react-native-launch-arguments-4.0.2.tgz", + "integrity": "sha512-OaXXOG9jVrmb66cTV8wPdhKxaSVivOBKuYr8wgKCM5uAHkY5B6SkvydgJ3B/x8uGoWqtr87scSYYDm4MMU4rSg==", + "requires": {} + }, "react-native-linear-gradient": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/react-native-linear-gradient/-/react-native-linear-gradient-2.8.1.tgz", diff --git a/package.json b/package.json index f51450a8dea4..6214bda9fbec 100644 --- a/package.json +++ b/package.json @@ -139,6 +139,7 @@ "react-native-image-picker": "^5.1.0", "react-native-image-size": "git+https://github.com/Expensify/react-native-image-size#8393b7e58df6ff65fd41f60aee8ece8822c91e2b", "react-native-key-command": "^1.0.6", + "react-native-launch-arguments": "^4.0.2", "react-native-linear-gradient": "^2.8.1", "react-native-localize": "^2.2.6", "react-native-modal": "^13.0.0", diff --git a/src/App.js b/src/App.js index 3553900bbc7f..3ad895eb3447 100644 --- a/src/App.js +++ b/src/App.js @@ -45,7 +45,9 @@ LogBox.ignoreLogs([ const fill = {flex: 1}; -function App() { +function App(props) { + console.log('App.js: App(): props:', props); + useDefaultDragAndDrop(); OnyxUpdateManager(); return ( diff --git a/src/libs/E2E/reactNativeLaunchingTest.ts b/src/libs/E2E/reactNativeLaunchingTest.ts index 937f69114918..cd17cf4ce9e9 100644 --- a/src/libs/E2E/reactNativeLaunchingTest.ts +++ b/src/libs/E2E/reactNativeLaunchingTest.ts @@ -11,7 +11,8 @@ import Performance from '@libs/Performance'; import Config from 'react-native-config'; import E2EConfig from '../../../tests/e2e/config'; import E2EClient from './client'; -import installNetworkInterceptor from './NetworkInterceptor'; +import installNetworkInterceptor from './utils/NetworkInterceptor'; +import LaunchArgs from './utils/LaunchArgs'; type Tests = Record, () => void>; @@ -53,8 +54,7 @@ const appReady = new Promise((resolve) => { installNetworkInterceptor( () => E2EClient.getNetworkCache(appInstanceId), (networkCache) => E2EClient.updateNetworkCache(appInstanceId, networkCache), - // TODO: this needs to be set my the launch args, which we aren't using yet … - false, + LaunchArgs.mockNetwork ?? false ) E2EClient.getTestConfig() diff --git a/src/libs/E2E/utils/LaunchArgs.ts b/src/libs/E2E/utils/LaunchArgs.ts new file mode 100644 index 000000000000..4e452d766eff --- /dev/null +++ b/src/libs/E2E/utils/LaunchArgs.ts @@ -0,0 +1,8 @@ +import {LaunchArguments} from 'react-native-launch-arguments'; + +type ExpectedArgs = { + mockNetwork?: boolean; +}; +const LaunchArgs = LaunchArguments.value(); + +export default LaunchArgs; diff --git a/src/libs/E2E/NetworkInterceptor.ts b/src/libs/E2E/utils/NetworkInterceptor.ts similarity index 98% rename from src/libs/E2E/NetworkInterceptor.ts rename to src/libs/E2E/utils/NetworkInterceptor.ts index f907bfd1aee0..d362c2925355 100644 --- a/src/libs/E2E/NetworkInterceptor.ts +++ b/src/libs/E2E/utils/NetworkInterceptor.ts @@ -1,5 +1,5 @@ /* eslint-disable @lwc/lwc/no-async-await */ -import type {NetworkCacheMap} from './types'; +import type {NetworkCacheMap} from '@libs/E2E/types'; const LOG_TAG = `[E2E][NetworkInterceptor]`; // Requests with these headers will be ignored: diff --git a/tests/e2e/config.js b/tests/e2e/config.js index 41c1668fb6ba..d782ec4316e5 100644 --- a/tests/e2e/config.js +++ b/tests/e2e/config.js @@ -31,6 +31,10 @@ module.exports = { ENTRY_FILE: 'src/libs/E2E/reactNativeLaunchingTest.ts', + // The path to the activity within the app that we want to launch. + // Note: even though we have different package _names_, this path doesn't change. + ACTIVITY_PATH: 'com.expensify.chat.MainActivity', + // The port of the testing server that communicates with the app SERVER_PORT: 4723, diff --git a/tests/e2e/testRunner.js b/tests/e2e/testRunner.js index 880e8641dd6f..98faff32397d 100644 --- a/tests/e2e/testRunner.js +++ b/tests/e2e/testRunner.js @@ -335,7 +335,9 @@ const runTests = async () => { await killApp('android', config.MAIN_APP_PACKAGE); Logger.log('Starting main app'); - await launchApp('android', config.MAIN_APP_PACKAGE); + await launchApp('android', config.MAIN_APP_PACKAGE, config.ACTIVITY_PATH, { + mockNetwork: true, + }); // Wait for a test to finish by waiting on its done call to the http server try { @@ -359,7 +361,9 @@ const runTests = async () => { await killApp('android', config.MAIN_APP_PACKAGE); Logger.log('Starting delta app'); - await launchApp('android', config.DELTA_APP_PACKAGE); + await launchApp('android', config.DELTA_APP_PACKAGE, config.ACTIVITY_PATH, { + mockNetwork: true, + }); // Wait for a test to finish by waiting on its done call to the http server try { diff --git a/tests/e2e/utils/launchApp.js b/tests/e2e/utils/launchApp.js index e0726d081086..f63e2e71cd8b 100644 --- a/tests/e2e/utils/launchApp.js +++ b/tests/e2e/utils/launchApp.js @@ -1,11 +1,15 @@ -const {APP_PACKAGE} = require('../config'); +/* eslint-disable rulesdir/prefer-underscore-method */ +const {APP_PACKAGE, ACTIVITY_PATH} = require('../config'); const execAsync = require('./execAsync'); -module.exports = function (platform = 'android', packageName = APP_PACKAGE) { +module.exports = function (platform = 'android', packageName = APP_PACKAGE, activityPath = ACTIVITY_PATH, launchArgs = {}) { if (platform !== 'android') { throw new Error(`launchApp() missing implementation for platform: ${platform}`); } // Use adb to start the app - return execAsync(`adb shell monkey -p ${packageName} -c android.intent.category.LAUNCHER 1`); + const launchArgsString = Object.keys(launchArgs) + .map((key) => `${typeof launchArgs[key] === 'boolean' ? '--ez' : '--es'} ${key} ${launchArgs[key]}`) + .join(' '); + return execAsync(`adb shell am start -n ${packageName}/${activityPath} ${launchArgsString}`); }; From 8fc25f44955793351fea3c1d443f0b7c56c80413 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Sun, 21 Jan 2024 16:09:21 +0100 Subject: [PATCH 259/391] e2e: reduce logging of NetworkInterceptor --- src/libs/E2E/utils/NetworkInterceptor.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/E2E/utils/NetworkInterceptor.ts b/src/libs/E2E/utils/NetworkInterceptor.ts index d362c2925355..260135319771 100644 --- a/src/libs/E2E/utils/NetworkInterceptor.ts +++ b/src/libs/E2E/utils/NetworkInterceptor.ts @@ -101,7 +101,7 @@ export default function installNetworkInterceptor( const hash = hashFetchArgs(args); if (shouldReturnRecordedResponse && networkCache?.[hash] != null) { - console.debug(LOG_TAG, 'Returning recorded response for hash:', hash); + console.debug(LOG_TAG, 'Returning recorded response for url:', url); const {response} = networkCache[hash]; return Promise.resolve(response); } From e571659ed904842b5306296aa84181eff78ec2eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Sun, 21 Jan 2024 16:10:37 +0100 Subject: [PATCH 260/391] remove debug changes --- src/App.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/App.js b/src/App.js index 3ad895eb3447..3553900bbc7f 100644 --- a/src/App.js +++ b/src/App.js @@ -45,9 +45,7 @@ LogBox.ignoreLogs([ const fill = {flex: 1}; -function App(props) { - console.log('App.js: App(): props:', props); - +function App() { useDefaultDragAndDrop(); OnyxUpdateManager(); return ( From 7faf409807187502c6ffe1c593f72d9fdbb350fd Mon Sep 17 00:00:00 2001 From: Fitsum Abebe Date: Sun, 21 Jan 2024 23:48:14 +0300 Subject: [PATCH 261/391] updated to run our effect only when there is new message received --- src/pages/home/report/ReportActionsList.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/home/report/ReportActionsList.js b/src/pages/home/report/ReportActionsList.js index 5cee5a77ea46..17ea4c99ec88 100644 --- a/src/pages/home/report/ReportActionsList.js +++ b/src/pages/home/report/ReportActionsList.js @@ -412,7 +412,7 @@ function ReportActionsList({ }, [calculateUnreadMarker, report.lastReadTime, messageManuallyMarkedUnread]); const onVisibilityChange = useCallback(() => { - if (!Visibility.isVisible() || scrollingVerticalOffset.current >= MSG_VISIBLE_THRESHOLD || !ReportUtils.isUnread(report)) { + if (!Visibility.isVisible() || scrollingVerticalOffset.current >= MSG_VISIBLE_THRESHOLD || userActiveSince.current > (report.lastVisibleActionCreated || '')) { return; } From 1a022dafedd9c745eadd87bfa44cd34c580ddd80 Mon Sep 17 00:00:00 2001 From: Fitsum Abebe Date: Mon, 22 Jan 2024 00:13:58 +0300 Subject: [PATCH 262/391] refined logic --- src/pages/home/report/ReportActionsList.js | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/src/pages/home/report/ReportActionsList.js b/src/pages/home/report/ReportActionsList.js index 17ea4c99ec88..66b0decedb42 100644 --- a/src/pages/home/report/ReportActionsList.js +++ b/src/pages/home/report/ReportActionsList.js @@ -143,6 +143,7 @@ function ReportActionsList({ const route = useRoute(); const opacity = useSharedValue(0); const userActiveSince = useRef(null); + const userInactiveSince = useRef(null); const markerInit = () => { if (!cacheUnreadMarkers.has(report.reportID)) { @@ -412,7 +413,23 @@ function ReportActionsList({ }, [calculateUnreadMarker, report.lastReadTime, messageManuallyMarkedUnread]); const onVisibilityChange = useCallback(() => { - if (!Visibility.isVisible() || scrollingVerticalOffset.current >= MSG_VISIBLE_THRESHOLD || userActiveSince.current > (report.lastVisibleActionCreated || '')) { + if (!Visibility.isVisible()) { + userInactiveSince.current = DateUtils.getDBTime(); + return; + } + + if ( + scrollingVerticalOffset.current >= MSG_VISIBLE_THRESHOLD || + !( + sortedVisibleReportActions && + _.some( + sortedVisibleReportActions, + (reportAction) => + userInactiveSince.current < reportAction.created && + (ReportActionsUtils.isReportPreviewAction(reportAction) ? !reportAction.childLastActorAccountID : reportAction.actorAccountID) !== Report.getCurrentUserAccountID(), + ) + ) + ) { return; } @@ -421,7 +438,7 @@ function ReportActionsList({ setCurrentUnreadMarker(null); cacheUnreadMarkers.delete(report.reportID); calculateUnreadMarker(); - }, [calculateUnreadMarker, report]); + }, [calculateUnreadMarker, report, sortedVisibleReportActions]); useEffect(() => { const unsubscribeVisibilityListener = Visibility.onVisibilityChange(onVisibilityChange); From c2547efcd3c7a24ffa6b938d6bedabfb7943c69e Mon Sep 17 00:00:00 2001 From: Tsaqif Date: Mon, 22 Jan 2024 14:18:39 +0700 Subject: [PATCH 263/391] Preserve the isNavigating parameter Signed-off-by: Tsaqif --- src/libs/actions/Modal.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/libs/actions/Modal.ts b/src/libs/actions/Modal.ts index 1c6fbe74ef01..e7f0ef4df098 100644 --- a/src/libs/actions/Modal.ts +++ b/src/libs/actions/Modal.ts @@ -1,9 +1,10 @@ import Onyx from 'react-native-onyx'; import ONYXKEYS from '@src/ONYXKEYS'; -const closeModals: Array<() => void> = []; +const closeModals: Array<(isNavigating?: boolean) => void> = []; let onModalClose: null | (() => void); +let isNavigate: undefined | boolean; /** * Allows other parts of the app to call modal close function @@ -28,18 +29,23 @@ function closeTop() { if (closeModals.length === 0) { return; } + if (onModalClose) { + closeModals[closeModals.length - 1](isNavigate); + return; + } closeModals[closeModals.length - 1](); } /** * Close modal in other parts of the app */ -function close(onModalCloseCallback: () => void) { +function close(onModalCloseCallback: () => void, isNavigating = true) { if (closeModals.length === 0) { onModalCloseCallback(); return; } onModalClose = onModalCloseCallback; + isNavigate = isNavigating; closeTop(); } From cf7c98dec86c7a202453e606d35481e329a40ee2 Mon Sep 17 00:00:00 2001 From: Tsaqif Date: Mon, 22 Jan 2024 15:15:36 +0700 Subject: [PATCH 264/391] add Reset isNavigate value Signed-off-by: Tsaqif --- src/libs/actions/Modal.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libs/actions/Modal.ts b/src/libs/actions/Modal.ts index e7f0ef4df098..71ba850e721f 100644 --- a/src/libs/actions/Modal.ts +++ b/src/libs/actions/Modal.ts @@ -59,6 +59,7 @@ function onModalDidClose() { } onModalClose(); onModalClose = null; + isNavigate = undefined; } /** From e9d8f4d7e996fa8d520e2c16a85ddd376a266745 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Mon, 22 Jan 2024 09:48:01 +0100 Subject: [PATCH 265/391] Add clear errors utils --- src/components/Form/FormProvider.tsx | 4 ++-- src/libs/actions/FormActions.ts | 14 +++++++++++--- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/components/Form/FormProvider.tsx b/src/components/Form/FormProvider.tsx index 10e4952a7896..b71b611e60e5 100644 --- a/src/components/Form/FormProvider.tsx +++ b/src/components/Form/FormProvider.tsx @@ -94,9 +94,9 @@ function FormProvider( const trimmedStringValues = ValidationUtils.prepareValues(values); if (shouldClearServerError) { - FormActions.setErrors(formID, null); + FormActions.clearErrors(formID); } - FormActions.setErrorFields(formID, null); + FormActions.clearErrorFields(formID); const validateErrors = validate?.(trimmedStringValues) ?? {}; diff --git a/src/libs/actions/FormActions.ts b/src/libs/actions/FormActions.ts index 9daaa4fef20c..00ad3652c665 100644 --- a/src/libs/actions/FormActions.ts +++ b/src/libs/actions/FormActions.ts @@ -9,14 +9,22 @@ function setIsLoading(formID: OnyxFormKey, isLoading: boolean) { Onyx.merge(formID, {isLoading}); } -function setErrors(formID: OnyxFormKey, errors?: OnyxCommon.Errors | null) { +function setErrors(formID: OnyxFormKey, errors: OnyxCommon.Errors) { Onyx.merge(formID, {errors}); } -function setErrorFields(formID: OnyxFormKey, errorFields?: OnyxCommon.ErrorFields | null) { +function setErrorFields(formID: OnyxFormKey, errorFields: OnyxCommon.ErrorFields) { Onyx.merge(formID, {errorFields}); } +function clearErrors(formID: OnyxFormKey) { + Onyx.merge(formID, {errors: null}); +} + +function clearErrorFields(formID: OnyxFormKey) { + Onyx.merge(formID, {errorFields: null}); +} + function setDraftValues(formID: OnyxFormKeyWithoutDraft, draftValues: NullishDeep) { Onyx.merge(FormUtils.getDraftKey(formID), draftValues); } @@ -25,4 +33,4 @@ function clearDraftValues(formID: OnyxFormKeyWithoutDraft) { Onyx.set(FormUtils.getDraftKey(formID), {}); } -export {setDraftValues, setErrorFields, setErrors, setIsLoading, clearDraftValues}; +export {setDraftValues, setErrorFields, setErrors, clearErrors, clearErrorFields, setIsLoading, clearDraftValues}; From 91a112518f6d0a5ebbf99362cd1799c075ea614f Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Mon, 22 Jan 2024 09:55:10 +0100 Subject: [PATCH 266/391] Change the order of deps back to original --- src/components/Form/FormWrapper.tsx | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/components/Form/FormWrapper.tsx b/src/components/Form/FormWrapper.tsx index c12c9d1b5a44..cdf66d986472 100644 --- a/src/components/Form/FormWrapper.tsx +++ b/src/components/Form/FormWrapper.tsx @@ -115,25 +115,25 @@ function FormWrapper({ ), [ - formID, - style, - onSubmit, children, - isSubmitButtonVisible, - submitButtonText, + enabledWhenOffline, + errorMessage, errors, + footerContent, + formID, formState?.errorFields, formState?.isLoading, - shouldHideFixErrorsAlert, - errorMessage, - footerContent, - onFixTheErrorsLinkPressed, + isSubmitActionDangerous, + isSubmitButtonVisible, + onSubmit, + style, + styles.flex1, styles.mh0, styles.mt5, - styles.flex1, submitButtonStyles, - enabledWhenOffline, - isSubmitActionDangerous, + submitButtonText, + shouldHideFixErrorsAlert, + onFixTheErrorsLinkPressed, ], ); From 72235f71d666d4c233a84ba4cfa99cbbd8bb7f49 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Mon, 22 Jan 2024 10:21:26 +0100 Subject: [PATCH 267/391] fix: typecheck --- src/libs/OptionsListUtils.ts | 6 ++++-- src/libs/ReportUtils.ts | 6 +++--- src/libs/actions/Task.ts | 6 +----- 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 4a1a8973d2f6..5f45dcedf1cb 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -10,7 +10,7 @@ import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {Beta, Login, PersonalDetails, PersonalDetailsList, Policy, PolicyCategories, Report, ReportAction, ReportActions, Transaction} from '@src/types/onyx'; +import type {Beta, Login, PersonalDetails, PersonalDetailsList, Policy, PolicyCategories, Report, ReportAction, ReportActions, Transaction, TransactionViolation} from '@src/types/onyx'; import type {Participant} from '@src/types/onyx/IOU'; import type * as OnyxCommon from '@src/types/onyx/OnyxCommon'; import type {PolicyTaxRate, PolicyTaxRates} from '@src/types/onyx/PolicyTaxRates'; @@ -95,6 +95,7 @@ type GetOptionsConfig = { includeSelectedOptions?: boolean; includePolicyTaxRates?: boolean; policyTaxRates?: PolicyTaxRateWithDefault; + transactionViolations?: OnyxCollection; }; type MemberForList = { @@ -1383,7 +1384,7 @@ function getOptions( const filteredReports = Object.values(reports ?? {}).filter((report) => { const {parentReportID, parentReportActionID} = report ?? {}; const canGetParentReport = parentReportID && parentReportActionID && allReportActions; - const parentReportAction = canGetParentReport ? lodashGet(allReportActions, [parentReportID, parentReportActionID], {}) : {}; + const parentReportAction = canGetParentReport ? allReportActions[parentReportID][parentReportActionID] : null; const doesReportHaveViolations = betas.includes(CONST.BETAS.VIOLATIONS) && ReportUtils.doesTransactionThreadHaveViolations(report, transactionViolations, parentReportAction); return ReportUtils.shouldReportBeInOptionList({ @@ -1393,6 +1394,7 @@ function getOptions( policies, doesReportHaveViolations, isInGSDMode: false, + excludeEmptyChats: false, }); }); diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 5c236767612b..7977a35db305 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -3540,8 +3540,8 @@ function shouldHideReport(report: OnyxEntry, currentReportId: string): b /** * Checks to see if a report's parentAction is a money request that contains a violation */ -function doesTransactionThreadHaveViolations(report: Report, transactionViolations: OnyxCollection, parentReportAction: ReportAction): boolean { - if (parentReportAction.actionName !== CONST.REPORT.ACTIONS.TYPE.IOU) { +function doesTransactionThreadHaveViolations(report: OnyxEntry, transactionViolations: OnyxCollection, parentReportAction: OnyxEntry): boolean { + if (parentReportAction?.actionName !== CONST.REPORT.ACTIONS.TYPE.IOU) { return false; } const {IOUTransactionID, IOUReportID} = parentReportAction.originalMessage ?? {}; @@ -3551,7 +3551,7 @@ function doesTransactionThreadHaveViolations(report: Report, transactionViolatio if (!isCurrentUserSubmitter(IOUReportID)) { return false; } - if (report.stateNum !== CONST.REPORT.STATE_NUM.OPEN && report.stateNum !== CONST.REPORT.STATE_NUM.SUBMITTED) { + if (report?.stateNum !== CONST.REPORT.STATE_NUM.OPEN && report?.stateNum !== CONST.REPORT.STATE_NUM.SUBMITTED) { return false; } return TransactionUtils.hasViolation(IOUTransactionID, transactionViolations); diff --git a/src/libs/actions/Task.ts b/src/libs/actions/Task.ts index c03fa15fe1ae..b2f6b57f390a 100644 --- a/src/libs/actions/Task.ts +++ b/src/libs/actions/Task.ts @@ -718,11 +718,7 @@ function getShareDestination(reportID: string, reports: OnyxCollection 1; - const displayNamesWithTooltips = ReportUtils.getDisplayNamesWithTooltips( - // @ts-expect-error TODO: Remove this once OptionsListUtils (https://github.com/Expensify/App/issues/24921) is migrated to TypeScript. - OptionsListUtils.getPersonalDetailsForAccountIDs(participantAccountIDs, personalDetails), - isMultipleParticipant, - ); + const displayNamesWithTooltips = ReportUtils.getDisplayNamesWithTooltips(OptionsListUtils.getPersonalDetailsForAccountIDs(participantAccountIDs, personalDetails), isMultipleParticipant); let subtitle = ''; if (ReportUtils.isChatReport(report) && ReportUtils.isDM(report) && ReportUtils.hasSingleParticipant(report)) { From 13c2a366301d182b2e59407124c075f4bca39072 Mon Sep 17 00:00:00 2001 From: tienifr Date: Mon, 22 Jan 2024 16:29:07 +0700 Subject: [PATCH 268/391] fix: clean code --- src/components/ImageView/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ImageView/index.tsx b/src/components/ImageView/index.tsx index 3677111c09df..ec37abf6d275 100644 --- a/src/components/ImageView/index.tsx +++ b/src/components/ImageView/index.tsx @@ -115,7 +115,7 @@ function ImageView({isAuthTokenRequired = false, url, fileName, onError}: ImageV const onContainerPress = (e?: GestureResponderEvent | KeyboardEvent | SyntheticEvent) => { if (!isZoomed && !isDragging) { - if (e && 'nativeEvent' in e && 'offsetX' in e.nativeEvent) { + if (e && 'nativeEvent' in e && e.nativeEvent instanceof PointerEvent) { const {offsetX, offsetY} = e.nativeEvent; // Dividing clicked positions by the zoom scale to get coordinates From e15c9649d9b835c5b4e341122d50e2bc46f62ad1 Mon Sep 17 00:00:00 2001 From: Tsaqif Date: Mon, 22 Jan 2024 16:46:49 +0700 Subject: [PATCH 269/391] revert resetting isNavigate Signed-off-by: Tsaqif --- src/libs/actions/Modal.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/libs/actions/Modal.ts b/src/libs/actions/Modal.ts index 71ba850e721f..e7f0ef4df098 100644 --- a/src/libs/actions/Modal.ts +++ b/src/libs/actions/Modal.ts @@ -59,7 +59,6 @@ function onModalDidClose() { } onModalClose(); onModalClose = null; - isNavigate = undefined; } /** From a38b4da649447e0a482645c4f94f0c3894fc9215 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Mon, 22 Jan 2024 13:54:14 +0100 Subject: [PATCH 270/391] fix chat opening test --- src/libs/E2E/tests/chatOpeningTest.e2e.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/libs/E2E/tests/chatOpeningTest.e2e.ts b/src/libs/E2E/tests/chatOpeningTest.e2e.ts index 5ec1d50f7cda..abbbf6b92acf 100644 --- a/src/libs/E2E/tests/chatOpeningTest.e2e.ts +++ b/src/libs/E2E/tests/chatOpeningTest.e2e.ts @@ -10,7 +10,8 @@ const test = () => { // check for login (if already logged in the action will simply resolve) console.debug('[E2E] Logging in for chat opening'); - const reportID = ''; // report.onyxData[0].value; // TODO: get report ID! + // #announce Chat with many messages + const reportID = '5421294415618529'; E2ELogin().then((neededLogin) => { if (neededLogin) { From 571e841616bc0a7583f67dedfa8474f109dc34ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Mon, 22 Jan 2024 13:54:34 +0100 Subject: [PATCH 271/391] fix various bugs with network interceptor --- src/libs/E2E/types.ts | 17 ++++-- src/libs/E2E/utils/NetworkInterceptor.ts | 76 ++++++++++++++++-------- 2 files changed, 63 insertions(+), 30 deletions(-) diff --git a/src/libs/E2E/types.ts b/src/libs/E2E/types.ts index 8a9529bd62d6..9d769cb40ed3 100644 --- a/src/libs/E2E/types.ts +++ b/src/libs/E2E/types.ts @@ -4,13 +4,18 @@ type SigninParams = { type IsE2ETestSession = () => boolean; +type NetworkCacheEntry = { + url: string; + options: RequestInit; + status: number; + statusText: string; + headers: Record; + body: string; +}; + type NetworkCacheMap = Record< string, // hash - { - url: string; - options: RequestInit; - response: Response; - } + NetworkCacheEntry >; -export type {SigninParams, IsE2ETestSession, NetworkCacheMap}; +export type {SigninParams, IsE2ETestSession, NetworkCacheMap, NetworkCacheEntry}; diff --git a/src/libs/E2E/utils/NetworkInterceptor.ts b/src/libs/E2E/utils/NetworkInterceptor.ts index 260135319771..756bee156bc5 100644 --- a/src/libs/E2E/utils/NetworkInterceptor.ts +++ b/src/libs/E2E/utils/NetworkInterceptor.ts @@ -1,5 +1,5 @@ /* eslint-disable @lwc/lwc/no-async-await */ -import type {NetworkCacheMap} from '@libs/E2E/types'; +import type {NetworkCacheEntry, NetworkCacheMap} from '@libs/E2E/types'; const LOG_TAG = `[E2E][NetworkInterceptor]`; // Requests with these headers will be ignored: @@ -13,14 +13,6 @@ const globalIsNetworkInterceptorInstalledPromise = new Promise((resolve, r }); let networkCache: NetworkCacheMap | null = null; -/** - * This function hashes the arguments of fetch. - */ -function hashFetchArgs(args: Parameters) { - const [url, options] = args; - return JSON.stringify({url, options}); -} - /** * The headers of a fetch request can be passed as an array of tuples or as an object. * This function converts the headers to an object. @@ -68,6 +60,33 @@ function fetchArgsGetUrl(args: Parameters): string { throw new Error('Could not get url from fetch args'); } +function networkCacheEntryToResponse({headers, status, statusText, body}: NetworkCacheEntry): Response { + // Transform headers to Headers object: + const newHeaders = new Headers(); + Object.entries(headers).forEach(([key, value]) => { + newHeaders.append(key, value); + }); + + return new Response(body, { + status, + statusText, + headers: newHeaders, + }); +} + +/** + * This function hashes the arguments of fetch. + */ +function hashFetchArgs(args: Parameters) { + const url = fetchArgsGetUrl(args); + const options = fetchArgsGetRequestInit(args); + const headers = getFetchRequestHeadersAsObject(options); + // Note: earlier we were using the body value as well, however + // the body for the same request might be different due to including + // times or app versions. + return `${url}${JSON.stringify(headers)}`; +} + export default function installNetworkInterceptor( getNetworkCache: () => Promise, updateNetworkCache: (networkCache: NetworkCacheMap) => Promise, @@ -78,11 +97,13 @@ export default function installNetworkInterceptor( if (networkCache == null && shouldReturnRecordedResponse) { console.debug(LOG_TAG, 'fetching network cache …'); - getNetworkCache().then((newCache) => { - networkCache = newCache; - globalResolveIsNetworkInterceptorInstalled(); - console.debug(LOG_TAG, 'network cache fetched!'); - }, globalRejectIsNetworkInterceptorInstalled); + getNetworkCache() + .then((newCache) => { + networkCache = newCache; + globalResolveIsNetworkInterceptorInstalled(); + console.debug(LOG_TAG, 'network cache fetched!'); + }, globalRejectIsNetworkInterceptorInstalled) + .catch(globalRejectIsNetworkInterceptorInstalled); } else { networkCache = {}; globalResolveIsNetworkInterceptorInstalled(); @@ -90,7 +111,8 @@ export default function installNetworkInterceptor( // @ts-expect-error Fetch global types weirdly include URL global.fetch = async (...args: Parameters) => { - const headers = getFetchRequestHeadersAsObject(fetchArgsGetRequestInit(args)); + const options = fetchArgsGetRequestInit(args); + const headers = getFetchRequestHeadersAsObject(options); const url = fetchArgsGetUrl(args); // Check if headers contain any of the ignored headers, or if react native metro server: if (IGNORE_REQUEST_HEADERS.some((header) => headers[header] != null) || url.includes('8081')) { @@ -100,23 +122,29 @@ export default function installNetworkInterceptor( await globalIsNetworkInterceptorInstalledPromise; const hash = hashFetchArgs(args); - if (shouldReturnRecordedResponse && networkCache?.[hash] != null) { + const cachedResponse = networkCache?.[hash]; + if (shouldReturnRecordedResponse && cachedResponse != null) { + const response = networkCacheEntryToResponse(cachedResponse); console.debug(LOG_TAG, 'Returning recorded response for url:', url); - const {response} = networkCache[hash]; return Promise.resolve(response); } + if (shouldReturnRecordedResponse) { + console.debug('!!! Missed cache hit for url:', url); + } return originalFetch(...args) - .then((res) => { + .then(async (res) => { if (networkCache != null) { - console.debug(LOG_TAG, 'Updating network cache for hash:'); + const body = await res.clone().text(); networkCache[hash] = { - // @ts-expect-error TODO: The user could pass these differently, add better handling - url: args[0], - // @ts-expect-error TODO: The user could pass these differently, add better handling - options: args[1], - response: res, + url, + options, + body, + headers: getFetchRequestHeadersAsObject(options), + status: res.status, + statusText: res.statusText, }; + console.debug(LOG_TAG, 'Updating network cache for url:', url); // Send the network cache to the test server: return updateNetworkCache(networkCache).then(() => res); } From 2bf2d0386867d9da70f69df04b36c34cb9450595 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Mon, 22 Jan 2024 13:57:04 +0100 Subject: [PATCH 272/391] fix reprot typing test --- src/libs/E2E/tests/reportTypingTest.e2e.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/libs/E2E/tests/reportTypingTest.e2e.ts b/src/libs/E2E/tests/reportTypingTest.e2e.ts index d6bffa3e171a..a4bb2144086a 100644 --- a/src/libs/E2E/tests/reportTypingTest.e2e.ts +++ b/src/libs/E2E/tests/reportTypingTest.e2e.ts @@ -30,7 +30,8 @@ const test = () => { } console.debug(`[E2E] Sidebar loaded, navigating to a report…`); - Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute('98345625')); + // Crowded Policy (Do Not Delete) Report, has a input bar available: + Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute('8268282951170052')); // Wait until keyboard is visible (so we are focused on the input): waitForKeyboard().then(() => { From 74a9710b78371d91ebdfc49b1c3c9cbbde3ff8c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Mon, 22 Jan 2024 14:43:24 +0100 Subject: [PATCH 273/391] fix lint --- src/components/LHNOptionsList/LHNOptionsList.js | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/components/LHNOptionsList/LHNOptionsList.js b/src/components/LHNOptionsList/LHNOptionsList.js index 08177288c477..0819f07fd85b 100644 --- a/src/components/LHNOptionsList/LHNOptionsList.js +++ b/src/components/LHNOptionsList/LHNOptionsList.js @@ -172,17 +172,18 @@ function LHNOptionsList({ ); }, [ - currentReportID, + reports, + reportActions, + policy, + transactions, draftComments, - onSelectRow, - optionMode, personalDetails, - policy, - preferredLocale, - reportActions, - reports, + optionMode, shouldDisableFocusOptions, - transactions, + currentReportID, + onSelectRow, + preferredLocale, + onLayoutItem, transactionViolations, canUseViolations, ], From 48a38dc2307b69d0c02adca029c0a2590269d95c Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Mon, 22 Jan 2024 14:50:48 +0100 Subject: [PATCH 274/391] create getLastBusinessDayOfMonth --- src/libs/DateUtils.ts | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/libs/DateUtils.ts b/src/libs/DateUtils.ts index 1a10eb03a00e..6e56d19a89b4 100644 --- a/src/libs/DateUtils.ts +++ b/src/libs/DateUtils.ts @@ -730,6 +730,26 @@ function formatToSupportedTimezone(timezoneInput: Timezone): Timezone { }; } +/** + * Returns the latest business day of input date month + * + * param {Date} inputDate + * returns {number} + */ +function getLastBusinessDayOfMonth(inputDate: Date): number { + const currentDate = new Date(inputDate); + + // Set the date to the last day of the month + currentDate.setMonth(currentDate.getMonth() + 1, 0); + + // Loop backward to find the latest business day + while (currentDate.getDay() === 0 || currentDate.getDay() === 6) { + currentDate.setDate(currentDate.getDate() - 1); + } + + return currentDate.getDate(); +} + const DateUtils = { formatToDayOfWeek, formatToLongDateWithWeekday, @@ -774,6 +794,7 @@ const DateUtils = { getWeekEndsOn, isTimeAtLeastOneMinuteInFuture, formatToSupportedTimezone, + getLastBusinessDayOfMonth, }; export default DateUtils; From e3dfa0693cdaef4f91c701b7bde7325af68ab3af Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Mon, 22 Jan 2024 14:50:52 +0100 Subject: [PATCH 275/391] test getLastBusinessDayOfMonth --- tests/unit/DateUtilsTest.js | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/tests/unit/DateUtilsTest.js b/tests/unit/DateUtilsTest.js index 7480da456d7f..17b25f24e327 100644 --- a/tests/unit/DateUtilsTest.js +++ b/tests/unit/DateUtilsTest.js @@ -213,4 +213,30 @@ describe('DateUtils', () => { }); }); }); + + describe('getLastBusinessDayOfMonth', () => { + const scenarios = [ + { + // Last business of May in 2025 + inputDate: new Date(2025, 4), + expectedResult: 30, + }, + { + // Last business of January in 2024 + inputDate: new Date(2024, 0), + expectedResult: 31, + }, + { + // Last business of September in 2023 + inputDate: new Date(2023, 8), + expectedResult: 29, + }, + ]; + + test.each(scenarios)('returns a last business day of an input date', ({inputDate, expectedResult}) => { + const lastBusinessDay = DateUtils.getLastBusinessDayOfMonth(inputDate); + + expect(lastBusinessDay).toEqual(expectedResult); + }); + }); }); From 698dbd9095901a0f87acdfc6df86e0690a14a43f Mon Sep 17 00:00:00 2001 From: Sibtain Ali Date: Mon, 22 Jan 2024 19:07:54 +0500 Subject: [PATCH 276/391] feat: allow policy admins to edit the report fields of a non-settled report --- .../ReportActionItem/MoneyReportView.tsx | 29 ++++++++++++++----- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/src/components/ReportActionItem/MoneyReportView.tsx b/src/components/ReportActionItem/MoneyReportView.tsx index 4fcca3e518a5..7e0ff9487232 100644 --- a/src/components/ReportActionItem/MoneyReportView.tsx +++ b/src/components/ReportActionItem/MoneyReportView.tsx @@ -1,6 +1,8 @@ import React, {useMemo} from 'react'; import type {StyleProp, TextStyle} from 'react-native'; import {View} from 'react-native'; +import {withOnyx} from 'react-native-onyx'; +import type {OnyxCollection} from 'react-native-onyx'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; @@ -17,9 +19,10 @@ import * as CurrencyUtils from '@libs/CurrencyUtils'; import * as ReportUtils from '@libs/ReportUtils'; import AnimatedEmptyStateBackground from '@pages/home/report/AnimatedEmptyStateBackground'; import variables from '@styles/variables'; -import type {PolicyReportField, Report} from '@src/types/onyx'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {Policy, PolicyReportField, Report} from '@src/types/onyx'; -type MoneyReportViewProps = { +type MoneyReportViewComponentProps = { /** The report currently being looked at */ report: Report; @@ -30,7 +33,14 @@ type MoneyReportViewProps = { shouldShowHorizontalRule: boolean; }; -function MoneyReportView({report, policyReportFields, shouldShowHorizontalRule}: MoneyReportViewProps) { +type MoneyReportViewOnyxProps = { + /** Policies that the user is part of */ + policies: OnyxCollection; +}; + +type MoneyReportViewProps = MoneyReportViewComponentProps & MoneyReportViewOnyxProps; + +function MoneyReportView({report, policyReportFields, shouldShowHorizontalRule, policies}: MoneyReportViewProps) { const theme = useTheme(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); @@ -57,7 +67,7 @@ function MoneyReportView({report, policyReportFields, shouldShowHorizontalRule}: () => policyReportFields.sort(({orderWeight: firstOrderWeight}, {orderWeight: secondOrderWeight}) => firstOrderWeight - secondOrderWeight), [policyReportFields], ); - + const isAdmin = ReportUtils.isPolicyAdmin(report.policyID ?? '', policies); return ( @@ -65,6 +75,7 @@ function MoneyReportView({report, policyReportFields, shouldShowHorizontalRule}: {canUseReportFields && sortedPolicyReportFields.map((reportField) => { const title = ReportUtils.getReportFieldTitle(report, reportField); + const isDisabled = !isAdmin || isSettled; return ( {}} - shouldShowRightIcon - disabled={false} + shouldShowRightIcon={!isDisabled} + disabled={isDisabled} wrapperStyle={[styles.pv2, styles.taskDescriptionMenuItem]} shouldGreyOutWhenDisabled={false} numberOfLinesTitle={0} @@ -165,4 +176,8 @@ function MoneyReportView({report, policyReportFields, shouldShowHorizontalRule}: MoneyReportView.displayName = 'MoneyReportView'; -export default MoneyReportView; +export default withOnyx({ + policies: { + key: ONYXKEYS.COLLECTION.POLICY, + }, +})(MoneyReportView); From 11e55f61c795266788f41d8e665d06a04d3916f7 Mon Sep 17 00:00:00 2001 From: Rocio Perez-Cano Date: Mon, 22 Jan 2024 15:11:01 +0100 Subject: [PATCH 277/391] Fix some translations --- src/languages/es.ts | 64 ++++++++++++++++++++++----------------------- 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/src/languages/es.ts b/src/languages/es.ts index 2478c8ba8bd2..6730951b885a 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -441,10 +441,10 @@ export default { copyEmailToClipboard: 'Copiar email al portapapeles', markAsUnread: 'Marcar como no leído', markAsRead: 'Marcar como leído', - editAction: ({action}: EditActionParams) => `Edit ${action?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? 'pedido' : 'comentario'}`, - deleteAction: ({action}: DeleteActionParams) => `Eliminar ${action?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? 'pedido' : 'comentario'}`, + editAction: ({action}: EditActionParams) => `Edit ${action?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? 'solicitud' : 'comentario'}`, + deleteAction: ({action}: DeleteActionParams) => `Eliminar ${action?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? 'solicitud' : 'comentario'}`, deleteConfirmation: ({action}: DeleteConfirmationParams) => - `¿Estás seguro de que quieres eliminar este ${action?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? 'pedido' : 'comentario'}`, + `¿Estás seguro de que quieres eliminar esta ${action?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? 'solicitud' : 'comentario'}`, onlyVisible: 'Visible sólo para', replyInThread: 'Responder en el hilo', joinThread: 'Unirse al hilo', @@ -459,16 +459,16 @@ export default { reportActionsView: { beginningOfArchivedRoomPartOne: 'Te perdiste la fiesta en ', beginningOfArchivedRoomPartTwo: ', no hay nada que ver aquí.', - beginningOfChatHistoryDomainRoomPartOne: ({domainRoom}: BeginningOfChatHistoryDomainRoomPartOneParams) => `Colabora aquí con todos los participantes de ${domainRoom}! 🎉\nUtiliza `, + beginningOfChatHistoryDomainRoomPartOne: ({domainRoom}: BeginningOfChatHistoryDomainRoomPartOneParams) => `¡Colabora aquí con todos los participantes de ${domainRoom}! 🎉\nUtiliza `, beginningOfChatHistoryDomainRoomPartTwo: ' para chatear con compañeros, compartir consejos o hacer una pregunta.', beginningOfChatHistoryAdminRoomPartOne: ({workspaceName}: BeginningOfChatHistoryAdminRoomPartOneParams) => - `Este es el lugar para que los administradores de ${workspaceName} colaboren! 🎉\nUsa `, + `¡Este es el lugar para que los administradores de ${workspaceName} colaboren! 🎉\nUsa `, beginningOfChatHistoryAdminRoomPartTwo: ' para chatear sobre temas como la configuración del espacio de trabajo y mas.', beginningOfChatHistoryAdminOnlyPostingRoom: 'Solo los administradores pueden enviar mensajes en esta sala.', beginningOfChatHistoryAnnounceRoomPartOne: ({workspaceName}: BeginningOfChatHistoryAnnounceRoomPartOneParams) => - `Este es el lugar para que todos los miembros de ${workspaceName} colaboren! 🎉\nUsa `, + `¡Este es el lugar para que todos los miembros de ${workspaceName} colaboren! 🎉\nUsa `, beginningOfChatHistoryAnnounceRoomPartTwo: ({workspaceName}: BeginningOfChatHistoryAnnounceRoomPartTwo) => ` para chatear sobre cualquier cosa relacionada con ${workspaceName}.`, - beginningOfChatHistoryUserRoomPartOne: 'Este es el lugar para colaborar! 🎉\nUsa este espacio para chatear sobre cualquier cosa relacionada con ', + beginningOfChatHistoryUserRoomPartOne: '¡Este es el lugar para colaborar! 🎉\nUsa este espacio para chatear sobre cualquier cosa relacionada con ', beginningOfChatHistoryUserRoomPartTwo: '.', beginningOfChatHistory: 'Aquí comienzan tus conversaciones con ', beginningOfChatHistoryPolicyExpenseChatPartOne: '¡La colaboración entre ', @@ -581,8 +581,8 @@ export default { transactionPendingText: 'La transacción tarda unos días en contabilizarse desde la fecha en que se utilizó la tarjeta.', requestCount: ({count, scanningReceipts = 0}: RequestCountParams) => `${count} ${Str.pluralize('solicitude', 'solicitudes', count)}${scanningReceipts > 0 ? `, ${scanningReceipts} escaneando` : ''}`, - deleteRequest: 'Eliminar pedido', - deleteConfirmation: '¿Estás seguro de que quieres eliminar este pedido?', + deleteRequest: 'Eliminar solicitud', + deleteConfirmation: '¿Estás seguro de que quieres eliminar esta solicitud?', settledExpensify: 'Pagado', settledElsewhere: 'Pagado de otra forma', settleExpensify: ({formattedAmount}: SettleExpensifyCardParams) => (formattedAmount ? `Pagar ${formattedAmount} con Expensify` : `Pagar con Expensify`), @@ -865,7 +865,7 @@ export default { }, }, passwordConfirmationScreen: { - passwordUpdated: 'Contraseña actualizada!', + passwordUpdated: '¡Contraseña actualizada!', allSet: 'Todo está listo. Guarda tu contraseña en un lugar seguro.', }, privateNotes: { @@ -922,7 +922,7 @@ export default { enableWallet: 'Habilitar Billetera', bankAccounts: 'Cuentas bancarias', addBankAccountToSendAndReceive: 'Añade una cuenta bancaria para enviar y recibir pagos directamente en la aplicación.', - addBankAccount: 'Agregar cuenta bancaria', + addBankAccount: 'Añadir cuenta bancaria', assignedCards: 'Tarjetas asignadas', assignedCardsDescription: 'Son tarjetas asignadas por un administrador del Espacio de Trabajo para gestionar los gastos de la empresa.', expensifyCard: 'Tarjeta Expensify', @@ -1211,7 +1211,7 @@ export default { }, statusPage: { status: 'Estado', - statusExplanation: 'Agrega un emoji para que tus colegas y amigos puedan saber fácilmente qué está pasando. ¡También puedes agregar un mensaje opcionalmente!', + statusExplanation: 'Añade un emoji para que tus colegas y amigos puedan saber fácilmente qué está pasando. ¡También puedes añadir un mensaje opcionalmente!', today: 'Hoy', clearStatus: 'Borrar estado', save: 'Guardar', @@ -1813,7 +1813,7 @@ export default { resultsAreLimited: 'Los resultados de búsqueda están limitados.', }, genericErrorPage: { - title: '¡Uh-oh, algo salió mal!', + title: '¡Oh-oh, algo salió mal!', body: { helpTextMobile: 'Intenta cerrar y volver a abrir la aplicación o cambiar a la', helpTextWeb: 'web.', @@ -1896,12 +1896,12 @@ export default { }, notAvailable: { title: 'Actualización no disponible', - message: 'No existe ninguna actualización disponible! Inténtalo de nuevo más tarde.', + message: '¡No existe ninguna actualización disponible! Inténtalo de nuevo más tarde.', okay: 'Vale', }, error: { title: 'Comprobación fallida', - message: 'No hemos podido comprobar si existe una actualización. Inténtalo de nuevo más tarde!', + message: 'No hemos podido comprobar si existe una actualización. ¡Inténtalo de nuevo más tarde!', }, }, report: { @@ -2419,7 +2419,7 @@ export default { }, parentReportAction: { deletedMessage: '[Mensaje eliminado]', - deletedRequest: '[Pedido eliminado]', + deletedRequest: '[Solicitud eliminada]', reversedTransaction: '[Transacción anulada]', deletedTask: '[Tarea eliminada]', hiddenMessage: '[Mensaje oculto]', @@ -2443,13 +2443,13 @@ export default { flagDescription: 'Todos los mensajes marcados se enviarán a un moderador para su revisión.', chooseAReason: 'Elige abajo un motivo para reportarlo:', spam: 'Spam', - spamDescription: 'Promoción fuera de tema no solicitada', + spamDescription: 'Publicidad no solicitada', inconsiderate: 'Desconsiderado', inconsiderateDescription: 'Frase insultante o irrespetuosa, con intenciones cuestionables', intimidation: 'Intimidación', intimidationDescription: 'Persigue agresivamente una agenda sobre objeciones válidas', bullying: 'Bullying', - bullyingDescription: 'Apunta a un individuo para obtener obediencia', + bullyingDescription: 'Se dirige a un individuo para obtener obediencia', harassment: 'Acoso', harassmentDescription: 'Comportamiento racista, misógino u otro comportamiento discriminatorio', assault: 'Agresion', @@ -2457,8 +2457,8 @@ export default { flaggedContent: 'Este mensaje ha sido marcado por violar las reglas de nuestra comunidad y el contenido se ha ocultado.', hideMessage: 'Ocultar mensaje', revealMessage: 'Revelar mensaje', - levelOneResult: 'Envia una advertencia anónima y el mensaje es reportado para revisión.', - levelTwoResult: 'Mensaje ocultado del canal, más advertencia anónima y mensaje reportado para revisión.', + levelOneResult: 'Envía una advertencia anónima y el mensaje es reportado para revisión.', + levelTwoResult: 'Mensaje ocultado en el canal, más advertencia anónima y mensaje reportado para revisión.', levelThreeResult: 'Mensaje eliminado del canal, más advertencia anónima y mensaje reportado para revisión.', }, teachersUnitePage: { @@ -2491,7 +2491,7 @@ export default { companySpend: 'Gastos de empresa', }, distance: { - addStop: 'Agregar parada', + addStop: 'Añadir parada', deleteWaypoint: 'Eliminar punto de ruta', deleteWaypointConfirmation: '¿Estás seguro de que quieres eliminar este punto de ruta?', address: 'Dirección', @@ -2565,21 +2565,21 @@ export default { allTagLevelsRequired: 'Todas las etiquetas son obligatorias', autoReportedRejectedExpense: ({rejectedBy, rejectReason}: ViolationsAutoReportedRejectedExpenseParams) => `${rejectedBy} rechazó la solicitud y comentó "${rejectReason}"`, billableExpense: 'La opción facturable ya no es válida', - cashExpenseWithNoReceipt: ({amount}: ViolationsCashExpenseWithNoReceiptParams) => `Recibo obligatorio para montos mayores a ${amount}`, + cashExpenseWithNoReceipt: ({amount}: ViolationsCashExpenseWithNoReceiptParams) => `Recibo obligatorio para cantidades mayores de ${amount}`, categoryOutOfPolicy: 'La categoría ya no es válida', conversionSurcharge: ({surcharge}: ViolationsConversionSurchargeParams) => `${surcharge}% de recargo aplicado`, - customUnitOutOfPolicy: 'Unidad ya no es válida', - duplicatedTransaction: 'Potencial duplicado', + customUnitOutOfPolicy: 'La unidad ya no es válida', + duplicatedTransaction: 'Posible duplicado', fieldRequired: 'Los campos del informe son obligatorios', futureDate: 'Fecha futura no permitida', invoiceMarkup: ({invoiceMarkup}: ViolationsInvoiceMarkupParams) => `Incrementado un ${invoiceMarkup}%`, maxAge: ({maxAge}: ViolationsMaxAgeParams) => `Fecha de más de ${maxAge} días`, missingCategory: 'Falta categoría', - missingComment: 'Descripción obligatoria para categoría seleccionada', + missingComment: 'Descripción obligatoria para la categoría seleccionada', missingTag: ({tagName}: ViolationsMissingTagParams) => `Falta ${tagName}`, modifiedAmount: 'Importe superior al del recibo escaneado', modifiedDate: 'Fecha difiere del recibo escaneado', - nonExpensiworksExpense: 'Gasto no es de Expensiworks', + nonExpensiworksExpense: 'Gasto no proviene de Expensiworks', overAutoApprovalLimit: ({formattedLimitAmount}: ViolationsOverAutoApprovalLimitParams) => `Importe supera el límite de aprobación automática de ${formattedLimitAmount}`, overCategoryLimit: ({categoryLimit}: ViolationsOverCategoryLimitParams) => `Importe supera el límite para la categoría de ${categoryLimit}/persona`, overLimit: ({amount}: ViolationsOverLimitParams) => `Importe supera el límite de ${amount}/persona`, @@ -2590,22 +2590,22 @@ export default { rter: ({brokenBankConnection, isAdmin, email, isTransactionOlderThan7Days, member}: ViolationsRterParams) => { if (brokenBankConnection) { return isAdmin - ? `No se puede adjuntar recibo debido a una conexión con su banco que ${email} necesita arreglar` - : 'No se puede adjuntar recibo debido a una conexión con su banco que necesitas arreglar'; + ? `No se puede adjuntar recibo debido a un problema con la conexión a su banco que ${email} necesita arreglar` + : 'No se puede adjuntar recibo debido a un problema con la conexión a su banco que necesitas arreglar'; } if (!isTransactionOlderThan7Days) { return isAdmin - ? `Pídele a ${member} que marque la transacción como efectivo o espera 7 días e intenta de nuevo` - : 'Esperando adjuntar automáticamente a transacción de tarjeta de crédito'; + ? `Píde a ${member} que marque la transacción como efectivo o espera 7 días e inténtalo de nuevo` + : 'Esperando a adjuntar automáticamente la transacción de tarjeta de crédito'; } return ''; }, smartscanFailed: 'No se pudo escanear el recibo. Introduce los datos manualmente', someTagLevelsRequired: 'Falta etiqueta', - tagOutOfPolicy: ({tagName}: ViolationsTagOutOfPolicyParams) => `Le etiqueta ${tagName} ya no es válida`, + tagOutOfPolicy: ({tagName}: ViolationsTagOutOfPolicyParams) => `La etiqueta ${tagName} ya no es válida`, taxAmountChanged: 'El importe del impuesto fue modificado', taxOutOfPolicy: ({taxName}: ViolationsTaxOutOfPolicyParams) => `${taxName} ya no es válido`, taxRateChanged: 'La tasa de impuesto fue modificada', - taxRequired: 'Falta tasa de impuesto', + taxRequired: 'Falta la tasa de impuesto', }, } satisfies EnglishTranslation; From 84f6980b96dc7d1d3e26984bae5070a304e9d509 Mon Sep 17 00:00:00 2001 From: Rocio Perez-Cano Date: Mon, 22 Jan 2024 16:05:12 +0100 Subject: [PATCH 278/391] =?UTF-8?q?=C3=8Fmportes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/languages/es.ts | 72 ++++++++++++++++++++++----------------------- 1 file changed, 36 insertions(+), 36 deletions(-) diff --git a/src/languages/es.ts b/src/languages/es.ts index 6730951b885a..2c4070ad30ad 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -441,7 +441,7 @@ export default { copyEmailToClipboard: 'Copiar email al portapapeles', markAsUnread: 'Marcar como no leído', markAsRead: 'Marcar como leído', - editAction: ({action}: EditActionParams) => `Edit ${action?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? 'solicitud' : 'comentario'}`, + editAction: ({action}: EditActionParams) => `Editar ${action?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? 'solicitud' : 'comentario'}`, deleteAction: ({action}: DeleteActionParams) => `Eliminar ${action?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? 'solicitud' : 'comentario'}`, deleteConfirmation: ({action}: DeleteConfirmationParams) => `¿Estás seguro de que quieres eliminar esta ${action?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? 'solicitud' : 'comentario'}`, @@ -627,22 +627,22 @@ export default { tagSelection: ({tagName}: TagSelectionParams) => `Seleccione una ${tagName} para organizar mejor tu dinero.`, categorySelection: 'Seleccione una categoría para organizar mejor tu dinero.', error: { - invalidCategoryLength: 'El largo de la categoría escogida excede el máximo permitido (255). Por favor escoge otra categoría o acorta la categoría primero.', - invalidAmount: 'Por favor ingresa un monto válido antes de continuar.', + invalidCategoryLength: 'El largo de la categoría escogida excede el máximo permitido (255). Por favor, escoge otra categoría o acorta la categoría primero.', + invalidAmount: 'Por favor, ingresa un importe válido antes de continuar.', invalidTaxAmount: ({amount}: RequestAmountParams) => `El importe máximo del impuesto es ${amount}`, - invalidSplit: 'La suma de las partes no equivale al monto total', + invalidSplit: 'La suma de las partes no equivale al importe total', other: 'Error inesperado, por favor inténtalo más tarde', - genericCreateFailureMessage: 'Error inesperado solicitando dinero, Por favor, inténtalo más tarde', + genericCreateFailureMessage: 'Error inesperado solicitando dinero. Por favor, inténtalo más tarde', receiptFailureMessage: 'El recibo no se subió. ', saveFileMessage: 'Guarda el archivo ', loseFileMessage: 'o descarta este error y piérdelo', genericDeleteFailureMessage: 'Error inesperado eliminando la solicitud de dinero. Por favor, inténtalo más tarde', genericEditFailureMessage: 'Error inesperado al guardar la solicitud de dinero. Por favor, inténtalo más tarde', genericSmartscanFailureMessage: 'La transacción tiene campos vacíos', - duplicateWaypointsErrorMessage: 'Por favor elimina los puntos de ruta duplicados', - atLeastTwoDifferentWaypoints: 'Por favor introduce al menos dos direcciones diferentes', - splitBillMultipleParticipantsErrorMessage: 'Solo puedes dividir una cuenta entre un único espacio de trabajo o con usuarios individuales. Por favor actualiza tu selección.', - invalidMerchant: 'Por favor ingrese un comerciante correcto.', + duplicateWaypointsErrorMessage: 'Por favor, elimina los puntos de ruta duplicados', + atLeastTwoDifferentWaypoints: 'Por favor, introduce al menos dos direcciones diferentes', + splitBillMultipleParticipantsErrorMessage: 'Solo puedes dividir una cuenta entre un único espacio de trabajo o con usuarios individuales. Por favor, actualiza tu selección.', + invalidMerchant: 'Por favor, introduce un comerciante correcto.', }, waitingOnEnabledWallet: ({submitterDisplayName}: WaitingOnBankAccountParams) => `Inició el pago, pero no se procesará hasta que ${submitterDisplayName} active su Billetera`, enableWallet: 'Habilitar Billetera', @@ -1375,35 +1375,35 @@ export default { headerTitle: 'Condiciones y tarifas', haveReadAndAgree: 'He leído y acepto recibir ', electronicDisclosures: 'divulgaciones electrónicas', - agreeToThe: 'Estoy de acuerdo con la ', - walletAgreement: 'Acuerdo de billetera', + agreeToThe: 'Estoy de acuerdo con el ', + walletAgreement: 'Acuerdo de la billetera', enablePayments: 'Habilitar pagos', - feeAmountZero: '$0', + feeAmountZero: 'US$0', monthlyFee: 'Cuota mensual', inactivity: 'Inactividad', - electronicFundsInstantFee: '1.5%', - noOverdraftOrCredit: 'Sin función de sobregiro / crédito', + electronicFundsInstantFee: '1,5%', + noOverdraftOrCredit: 'Sin función de sobregiro/crédito', electronicFundsWithdrawal: 'Retiro electrónico de fondos', standard: 'Estándar', shortTermsForm: { expensifyPaymentsAccount: ({walletProgram}: WalletProgramParams) => `La billetera Expensify es emitida por ${walletProgram}.`, perPurchase: 'Por compra', - atmWithdrawal: 'Retiro de cajero automático', + atmWithdrawal: 'Retiro en cajeros automáticos', cashReload: 'Recarga de efectivo', inNetwork: 'en la red', outOfNetwork: 'fuera de la red', - atmBalanceInquiry: 'Consulta de saldo de cajero automático', + atmBalanceInquiry: 'Consulta de saldo en cajeros automáticos', inOrOutOfNetwork: '(dentro o fuera de la red)', customerService: 'Servicio al cliente', automatedOrLive: '(agente automatizado o en vivo)', afterTwelveMonths: '(después de 12 meses sin transacciones)', weChargeOneFee: 'Cobramos un tipo de tarifa.', - fdicInsurance: 'Sus fondos son elegibles para el seguro de la FDIC.', - generalInfo: 'Para obtener información general sobre cuentas prepagas, visite', + fdicInsurance: 'Tus fondos pueden acogerse al seguro de la FDIC.', + generalInfo: 'Para obtener información general sobre cuentas de prepago, visite', conditionsDetails: 'Encuentra detalles y condiciones para todas las tarifas y servicios visitando', conditionsPhone: 'o llamando al +1 833-400-0904.', instant: '(instantáneo)', - electronicFundsInstantFeeMin: '(mínimo $0.25)', + electronicFundsInstantFeeMin: '(mínimo US$0,25)', }, longTermsForm: { listOfAllFees: 'Una lista de todas las tarifas de la billetera Expensify', @@ -1417,30 +1417,30 @@ export default { customerServiceDetails: 'No hay tarifas de servicio al cliente.', inactivityDetails: 'No hay tarifa de inactividad.', sendingFundsTitle: 'Enviar fondos a otro titular de cuenta', - sendingFundsDetails: 'No se aplica ningún cargo por enviar fondos a otro titular de cuenta utilizando su saldo cuenta bancaria o tarjeta de débito', + sendingFundsDetails: 'No se aplica ningún cargo por enviar fondos a otro titular de cuenta utilizando tu saldo cuenta bancaria o tarjeta de débito', electronicFundsStandardDetails: - 'No hay cargo por transferir fondos desde su billetera Expensify ' + - 'a su cuenta bancaria utilizando la opción estándar. Esta transferencia generalmente se completa en' + - '1-3 negocios días.', + 'No hay cargo por transferir fondos desde tu billetera Expensify ' + + 'a tu cuenta bancaria utilizando la opción estándar. Esta transferencia generalmente se completa en' + + '1-3 días laborables.', electronicFundsInstantDetails: - 'Hay una tarifa para transferir fondos desde su billetera Expensify a ' + - 'su tarjeta de débito vinculada utilizando la opción de transferencia instantánea. Esta transferencia ' + - 'generalmente se completa dentro de varios minutos. La tarifa es el 1.5% del monto de la ' + - 'transferencia (con una tarifa mínima de $ 0.25). ', + 'Hay una tarifa para transferir fondos desde tu billetera Expensify a ' + + 'la tarjeta de débito vinculada utilizando la opción de transferencia instantánea. Esta transferencia ' + + 'generalmente se completa dentro de varios minutos. La tarifa es el 1,5% del importe de la ' + + 'transferencia (con una tarifa mínima de US$0,25). ', fdicInsuranceBancorp: - 'Sus fondos son elegibles para el seguro de la FDIC. Sus fondos se mantendrán en o ' + - `transferido a ${CONST.WALLET.PROGRAM_ISSUERS.BANCORP_BANK}, una institución asegurada por la FDIC. Una vez allí, sus fondos ` + - `están asegurados a $ 250,000 por la FDIC en caso de que ${CONST.WALLET.PROGRAM_ISSUERS.BANCORP_BANK} quiebre. Ver`, - fdicInsuranceBancorp2: 'para detalles.', - contactExpensifyPayments: `Comuníquese con ${CONST.WALLET.PROGRAM_ISSUERS.EXPENSIFY_PAYMENTS} llamando al + 1833-400-0904, por correoelectrónico a`, + 'Tus fondos pueden acogerse al seguro de la FDIC. Tus fondos se mantendrán o serán ' + + `transferidos a ${CONST.WALLET.PROGRAM_ISSUERS.BANCORP_BANK}, una institución asegurada por la FDIC. Una vez allí, tus fondos ` + + `están asegurados hasta US$250.000 por la FDIC en caso de que ${CONST.WALLET.PROGRAM_ISSUERS.BANCORP_BANK} quiebre. Ver`, + fdicInsuranceBancorp2: 'para más detalles.', + contactExpensifyPayments: `Comunícate con ${CONST.WALLET.PROGRAM_ISSUERS.EXPENSIFY_PAYMENTS} llamando al + 1833-400-0904, o por correo electrónico a`, contactExpensifyPayments2: 'o inicie sesión en', - generalInformation: 'Para obtener información general sobre cuentas prepagas, visite', - generalInformation2: 'Si tiene una queja sobre una cuenta prepaga, llame al Consumer Financial Oficina de Protección al 1-855-411-2372 o visite', + generalInformation: 'Para obtener información general sobre cuentas de prepago, visite', + generalInformation2: 'Si tienes alguna queja sobre una cuenta de prepago, llama al Consumer Financial Oficina de Protección al 1-855-411-2372 o visita', printerFriendlyView: 'Ver versión para imprimir', automated: 'Automatizado', liveAgent: 'Agente en vivo', instant: 'Instantáneo', - electronicFundsInstantFeeMin: 'Mínimo $0.25', + electronicFundsInstantFeeMin: 'Mínimo US$0,25', }, }, activateStep: { @@ -1448,7 +1448,7 @@ export default { activatedTitle: '¡Billetera activada!', activatedMessage: 'Felicidades, tu Billetera está configurada y lista para hacer pagos.', checkBackLaterTitle: 'Un momento...', - checkBackLaterMessage: 'Todavía estamos revisando tu información. Por favor, vuelva más tarde.', + checkBackLaterMessage: 'Todavía estamos revisando tu información. Por favor, vuelve más tarde.', continueToPayment: 'Continuar al pago', continueToTransfer: 'Continuar a la transferencia', }, From fa47c0da6c59f2b86adadc1b819c9f99d9a0ae7b Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Mon, 22 Jan 2024 16:18:07 +0100 Subject: [PATCH 279/391] re-test From 453de73dbdbad0dd979e6c1741225dbbea40e18a Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Mon, 22 Jan 2024 16:56:16 +0100 Subject: [PATCH 280/391] use date-fns --- src/libs/DateUtils.ts | 18 ++++++++++-------- tests/unit/DateUtilsTest.js | 2 +- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/libs/DateUtils.ts b/src/libs/DateUtils.ts index 6e56d19a89b4..188e0e54afe8 100644 --- a/src/libs/DateUtils.ts +++ b/src/libs/DateUtils.ts @@ -5,9 +5,12 @@ import { eachDayOfInterval, eachMonthOfInterval, endOfDay, + endOfMonth, endOfWeek, format, formatDistanceToNow, + getDate, + getDay, getDayOfYear, isAfter, isBefore, @@ -737,17 +740,16 @@ function formatToSupportedTimezone(timezoneInput: Timezone): Timezone { * returns {number} */ function getLastBusinessDayOfMonth(inputDate: Date): number { - const currentDate = new Date(inputDate); + let currentDate = endOfMonth(inputDate); + const dayOfWeek = getDay(currentDate); - // Set the date to the last day of the month - currentDate.setMonth(currentDate.getMonth() + 1, 0); - - // Loop backward to find the latest business day - while (currentDate.getDay() === 0 || currentDate.getDay() === 6) { - currentDate.setDate(currentDate.getDate() - 1); + if (dayOfWeek === 0) { + currentDate = subDays(currentDate, 2); + } else if (dayOfWeek === 6) { + currentDate = subDays(currentDate, 1); } - return currentDate.getDate(); + return getDate(currentDate); } const DateUtils = { diff --git a/tests/unit/DateUtilsTest.js b/tests/unit/DateUtilsTest.js index 17b25f24e327..a8bdc6c6b7bc 100644 --- a/tests/unit/DateUtilsTest.js +++ b/tests/unit/DateUtilsTest.js @@ -214,7 +214,7 @@ describe('DateUtils', () => { }); }); - describe('getLastBusinessDayOfMonth', () => { + describe.only('getLastBusinessDayOfMonth', () => { const scenarios = [ { // Last business of May in 2025 From 2aadb0a4fa973b1f64774c5ff70d98ad13283914 Mon Sep 17 00:00:00 2001 From: Pujan Date: Mon, 22 Jan 2024 23:59:32 +0530 Subject: [PATCH 281/391] used StackScreenProps --- src/libs/Navigation/types.ts | 5 +---- src/pages/PrivateNotes/PrivateNotesEditPage.tsx | 15 ++++++++------- src/pages/PrivateNotes/PrivateNotesListPage.tsx | 17 +---------------- 3 files changed, 10 insertions(+), 27 deletions(-) diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index fcd01aabda9e..2f1469e40a19 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -331,10 +331,7 @@ type ProcessMoneyRequestHoldNavigatorParamList = { }; type PrivateNotesNavigatorParamList = { - [SCREENS.PRIVATE_NOTES.LIST]: { - reportID: string; - accountID: string; - }; + [SCREENS.PRIVATE_NOTES.LIST]: undefined; [SCREENS.PRIVATE_NOTES.EDIT]: { reportID: string; accountID: string; diff --git a/src/pages/PrivateNotes/PrivateNotesEditPage.tsx b/src/pages/PrivateNotes/PrivateNotesEditPage.tsx index c6095a318029..6292a2e3c412 100644 --- a/src/pages/PrivateNotes/PrivateNotesEditPage.tsx +++ b/src/pages/PrivateNotes/PrivateNotesEditPage.tsx @@ -1,5 +1,5 @@ import {useFocusEffect} from '@react-navigation/native'; -import type {RouteProp} from '@react-navigation/native'; +import type {StackScreenProps} from '@react-navigation/stack'; import ExpensiMark from 'expensify-common/lib/ExpensiMark'; import Str from 'expensify-common/lib/str'; import lodashDebounce from 'lodash/debounce'; @@ -18,6 +18,7 @@ import TextInput from '@components/TextInput'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@libs/Navigation/Navigation'; +import type {PrivateNotesNavigatorParamList} from '@libs/Navigation/types'; import * as ReportUtils from '@libs/ReportUtils'; import updateMultilineInputRange from '@libs/updateMultilineInputRange'; import withReportAndPrivateNotesOrNotFound from '@pages/home/report/withReportAndPrivateNotesOrNotFound'; @@ -25,6 +26,7 @@ import * as ReportActions from '@userActions/Report'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import type SCREENS from '@src/SCREENS'; import type {PersonalDetails, Report} from '@src/types/onyx'; import type {Note} from '@src/types/onyx/Report'; @@ -35,12 +37,11 @@ type PrivateNotesEditPageOnyxProps = { personalDetailsList: OnyxCollection; }; -type PrivateNotesEditPageProps = PrivateNotesEditPageOnyxProps & { - /** The report currently being looked at */ - report: Report; - - route: RouteProp<{params: {reportID: string; accountID: string}}>; -}; +type PrivateNotesEditPageProps = PrivateNotesEditPageOnyxProps & + StackScreenProps & { + /** The report currently being looked at */ + report: Report; + }; function PrivateNotesEditPage({route, personalDetailsList, report}: PrivateNotesEditPageProps) { const styles = useThemeStyles(); diff --git a/src/pages/PrivateNotes/PrivateNotesListPage.tsx b/src/pages/PrivateNotes/PrivateNotesListPage.tsx index 550234a0707e..30bd90bed5b6 100644 --- a/src/pages/PrivateNotes/PrivateNotesListPage.tsx +++ b/src/pages/PrivateNotes/PrivateNotesListPage.tsx @@ -1,5 +1,4 @@ -import {useIsFocused} from '@react-navigation/native'; -import React, {useEffect, useMemo} from 'react'; +import React, {useMemo} from 'react'; import {ScrollView} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; @@ -43,20 +42,6 @@ type NoteListItem = { function PrivateNotesListPage({report, personalDetailsList, session}: PrivateNotesListPageProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); - const isFocused = useIsFocused(); - - useEffect(() => { - const navigateToEditPageTimeout = setTimeout(() => { - if (Object.values(report.privateNotes ?? {}).some((item) => item.note) || !isFocused) { - return; - } - Navigation.navigate(ROUTES.PRIVATE_NOTES_EDIT.getRoute(report.reportID, session?.accountID ?? '')); - }, CONST.ANIMATED_TRANSITION); - - return () => { - clearTimeout(navigateToEditPageTimeout); - }; - }, [report.privateNotes, report.reportID, session?.accountID, isFocused]); /** * Gets the menu item for each workspace From 44d82985dedb00395a913b1ee59278513b1de9a7 Mon Sep 17 00:00:00 2001 From: Fitsum Abebe Date: Mon, 22 Jan 2024 22:59:49 +0300 Subject: [PATCH 282/391] fixed unread marker inconsistency --- src/pages/home/report/ReportActionsList.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pages/home/report/ReportActionsList.js b/src/pages/home/report/ReportActionsList.js index 66b0decedb42..5803e97aaf63 100644 --- a/src/pages/home/report/ReportActionsList.js +++ b/src/pages/home/report/ReportActionsList.js @@ -435,6 +435,7 @@ function ReportActionsList({ Report.readNewestAction(report.reportID, false); userActiveSince.current = DateUtils.getDBTime(); + lastReadTimeRef.current = userInactiveSince.current; setCurrentUnreadMarker(null); cacheUnreadMarkers.delete(report.reportID); calculateUnreadMarker(); From 2e240a32c7ef5804f153594a964abb887478822b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Mon, 22 Jan 2024 21:13:56 +0100 Subject: [PATCH 283/391] revert wip changes --- appIndex.js | 12 ------------ index.js | 13 ++++++++++++- src/libs/E2E/reactNativeLaunchingTest.ts | 2 +- 3 files changed, 13 insertions(+), 14 deletions(-) delete mode 100644 appIndex.js diff --git a/appIndex.js b/appIndex.js deleted file mode 100644 index 2a3de088f934..000000000000 --- a/appIndex.js +++ /dev/null @@ -1,12 +0,0 @@ -/** - * @format - */ -import {AppRegistry} from 'react-native'; -import {enableLegacyWebImplementation} from 'react-native-gesture-handler'; -import App from './src/App'; -import Config from './src/CONFIG'; -import additionalAppSetup from './src/setup'; - -enableLegacyWebImplementation(true); -AppRegistry.registerComponent(Config.APP_NAME, () => App); -additionalAppSetup(); diff --git a/index.js b/index.js index f7d262e1271b..2a3de088f934 100644 --- a/index.js +++ b/index.js @@ -1 +1,12 @@ -require('./src/libs/E2E/reactNativeLaunchingTest'); +/** + * @format + */ +import {AppRegistry} from 'react-native'; +import {enableLegacyWebImplementation} from 'react-native-gesture-handler'; +import App from './src/App'; +import Config from './src/CONFIG'; +import additionalAppSetup from './src/setup'; + +enableLegacyWebImplementation(true); +AppRegistry.registerComponent(Config.APP_NAME, () => App); +additionalAppSetup(); diff --git a/src/libs/E2E/reactNativeLaunchingTest.ts b/src/libs/E2E/reactNativeLaunchingTest.ts index cd17cf4ce9e9..c86df7afee95 100644 --- a/src/libs/E2E/reactNativeLaunchingTest.ts +++ b/src/libs/E2E/reactNativeLaunchingTest.ts @@ -86,5 +86,5 @@ E2EClient.getTestConfig() // start the usual app Performance.markStart('regularAppStart'); -import '../../../appIndex'; +import '../../../index'; Performance.markEnd('regularAppStart'); From d4c670b766659c2975acd92bea1652f7a3beab27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Mon, 22 Jan 2024 21:15:07 +0100 Subject: [PATCH 284/391] remove wip changes --- index.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/index.js b/index.js index 2a3de088f934..283120f50395 100644 --- a/index.js +++ b/index.js @@ -2,11 +2,9 @@ * @format */ import {AppRegistry} from 'react-native'; -import {enableLegacyWebImplementation} from 'react-native-gesture-handler'; import App from './src/App'; import Config from './src/CONFIG'; import additionalAppSetup from './src/setup'; -enableLegacyWebImplementation(true); AppRegistry.registerComponent(Config.APP_NAME, () => App); additionalAppSetup(); From 95f6022c3e0540edfe2dcdc85bd32cd2032954f7 Mon Sep 17 00:00:00 2001 From: Andrew Rosiclair Date: Mon, 22 Jan 2024 17:06:35 -0500 Subject: [PATCH 285/391] add logging --- src/pages/LogInWithShortLivedAuthTokenPage.tsx | 2 ++ src/pages/LogOutPreviousUserPage.js | 2 ++ src/pages/signin/SAMLSignInPage/index.native.js | 3 +++ 3 files changed, 7 insertions(+) diff --git a/src/pages/LogInWithShortLivedAuthTokenPage.tsx b/src/pages/LogInWithShortLivedAuthTokenPage.tsx index c5f8a9c20d5b..16e990392f14 100644 --- a/src/pages/LogInWithShortLivedAuthTokenPage.tsx +++ b/src/pages/LogInWithShortLivedAuthTokenPage.tsx @@ -18,6 +18,7 @@ import * as Session from '@userActions/Session'; import ONYXKEYS from '@src/ONYXKEYS'; import type SCREENS from '@src/SCREENS'; import type {Account} from '@src/types/onyx'; +import Log from '@libs/Log'; type LogInWithShortLivedAuthTokenPageOnyxProps = { /** The details about the account that the user is signing in with */ @@ -38,6 +39,7 @@ function LogInWithShortLivedAuthTokenPage({route, account}: LogInWithShortLivedA // Try to authenticate using the shortLivedToken if we're not already trying to load the accounts if (token && !account?.isLoading) { + Log.info('LogInWithShortLivedAuthTokenPage - Successfully received shortLivedAuthToken. Signing in...'); Session.signInWithShortLivedAuthToken(email, token); return; } diff --git a/src/pages/LogOutPreviousUserPage.js b/src/pages/LogOutPreviousUserPage.js index 5c8a39204467..974823f489ed 100644 --- a/src/pages/LogOutPreviousUserPage.js +++ b/src/pages/LogOutPreviousUserPage.js @@ -4,6 +4,7 @@ import React, {useEffect} from 'react'; import {Linking} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; +import Log from '@libs/Log'; import * as SessionUtils from '@libs/SessionUtils'; import * as Session from '@userActions/Session'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -51,6 +52,7 @@ function LogOutPreviousUserPage(props) { // On Enabling 2FA, authToken stored in Onyx becomes expired and hence we need to fetch new authToken const shouldForceLogin = lodashGet(props, 'route.params.shouldForceLogin', '') === 'true'; if (shouldForceLogin) { + Log.info('LogOutPreviousUserPage - forcing login with shortLivedAuthToken'); const email = lodashGet(props, 'route.params.email', ''); const shortLivedAuthToken = lodashGet(props, 'route.params.shortLivedAuthToken', ''); Session.signInWithShortLivedAuthToken(email, shortLivedAuthToken); diff --git a/src/pages/signin/SAMLSignInPage/index.native.js b/src/pages/signin/SAMLSignInPage/index.native.js index 502e26e337b9..7211122b5d24 100644 --- a/src/pages/signin/SAMLSignInPage/index.native.js +++ b/src/pages/signin/SAMLSignInPage/index.native.js @@ -7,6 +7,7 @@ import HeaderWithBackButton from '@components/HeaderWithBackButton'; import SAMLLoadingIndicator from '@components/SAMLLoadingIndicator'; import ScreenWrapper from '@components/ScreenWrapper'; import getPlatform from '@libs/getPlatform'; +import Log from '@libs/Log'; import Navigation from '@libs/Navigation/Navigation'; import * as Session from '@userActions/Session'; import CONFIG from '@src/CONFIG'; @@ -36,6 +37,7 @@ function SAMLSignInPage({credentials}) { */ const handleNavigationStateChange = useCallback( ({url}) => { + Log.info('SAMLSignInPage - Handling SAML navigation change'); // If we've gotten a callback then remove the option to navigate back to the sign in page if (url.includes('loginCallback')) { shouldShowNavigation(false); @@ -43,6 +45,7 @@ function SAMLSignInPage({credentials}) { const searchParams = new URLSearchParams(new URL(url).search); if (searchParams.has('shortLivedAuthToken')) { + Log.info('SAMLSignInPage - Successfully received shortLivedAuthToken. Signing in...'); const shortLivedAuthToken = searchParams.get('shortLivedAuthToken'); Session.signInWithShortLivedAuthToken(credentials.login, shortLivedAuthToken); } From 454c2b1ff8d466be6e8c88617299047e1798a634 Mon Sep 17 00:00:00 2001 From: Andrew Rosiclair Date: Mon, 22 Jan 2024 17:29:46 -0500 Subject: [PATCH 286/391] check for account.isLoading --- src/pages/signin/SAMLSignInPage/index.native.js | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/pages/signin/SAMLSignInPage/index.native.js b/src/pages/signin/SAMLSignInPage/index.native.js index 7211122b5d24..9fe60e56353e 100644 --- a/src/pages/signin/SAMLSignInPage/index.native.js +++ b/src/pages/signin/SAMLSignInPage/index.native.js @@ -20,13 +20,20 @@ const propTypes = { /** The email/phone the user logged in with */ login: PropTypes.string, }), + + /** State of the logging in user's account */ + account: PropTypes.shape({ + /** Whether the account is loading */ + isLoading: PropTypes.bool, + }), }; const defaultProps = { credentials: {}, + account: {}, }; -function SAMLSignInPage({credentials}) { +function SAMLSignInPage({credentials, account}) { const samlLoginURL = `${CONFIG.EXPENSIFY.SAML_URL}?email=${credentials.login}&referer=${CONFIG.EXPENSIFY.EXPENSIFY_CASH_REFERER}&platform=${getPlatform()}`; const [showNavigation, shouldShowNavigation] = useState(true); @@ -44,7 +51,7 @@ function SAMLSignInPage({credentials}) { } const searchParams = new URLSearchParams(new URL(url).search); - if (searchParams.has('shortLivedAuthToken')) { + if (searchParams.has('shortLivedAuthToken') && !account.isLoading) { Log.info('SAMLSignInPage - Successfully received shortLivedAuthToken. Signing in...'); const shortLivedAuthToken = searchParams.get('shortLivedAuthToken'); Session.signInWithShortLivedAuthToken(credentials.login, shortLivedAuthToken); @@ -57,7 +64,7 @@ function SAMLSignInPage({credentials}) { Navigation.navigate(ROUTES.HOME); } }, - [credentials.login, shouldShowNavigation], + [credentials.login, shouldShowNavigation, account.isLoading], ); return ( @@ -95,4 +102,5 @@ SAMLSignInPage.displayName = 'SAMLSignInPage'; export default withOnyx({ credentials: {key: ONYXKEYS.CREDENTIALS}, + account: {key: ONYXKEYS.ACCOUNT}, })(SAMLSignInPage); From 420878d882ae2bf1474658a941ffdb7ea32a8cf4 Mon Sep 17 00:00:00 2001 From: Andrew Rosiclair Date: Mon, 22 Jan 2024 17:49:32 -0500 Subject: [PATCH 287/391] prettier --- src/pages/LogInWithShortLivedAuthTokenPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/LogInWithShortLivedAuthTokenPage.tsx b/src/pages/LogInWithShortLivedAuthTokenPage.tsx index 16e990392f14..811c35fff34e 100644 --- a/src/pages/LogInWithShortLivedAuthTokenPage.tsx +++ b/src/pages/LogInWithShortLivedAuthTokenPage.tsx @@ -12,13 +12,13 @@ import TextLink from '@components/TextLink'; import useLocalize from '@hooks/useLocalize'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; +import Log from '@libs/Log'; import Navigation from '@libs/Navigation/Navigation'; import type {PublicScreensParamList} from '@libs/Navigation/types'; import * as Session from '@userActions/Session'; import ONYXKEYS from '@src/ONYXKEYS'; import type SCREENS from '@src/SCREENS'; import type {Account} from '@src/types/onyx'; -import Log from '@libs/Log'; type LogInWithShortLivedAuthTokenPageOnyxProps = { /** The details about the account that the user is signing in with */ From d0f1991ca041cc44c9e221fd4f9583165a46c22d Mon Sep 17 00:00:00 2001 From: Tsaqif Date: Tue, 23 Jan 2024 07:23:27 +0700 Subject: [PATCH 288/391] add reset for isNavigate Signed-off-by: Tsaqif --- src/libs/actions/Modal.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libs/actions/Modal.ts b/src/libs/actions/Modal.ts index e7f0ef4df098..71ba850e721f 100644 --- a/src/libs/actions/Modal.ts +++ b/src/libs/actions/Modal.ts @@ -59,6 +59,7 @@ function onModalDidClose() { } onModalClose(); onModalClose = null; + isNavigate = undefined; } /** From e16d28286b4f3681c0638e1a6396d61b2d6c903b Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Tue, 23 Jan 2024 09:37:09 +0700 Subject: [PATCH 289/391] edit comment --- src/pages/home/report/ReportActionItem.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.js index 60503424f663..80fb341a2cf8 100644 --- a/src/pages/home/report/ReportActionItem.js +++ b/src/pages/home/report/ReportActionItem.js @@ -346,8 +346,7 @@ function ReportActionItem(props) { const iouReportID = originalMessage.IOUReportID ? originalMessage.IOUReportID.toString() : '0'; children = ( Date: Tue, 23 Jan 2024 10:03:00 +0700 Subject: [PATCH 290/391] fix: amount text input is hard to paste --- .../BaseTextInputWithCurrencySymbol.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/components/TextInputWithCurrencySymbol/BaseTextInputWithCurrencySymbol.js b/src/components/TextInputWithCurrencySymbol/BaseTextInputWithCurrencySymbol.js index ee7abde8c554..bb1bb9c9bfa1 100644 --- a/src/components/TextInputWithCurrencySymbol/BaseTextInputWithCurrencySymbol.js +++ b/src/components/TextInputWithCurrencySymbol/BaseTextInputWithCurrencySymbol.js @@ -2,6 +2,7 @@ import React from 'react'; import AmountTextInput from '@components/AmountTextInput'; import CurrencySymbolButton from '@components/CurrencySymbolButton'; import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; import * as CurrencyUtils from '@libs/CurrencyUtils'; import * as MoneyRequestUtils from '@libs/MoneyRequestUtils'; import * as textInputWithCurrencySymbolPropTypes from './textInputWithCurrencySymbolPropTypes'; @@ -10,6 +11,7 @@ function BaseTextInputWithCurrencySymbol(props) { const {fromLocaleDigit} = useLocalize(); const currencySymbol = CurrencyUtils.getLocalizedCurrencySymbol(props.selectedCurrencyCode); const isCurrencySymbolLTR = CurrencyUtils.isCurrencySymbolLTR(props.selectedCurrencyCode); + const styles = useThemeStyles(); const currencySymbolButton = ( ); From 0273d3c1bf9a7a477c12580f5a196cc0db2b6cc9 Mon Sep 17 00:00:00 2001 From: tienifr Date: Tue, 23 Jan 2024 10:48:09 +0700 Subject: [PATCH 291/391] clean code --- .../HTMLRenderers/MentionUserRenderer.js | 27 +++++++++++-------- src/libs/LoginUtils.ts | 4 +-- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.js b/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.js index ca316c5aa9eb..203c728fe2c6 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.js +++ b/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.js @@ -33,34 +33,39 @@ function MentionUserRenderer(props) { const StyleUtils = useStyleUtils(); const {translate} = useLocalize(); const defaultRendererProps = _.omit(props, ['TDefaultRenderer', 'style']); - const htmlAttribAccountID = lodashGet(props.tnode.attributes, 'accountid'); + const htmlAttributeAccountID = lodashGet(props.tnode.attributes, 'accountid'); const personalDetails = usePersonalDetails() || CONST.EMPTY_OBJECT; let accountID; let displayNameOrLogin; let navigationRoute; const tnode = cloneDeep(props.tnode); - const getMentionDisplayText = (displayText, accountId, userLogin = '') => { - if (accountId && userLogin !== displayText) { + + const getMentionDisplayText = (displayText, userAccountID, userLogin = '') => { + // if the userAccountID does not exist, this is email-based mention so the displayName must be an email. + // If the userAccountID exists but userLogin is different from displayName, this means the displayName is either user display name, Hidden, or phone number, in which case we should return it as is. + if (userAccountID && userLogin !== displayText) { return displayText; } + // If the emails are not in the same private domain, we also return the displayText if (!LoginUtils.areEmailsFromSamePrivateDomain(displayText, props.currentUserPersonalDetails.login)) { return displayText; } - + // Otherwise, the emails must be of the same private domain, so we should remove the domain part return displayText.split('@')[0]; }; - if (!_.isEmpty(htmlAttribAccountID)) { - const user = lodashGet(personalDetails, htmlAttribAccountID); - accountID = parseInt(htmlAttribAccountID, 10); + if (!_.isEmpty(htmlAttributeAccountID)) { + const user = lodashGet(personalDetails, htmlAttributeAccountID); + accountID = parseInt(htmlAttributeAccountID, 10); displayNameOrLogin = LocalePhoneNumber.formatPhoneNumber(lodashGet(user, 'login', '')) || lodashGet(user, 'displayName', '') || translate('common.hidden'); - displayNameOrLogin = getMentionDisplayText(displayNameOrLogin, htmlAttribAccountID, lodashGet(user, 'login', '')); - navigationRoute = ROUTES.PROFILE.getRoute(htmlAttribAccountID); + displayNameOrLogin = getMentionDisplayText(displayNameOrLogin, htmlAttributeAccountID, lodashGet(user, 'login', '')); + navigationRoute = ROUTES.PROFILE.getRoute(htmlAttributeAccountID); } else if (!_.isEmpty(tnode.data)) { // We need to remove the LTR unicode and leading @ from data as it is not part of the login displayNameOrLogin = tnode.data.replace(CONST.UNICODE.LTR, '').slice(1); - tnode.data = tnode.data.replace(displayNameOrLogin, getMentionDisplayText(displayNameOrLogin, htmlAttribAccountID)); + // We need to replace tnode.data here because we will pass it to TNodeChildrenRenderer below + tnode.data = tnode.data.replace(displayNameOrLogin, getMentionDisplayText(displayNameOrLogin, htmlAttributeAccountID)); accountID = _.first(PersonalDetailsUtils.getAccountIDsByLogins([displayNameOrLogin])); navigationRoute = ROUTES.DETAILS.getRoute(displayNameOrLogin); @@ -98,7 +103,7 @@ function MentionUserRenderer(props) { // eslint-disable-next-line react/jsx-props-no-spreading {...defaultRendererProps} > - {!_.isEmpty(htmlAttribAccountID) ? `@${displayNameOrLogin}` : } + {!_.isEmpty(htmlAttributeAccountID) ? `@${displayNameOrLogin}` : } diff --git a/src/libs/LoginUtils.ts b/src/libs/LoginUtils.ts index 3b38ae716982..3781890013eb 100644 --- a/src/libs/LoginUtils.ts +++ b/src/libs/LoginUtils.ts @@ -66,9 +66,7 @@ function areEmailsFromSamePrivateDomain(email1: string, email2: string): boolean if (isEmailPublicDomain(email1) || isEmailPublicDomain(email2)) { return false; } - const emailDomain1 = Str.extractEmailDomain(email1).toLowerCase(); - const emailDomain2 = Str.extractEmailDomain(email2).toLowerCase(); - return emailDomain1 === emailDomain2; + return Str.extractEmailDomain(email1).toLowerCase() === Str.extractEmailDomain(email2).toLowerCase(); } export {getPhoneNumberWithoutSpecialChars, appendCountryCode, isEmailPublicDomain, validateNumber, getPhoneLogin, areEmailsFromSamePrivateDomain}; From 47359b7cc384b7c50de4b7bf20dfa7c2af73dd3f Mon Sep 17 00:00:00 2001 From: tienifr Date: Tue, 23 Jan 2024 10:49:13 +0700 Subject: [PATCH 292/391] edit comment --- .../HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.js b/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.js index 203c728fe2c6..a5172125dc3e 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.js +++ b/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.js @@ -42,8 +42,8 @@ function MentionUserRenderer(props) { const tnode = cloneDeep(props.tnode); const getMentionDisplayText = (displayText, userAccountID, userLogin = '') => { - // if the userAccountID does not exist, this is email-based mention so the displayName must be an email. - // If the userAccountID exists but userLogin is different from displayName, this means the displayName is either user display name, Hidden, or phone number, in which case we should return it as is. + // if the userAccountID does not exist, this is email-based mention so the displayText must be an email. + // If the userAccountID exists but userLogin is different from displayText, this means the displayText is either user display name, Hidden, or phone number, in which case we should return it as is. if (userAccountID && userLogin !== displayText) { return displayText; } From b97e34887116eeebe23aa7b5ed3e683aa7b824fb Mon Sep 17 00:00:00 2001 From: Dylan Date: Tue, 23 Jan 2024 13:56:39 +0700 Subject: [PATCH 293/391] Remove MoneyRequestConfirmPage --- src/ROUTES.ts | 4 - src/SCREENS.ts | 1 - .../AppNavigator/ModalStackNavigators.tsx | 1 - src/libs/Navigation/linkingConfig.ts | 1 - src/libs/Navigation/types.ts | 4 - .../step/IOURequestStepConfirmation.js | 14 +- .../iou/steps/MoneyRequestConfirmPage.js | 473 ------------------ 7 files changed, 2 insertions(+), 496 deletions(-) delete mode 100644 src/pages/iou/steps/MoneyRequestConfirmPage.js diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 5ebe05eb93e2..78b9dbbc172c 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -262,10 +262,6 @@ const ROUTES = { route: ':iouType/new/participants/:reportID?', getRoute: (iouType: string, reportID = '') => `${iouType}/new/participants/${reportID}` as const, }, - MONEY_REQUEST_CONFIRMATION: { - route: ':iouType/new/confirmation/:reportID?', - getRoute: (iouType: string, reportID = '') => `${iouType}/new/confirmation/${reportID}` as const, - }, MONEY_REQUEST_DATE: { route: ':iouType/new/date/:reportID?', getRoute: (iouType: string, reportID = '') => `${iouType}/new/date/${reportID}` as const, diff --git a/src/SCREENS.ts b/src/SCREENS.ts index e2f4a849d4aa..952bd860b3de 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -138,7 +138,6 @@ const SCREENS = { ROOT: 'Money_Request', AMOUNT: 'Money_Request_Amount', PARTICIPANTS: 'Money_Request_Participants', - CONFIRMATION: 'Money_Request_Confirmation', CURRENCY: 'Money_Request_Currency', DATE: 'Money_Request_Date', DESCRIPTION: 'Money_Request_Description', diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx index cf5eed232212..edd09f1de3c7 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx @@ -93,7 +93,6 @@ const MoneyRequestModalStackNavigator = createModalStackNavigator require('../../../pages/iou/MoneyRequestSelectorPage').default as React.ComponentType, [SCREENS.MONEY_REQUEST.AMOUNT]: () => require('../../../pages/iou/steps/NewRequestAmountPage').default as React.ComponentType, [SCREENS.MONEY_REQUEST.PARTICIPANTS]: () => require('../../../pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsPage').default as React.ComponentType, - [SCREENS.MONEY_REQUEST.CONFIRMATION]: () => require('../../../pages/iou/steps/MoneyRequestConfirmPage').default as React.ComponentType, [SCREENS.MONEY_REQUEST.CURRENCY]: () => require('../../../pages/iou/IOUCurrencySelection').default as React.ComponentType, [SCREENS.MONEY_REQUEST.DATE]: () => require('../../../pages/iou/MoneyRequestDatePage').default as React.ComponentType, [SCREENS.MONEY_REQUEST.DESCRIPTION]: () => require('../../../pages/iou/MoneyRequestDescriptionPage').default as React.ComponentType, diff --git a/src/libs/Navigation/linkingConfig.ts b/src/libs/Navigation/linkingConfig.ts index 16d4ef0e350f..32a3b3fb89dd 100644 --- a/src/libs/Navigation/linkingConfig.ts +++ b/src/libs/Navigation/linkingConfig.ts @@ -426,7 +426,6 @@ const linkingConfig: LinkingOptions = { [SCREENS.MONEY_REQUEST.STEP_TAX_AMOUNT]: ROUTES.MONEY_REQUEST_STEP_TAX_AMOUNT.route, [SCREENS.MONEY_REQUEST.STEP_TAX_RATE]: ROUTES.MONEY_REQUEST_STEP_TAX_RATE.route, [SCREENS.MONEY_REQUEST.PARTICIPANTS]: ROUTES.MONEY_REQUEST_PARTICIPANTS.route, - [SCREENS.MONEY_REQUEST.CONFIRMATION]: ROUTES.MONEY_REQUEST_CONFIRMATION.route, [SCREENS.MONEY_REQUEST.DATE]: ROUTES.MONEY_REQUEST_DATE.route, [SCREENS.MONEY_REQUEST.CURRENCY]: ROUTES.MONEY_REQUEST_CURRENCY.route, [SCREENS.MONEY_REQUEST.DESCRIPTION]: ROUTES.MONEY_REQUEST_DESCRIPTION.route, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index aa5ab47ad9ba..2501a0b3a094 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -185,10 +185,6 @@ type MoneyRequestNavigatorParamList = { iouType: string; reportID: string; }; - [SCREENS.MONEY_REQUEST.CONFIRMATION]: { - iouType: string; - reportID: string; - }; [SCREENS.MONEY_REQUEST.CURRENCY]: { iouType: string; reportID: string; diff --git a/src/pages/iou/request/step/IOURequestStepConfirmation.js b/src/pages/iou/request/step/IOURequestStepConfirmation.js index 9df2564ae38d..801568b1c5b8 100644 --- a/src/pages/iou/request/step/IOURequestStepConfirmation.js +++ b/src/pages/iou/request/step/IOURequestStepConfirmation.js @@ -8,6 +8,7 @@ import categoryPropTypes from '@components/categoryPropTypes'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import * as Expensicons from '@components/Icon/Expensicons'; import MoneyRequestConfirmationList from '@components/MoneyTemporaryForRefactorRequestConfirmationList'; +import {usePersonalDetails} from '@components/OnyxProvider'; import ScreenWrapper from '@components/ScreenWrapper'; import tagPropTypes from '@components/tagPropTypes'; import transactionPropTypes from '@components/transactionPropTypes'; @@ -23,7 +24,6 @@ import Navigation from '@libs/Navigation/Navigation'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as ReportUtils from '@libs/ReportUtils'; import * as TransactionUtils from '@libs/TransactionUtils'; -import personalDetailsPropType from '@pages/personalDetailsPropType'; import reportPropTypes from '@pages/reportPropTypes'; import {policyPropTypes} from '@pages/workspace/withPolicy'; import * as IOU from '@userActions/IOU'; @@ -43,9 +43,6 @@ const propTypes = { /** The personal details of the current user */ ...withCurrentUserPersonalDetailsPropTypes, - /** Personal details of all users */ - personalDetails: personalDetailsPropType, - /** The policy of the report */ ...policyPropTypes, @@ -62,7 +59,6 @@ const propTypes = { transaction: transactionPropTypes, }; const defaultProps = { - personalDetails: {}, policy: {}, policyCategories: {}, policyTags: {}, @@ -72,7 +68,6 @@ const defaultProps = { }; function IOURequestStepConfirmation({ currentUserPersonalDetails, - personalDetails, policy, policyTags, policyCategories, @@ -86,6 +81,7 @@ function IOURequestStepConfirmation({ const {translate} = useLocalize(); const {windowWidth} = useWindowDimensions(); const {isOffline} = useNetwork(); + const personalDetails = usePersonalDetails() || CONST.EMPTY_OBJECT; const [receiptFile, setReceiptFile] = useState(); const receiptFilename = lodashGet(transaction, 'filename'); const receiptPath = lodashGet(transaction, 'receipt.source'); @@ -385,12 +381,6 @@ export default compose( withCurrentUserPersonalDetails, withWritableReportOrNotFound, withFullTransactionOrNotFound, - withOnyx({ - personalDetails: { - key: ONYXKEYS.PERSONAL_DETAILS_LIST, - }, - }), - // eslint-disable-next-line rulesdir/no-multiple-onyx-in-file withOnyx({ policy: { key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY}${report ? report.policyID : '0'}`, diff --git a/src/pages/iou/steps/MoneyRequestConfirmPage.js b/src/pages/iou/steps/MoneyRequestConfirmPage.js deleted file mode 100644 index 1738ac78df47..000000000000 --- a/src/pages/iou/steps/MoneyRequestConfirmPage.js +++ /dev/null @@ -1,473 +0,0 @@ -import lodashGet from 'lodash/get'; -import PropTypes from 'prop-types'; -import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; -import {View} from 'react-native'; -import {withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; -import categoryPropTypes from '@components/categoryPropTypes'; -import HeaderWithBackButton from '@components/HeaderWithBackButton'; -import * as Expensicons from '@components/Icon/Expensicons'; -import MoneyRequestConfirmationList from '@components/MoneyRequestConfirmationList'; -import {usePersonalDetails} from '@components/OnyxProvider'; -import ScreenWrapper from '@components/ScreenWrapper'; -import tagPropTypes from '@components/tagPropTypes'; -import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsDefaultProps, withCurrentUserPersonalDetailsPropTypes} from '@components/withCurrentUserPersonalDetails'; -import withLocalize from '@components/withLocalize'; -import useInitialValue from '@hooks/useInitialValue'; -import useNetwork from '@hooks/useNetwork'; -import useThemeStyles from '@hooks/useThemeStyles'; -import useWindowDimensions from '@hooks/useWindowDimensions'; -import compose from '@libs/compose'; -import * as FileUtils from '@libs/fileDownload/FileUtils'; -import * as MoneyRequestUtils from '@libs/MoneyRequestUtils'; -import Navigation from '@libs/Navigation/Navigation'; -import * as OptionsListUtils from '@libs/OptionsListUtils'; -import * as ReportUtils from '@libs/ReportUtils'; -import {iouDefaultProps, iouPropTypes} from '@pages/iou/propTypes'; -import reportPropTypes from '@pages/reportPropTypes'; -import {policyDefaultProps, policyPropTypes} from '@pages/workspace/withPolicy'; -import * as IOU from '@userActions/IOU'; -import * as Policy from '@userActions/Policy'; -import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; -import ROUTES from '@src/ROUTES'; - -const propTypes = { - /** React Navigation route */ - route: PropTypes.shape({ - /** Params from the route */ - params: PropTypes.shape({ - /** The type of IOU report, i.e. bill, request, send */ - iouType: PropTypes.string, - - /** The report ID of the IOU */ - reportID: PropTypes.string, - }), - }).isRequired, - - report: reportPropTypes, - - /** Holds data related to Money Request view state, rather than the underlying Money Request data. */ - iou: iouPropTypes, - - /** The policy of the current request */ - policy: policyPropTypes, - - policyTags: tagPropTypes, - - policyCategories: PropTypes.objectOf(categoryPropTypes), - - ...withCurrentUserPersonalDetailsPropTypes, -}; - -const defaultProps = { - report: {}, - policyCategories: {}, - policyTags: {}, - iou: iouDefaultProps, - policy: policyDefaultProps, - ...withCurrentUserPersonalDetailsDefaultProps, -}; - -function MoneyRequestConfirmPage(props) { - const styles = useThemeStyles(); - const {isOffline} = useNetwork(); - const {windowWidth} = useWindowDimensions(); - const prevMoneyRequestId = useRef(props.iou.id); - const iouType = useInitialValue(() => lodashGet(props.route, 'params.iouType', '')); - const reportID = useInitialValue(() => lodashGet(props.route, 'params.reportID', '')); - const isDistanceRequest = MoneyRequestUtils.isDistanceRequest(iouType, props.selectedTab); - const isScanRequest = MoneyRequestUtils.isScanRequest(props.selectedTab); - const [receiptFile, setReceiptFile] = useState(); - const personalDetails = usePersonalDetails() || CONST.EMPTY_OBJECT; - - const participants = useMemo( - () => - _.map(props.iou.participants, (participant) => { - const isPolicyExpenseChat = lodashGet(participant, 'isPolicyExpenseChat', false); - return isPolicyExpenseChat ? OptionsListUtils.getPolicyExpenseReportOption(participant) : OptionsListUtils.getParticipantsOption(participant, personalDetails); - }), - [props.iou.participants, personalDetails], - ); - const isPolicyExpenseChat = useMemo(() => ReportUtils.isPolicyExpenseChat(ReportUtils.getRootParentReport(props.report)), [props.report]); - const isManualRequestDM = props.selectedTab === CONST.TAB_REQUEST.MANUAL && iouType === CONST.IOU.TYPE.REQUEST; - - useEffect(() => { - const policyExpenseChat = _.find(participants, (participant) => participant.isPolicyExpenseChat); - if (policyExpenseChat) { - Policy.openDraftWorkspaceRequest(policyExpenseChat.policyID); - } - }, [isOffline, participants, props.iou.billable, props.policy]); - - const defaultBillable = lodashGet(props.policy, 'defaultBillable', false); - useEffect(() => { - IOU.setMoneyRequestBillable(defaultBillable); - }, [defaultBillable, isOffline]); - - useEffect(() => { - if (!props.iou.receiptPath || !props.iou.receiptFilename) { - return; - } - const onSuccess = (file) => { - const receipt = file; - receipt.state = file && isManualRequestDM ? CONST.IOU.RECEIPT_STATE.OPEN : CONST.IOU.RECEIPT_STATE.SCANREADY; - setReceiptFile(receipt); - }; - const onFailure = () => { - Navigation.goBack(ROUTES.MONEY_REQUEST.getRoute(iouType, reportID)); - }; - FileUtils.readFileAsync(props.iou.receiptPath, props.iou.receiptFilename, onSuccess, onFailure); - }, [props.iou.receiptPath, props.iou.receiptFilename, isManualRequestDM, iouType, reportID]); - - useEffect(() => { - // ID in Onyx could change by initiating a new request in a separate browser tab or completing a request - if (!isDistanceRequest && prevMoneyRequestId.current !== props.iou.id) { - // The ID is cleared on completing a request. In that case, we will do nothing. - if (props.iou.id) { - Navigation.goBack(ROUTES.MONEY_REQUEST.getRoute(iouType, reportID), true); - } - return; - } - - // Reset the money request Onyx if the ID in Onyx does not match the ID from params - const moneyRequestId = `${iouType}${reportID}`; - const shouldReset = !isDistanceRequest && props.iou.id !== moneyRequestId && !_.isEmpty(reportID); - if (shouldReset) { - IOU.resetMoneyRequestInfo(moneyRequestId); - } - - if (_.isEmpty(props.iou.participants) || (props.iou.amount === 0 && !props.iou.receiptPath && !isDistanceRequest) || shouldReset || ReportUtils.isArchivedRoom(props.report)) { - Navigation.goBack(ROUTES.MONEY_REQUEST.getRoute(iouType, reportID), true); - } - - return () => { - prevMoneyRequestId.current = props.iou.id; - }; - }, [props.iou.participants, props.iou.amount, props.iou.id, props.iou.receiptPath, isDistanceRequest, props.report, iouType, reportID]); - - const navigateBack = () => { - let fallback; - if (reportID) { - fallback = ROUTES.MONEY_REQUEST.getRoute(iouType, reportID); - } else { - fallback = ROUTES.MONEY_REQUEST_PARTICIPANTS.getRoute(iouType); - } - Navigation.goBack(fallback); - }; - - /** - * @param {Array} selectedParticipants - * @param {String} trimmedComment - * @param {File} [receipt] - */ - const requestMoney = useCallback( - (selectedParticipants, trimmedComment, receipt) => { - IOU.requestMoney( - props.report, - props.iou.amount, - props.iou.currency, - props.iou.created, - props.iou.merchant, - props.currentUserPersonalDetails.login, - props.currentUserPersonalDetails.accountID, - selectedParticipants[0], - trimmedComment, - receipt, - props.iou.category, - props.iou.tag, - props.iou.billable, - props.policy, - props.policyTags, - props.policyCategories, - ); - }, - [ - props.report, - props.iou.amount, - props.iou.currency, - props.iou.created, - props.iou.merchant, - props.currentUserPersonalDetails.login, - props.currentUserPersonalDetails.accountID, - props.iou.category, - props.iou.tag, - props.iou.billable, - props.policy, - props.policyTags, - props.policyCategories, - ], - ); - - /** - * @param {Array} selectedParticipants - * @param {String} trimmedComment - */ - const createDistanceRequest = useCallback( - (selectedParticipants, trimmedComment) => { - IOU.createDistanceRequest( - props.report, - selectedParticipants[0], - trimmedComment, - props.iou.created, - props.iou.transactionID, - props.iou.category, - props.iou.tag, - props.iou.amount, - props.iou.currency, - props.iou.merchant, - props.iou.billable, - props.policy, - props.policyTags, - props.policyCategories, - ); - }, - [ - props.report, - props.iou.created, - props.iou.transactionID, - props.iou.category, - props.iou.tag, - props.iou.amount, - props.iou.currency, - props.iou.merchant, - props.iou.billable, - props.policy, - props.policyTags, - props.policyCategories, - ], - ); - - const createTransaction = useCallback( - (selectedParticipants) => { - const trimmedComment = props.iou.comment.trim(); - - // If we have a receipt let's start the split bill by creating only the action, the transaction, and the group DM if needed - if (iouType === CONST.IOU.TYPE.SPLIT && props.iou.receiptPath) { - const existingSplitChatReportID = CONST.REGEX.NUMBER.test(reportID) ? reportID : ''; - const onSuccess = (receipt) => { - IOU.startSplitBill( - selectedParticipants, - props.currentUserPersonalDetails.login, - props.currentUserPersonalDetails.accountID, - trimmedComment, - receipt, - existingSplitChatReportID, - ); - }; - FileUtils.readFileAsync(props.iou.receiptPath, props.iou.receiptFilename, onSuccess); - return; - } - - // IOUs created from a group report will have a reportID param in the route. - // Since the user is already viewing the report, we don't need to navigate them to the report - if (iouType === CONST.IOU.TYPE.SPLIT && CONST.REGEX.NUMBER.test(reportID)) { - IOU.splitBill( - selectedParticipants, - props.currentUserPersonalDetails.login, - props.currentUserPersonalDetails.accountID, - props.iou.amount, - trimmedComment, - props.iou.currency, - props.iou.category, - props.iou.tag, - reportID, - props.iou.merchant, - ); - return; - } - - // If the request is created from the global create menu, we also navigate the user to the group report - if (iouType === CONST.IOU.TYPE.SPLIT) { - IOU.splitBillAndOpenReport( - selectedParticipants, - props.currentUserPersonalDetails.login, - props.currentUserPersonalDetails.accountID, - props.iou.amount, - trimmedComment, - props.iou.currency, - props.iou.category, - props.iou.tag, - props.iou.merchant, - ); - return; - } - - if (receiptFile) { - requestMoney(selectedParticipants, trimmedComment, receiptFile); - return; - } - - if (isDistanceRequest) { - createDistanceRequest(selectedParticipants, trimmedComment); - return; - } - - requestMoney(selectedParticipants, trimmedComment); - }, - [ - props.iou.amount, - props.iou.comment, - props.currentUserPersonalDetails.login, - props.currentUserPersonalDetails.accountID, - props.iou.currency, - props.iou.category, - props.iou.tag, - props.iou.receiptPath, - props.iou.receiptFilename, - isDistanceRequest, - requestMoney, - createDistanceRequest, - receiptFile, - iouType, - reportID, - props.iou.merchant, - ], - ); - - /** - * Checks if user has a GOLD wallet then creates a paid IOU report on the fly - * - * @param {String} paymentMethodType - */ - const sendMoney = useCallback( - (paymentMethodType) => { - const currency = props.iou.currency; - const trimmedComment = props.iou.comment.trim(); - const participant = participants[0]; - - if (paymentMethodType === CONST.IOU.PAYMENT_TYPE.ELSEWHERE) { - IOU.sendMoneyElsewhere(props.report, props.iou.amount, currency, trimmedComment, props.currentUserPersonalDetails.accountID, participant); - return; - } - - if (paymentMethodType === CONST.IOU.PAYMENT_TYPE.EXPENSIFY) { - IOU.sendMoneyWithWallet(props.report, props.iou.amount, currency, trimmedComment, props.currentUserPersonalDetails.accountID, participant); - } - }, - [props.iou.amount, props.iou.comment, participants, props.iou.currency, props.currentUserPersonalDetails.accountID, props.report], - ); - - const headerTitle = () => { - if (isDistanceRequest) { - return props.translate('common.distance'); - } - - if (iouType === CONST.IOU.TYPE.SPLIT) { - return props.translate('iou.split'); - } - - if (iouType === CONST.IOU.TYPE.SEND) { - return props.translate('common.send'); - } - - if (isScanRequest) { - return props.translate('tabSelector.scan'); - } - - return props.translate('tabSelector.manual'); - }; - - return ( - - {({safeAreaPaddingBottomStyle}) => ( - - Navigation.navigate(ROUTES.MONEY_REQUEST_RECEIPT.getRoute(iouType, reportID)), - }, - ]} - /> - { - const newParticipants = _.map(props.iou.participants, (participant) => { - if (participant.accountID === option.accountID) { - return {...participant, selected: !participant.selected}; - } - return participant; - }); - IOU.setMoneyRequestParticipants(newParticipants); - }} - receiptPath={props.iou.receiptPath} - receiptFilename={props.iou.receiptFilename} - iouType={iouType} - reportID={reportID} - isPolicyExpenseChat={isPolicyExpenseChat} - // The participants can only be modified when the action is initiated from directly within a group chat and not the floating-action-button. - // This is because when there is a group of people, say they are on a trip, and you have some shared expenses with some of the people, - // but not all of them (maybe someone skipped out on dinner). Then it's nice to be able to select/deselect people from the group chat bill - // split rather than forcing the user to create a new group, just for that expense. The reportID is empty, when the action was initiated from - // the floating-action-button (since it is something that exists outside the context of a report). - canModifyParticipants={!_.isEmpty(reportID)} - policyID={props.report.policyID} - bankAccountRoute={ReportUtils.getBankAccountRoute(props.report)} - iouMerchant={props.iou.merchant} - iouCreated={props.iou.created} - isScanRequest={isScanRequest} - isDistanceRequest={isDistanceRequest} - shouldShowSmartScanFields={_.isEmpty(props.iou.receiptPath)} - /> - - )} - - ); -} - -MoneyRequestConfirmPage.displayName = 'MoneyRequestConfirmPage'; -MoneyRequestConfirmPage.propTypes = propTypes; -MoneyRequestConfirmPage.defaultProps = defaultProps; - -export default compose( - withCurrentUserPersonalDetails, - withLocalize, - withOnyx({ - iou: { - key: ONYXKEYS.IOU, - }, - }), - // eslint-disable-next-line rulesdir/no-multiple-onyx-in-file - withOnyx({ - report: { - key: ({route, iou}) => { - const reportID = IOU.getIOUReportID(iou, route); - - return `${ONYXKEYS.COLLECTION.REPORT}${reportID}`; - }, - }, - selectedTab: { - key: `${ONYXKEYS.COLLECTION.SELECTED_TAB}${CONST.TAB.RECEIPT_TAB_ID}`, - }, - }), - withOnyx({ - policy: { - key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY}${report ? report.policyID : '0'}`, - }, - policyCategories: { - key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${report ? report.policyID : '0'}`, - }, - policyTags: { - key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY_TAGS}${report ? report.policyID : '0'}`, - }, - }), -)(MoneyRequestConfirmPage); From a54a0addd8dac4a58b0dc3d691b7bdcf165f1c0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Tue, 23 Jan 2024 08:22:29 +0100 Subject: [PATCH 294/391] code clean + documentation --- src/libs/E2E/actions/e2eLogin.ts | 10 +--------- src/libs/E2E/utils/NetworkInterceptor.ts | 14 ++++++++++++++ src/libs/E2E/utils/getConfigValueOrThrow.ts | 12 ++++++++++++ 3 files changed, 27 insertions(+), 9 deletions(-) create mode 100644 src/libs/E2E/utils/getConfigValueOrThrow.ts diff --git a/src/libs/E2E/actions/e2eLogin.ts b/src/libs/E2E/actions/e2eLogin.ts index 741146837a16..f98eab5005e1 100644 --- a/src/libs/E2E/actions/e2eLogin.ts +++ b/src/libs/E2E/actions/e2eLogin.ts @@ -1,20 +1,12 @@ /* eslint-disable rulesdir/prefer-actions-set-data */ /* eslint-disable rulesdir/prefer-onyx-connect-in-libs */ -import Config from 'react-native-config'; import Onyx from 'react-native-onyx'; import {Authenticate} from '@libs/Authentication'; +import getConfigValueOrThrow from '@libs/E2E/utils/getConfigValueOrThrow'; import CONFIG from '@src/CONFIG'; import ONYXKEYS from '@src/ONYXKEYS'; -function getConfigValueOrThrow(key: string): string { - const value = Config[key]; - if (value == null) { - throw new Error(`Missing config value for ${key}`); - } - return value; -} - const e2eUserCredentials = { email: getConfigValueOrThrow('EXPENSIFY_PARTNER_PASSWORD_EMAIL'), partnerUserID: getConfigValueOrThrow('EXPENSIFY_PARTNER_USER_ID'), diff --git a/src/libs/E2E/utils/NetworkInterceptor.ts b/src/libs/E2E/utils/NetworkInterceptor.ts index 756bee156bc5..361e14d9fdb7 100644 --- a/src/libs/E2E/utils/NetworkInterceptor.ts +++ b/src/libs/E2E/utils/NetworkInterceptor.ts @@ -46,6 +46,9 @@ function fetchArgsGetRequestInit(args: Parameters): RequestInit { return firstArg; } +/** + * This function extracts the url from the arguments of fetch. + */ function fetchArgsGetUrl(args: Parameters): string { const [firstArg] = args; if (typeof firstArg === 'string') { @@ -60,6 +63,9 @@ function fetchArgsGetUrl(args: Parameters): string { throw new Error('Could not get url from fetch args'); } +/** + * This function transforms a NetworkCacheEntry (internal representation) to a (fetch) Response. + */ function networkCacheEntryToResponse({headers, status, statusText, body}: NetworkCacheEntry): Response { // Transform headers to Headers object: const newHeaders = new Headers(); @@ -87,6 +93,14 @@ function hashFetchArgs(args: Parameters) { return `${url}${JSON.stringify(headers)}`; } +/** + * Install a network interceptor by overwriting the global fetch function: + * - Overwrites fetch globally with a custom implementation + * - For each fetch request we cache the request and the response + * - The cache is send to the test runner server to persist the network cache in between sessions + * - On e2e test start the network cache is requested and loaded + * - If a fetch request is already in the NetworkInterceptors cache instead of making a real API request the value from the cache is used. + */ export default function installNetworkInterceptor( getNetworkCache: () => Promise, updateNetworkCache: (networkCache: NetworkCacheMap) => Promise, diff --git a/src/libs/E2E/utils/getConfigValueOrThrow.ts b/src/libs/E2E/utils/getConfigValueOrThrow.ts new file mode 100644 index 000000000000..c29586b481a9 --- /dev/null +++ b/src/libs/E2E/utils/getConfigValueOrThrow.ts @@ -0,0 +1,12 @@ +import Config from 'react-native-config'; + +/** + * Gets a build-in config value or throws an error if the value is not defined. + */ +export default function getConfigValueOrThrow(key: string): string { + const value = Config[key]; + if (value == null) { + throw new Error(`Missing config value for ${key}`); + } + return value; +} From a6f7b5aae37ce9b248c395ef19e1dfd462a027a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Tue, 23 Jan 2024 08:56:13 +0100 Subject: [PATCH 295/391] put report ID in e2e test configs --- src/libs/E2E/client.ts | 6 +----- src/libs/E2E/reactNativeLaunchingTest.ts | 5 +++-- src/libs/E2E/tests/chatOpeningTest.e2e.ts | 7 ++++--- src/libs/E2E/tests/reportTypingTest.e2e.ts | 8 ++++++-- src/libs/E2E/types.ts | 7 ++++++- src/libs/E2E/utils/getConfigValueOrThrow.ts | 6 +++--- tests/e2e/config.js | 9 ++++----- 7 files changed, 27 insertions(+), 21 deletions(-) diff --git a/src/libs/E2E/client.ts b/src/libs/E2E/client.ts index 30822063b558..265c55c4a230 100644 --- a/src/libs/E2E/client.ts +++ b/src/libs/E2E/client.ts @@ -1,6 +1,6 @@ import Config from '../../../tests/e2e/config'; import Routes from '../../../tests/e2e/server/routes'; -import type {NetworkCacheMap} from './types'; +import type {NetworkCacheMap, TestConfig} from './types'; type TestResult = { name: string; @@ -10,10 +10,6 @@ type TestResult = { renderCount?: number; }; -type TestConfig = { - name: string; -}; - type NativeCommandPayload = { text: string; }; diff --git a/src/libs/E2E/reactNativeLaunchingTest.ts b/src/libs/E2E/reactNativeLaunchingTest.ts index c86df7afee95..79276e7a5d75 100644 --- a/src/libs/E2E/reactNativeLaunchingTest.ts +++ b/src/libs/E2E/reactNativeLaunchingTest.ts @@ -13,8 +13,9 @@ import E2EConfig from '../../../tests/e2e/config'; import E2EClient from './client'; import installNetworkInterceptor from './utils/NetworkInterceptor'; import LaunchArgs from './utils/LaunchArgs'; +import type { TestConfig } from './types'; -type Tests = Record, () => void>; +type Tests = Record, (config: TestConfig) => void>; console.debug('=========================='); console.debug('==== Running e2e test ===='); @@ -74,7 +75,7 @@ E2EClient.getTestConfig() .then(() => { console.debug('[E2E] App is ready, running test…'); Performance.measureFailSafe('appStartedToReady', 'regularAppStart'); - test(); + test(config); }) .catch((error) => { console.error('[E2E] Error while waiting for app to become ready', error); diff --git a/src/libs/E2E/tests/chatOpeningTest.e2e.ts b/src/libs/E2E/tests/chatOpeningTest.e2e.ts index abbbf6b92acf..ef380f847c3f 100644 --- a/src/libs/E2E/tests/chatOpeningTest.e2e.ts +++ b/src/libs/E2E/tests/chatOpeningTest.e2e.ts @@ -1,17 +1,18 @@ import E2ELogin from '@libs/E2E/actions/e2eLogin'; import waitForAppLoaded from '@libs/E2E/actions/waitForAppLoaded'; import E2EClient from '@libs/E2E/client'; +import type {TestConfig} from '@libs/E2E/types'; +import getConfigValueOrThrow from '@libs/E2E/utils/getConfigValueOrThrow'; import Navigation from '@libs/Navigation/Navigation'; import Performance from '@libs/Performance'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; -const test = () => { +const test = (config: TestConfig) => { // check for login (if already logged in the action will simply resolve) console.debug('[E2E] Logging in for chat opening'); - // #announce Chat with many messages - const reportID = '5421294415618529'; + const reportID = getConfigValueOrThrow('reportID', config); E2ELogin().then((neededLogin) => { if (neededLogin) { diff --git a/src/libs/E2E/tests/reportTypingTest.e2e.ts b/src/libs/E2E/tests/reportTypingTest.e2e.ts index a4bb2144086a..4e0678aeb020 100644 --- a/src/libs/E2E/tests/reportTypingTest.e2e.ts +++ b/src/libs/E2E/tests/reportTypingTest.e2e.ts @@ -3,6 +3,8 @@ import E2ELogin from '@libs/E2E/actions/e2eLogin'; import waitForAppLoaded from '@libs/E2E/actions/waitForAppLoaded'; import waitForKeyboard from '@libs/E2E/actions/waitForKeyboard'; import E2EClient from '@libs/E2E/client'; +import type {TestConfig} from '@libs/E2E/types'; +import getConfigValueOrThrow from '@libs/E2E/utils/getConfigValueOrThrow'; import Navigation from '@libs/Navigation/Navigation'; import Performance from '@libs/Performance'; import {getRerenderCount, resetRerenderCount} from '@pages/home/report/ReportActionCompose/ComposerWithSuggestions/index.e2e'; @@ -10,10 +12,12 @@ import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; import * as NativeCommands from '../../../../tests/e2e/nativeCommands/NativeCommandsAction'; -const test = () => { +const test = (config: TestConfig) => { // check for login (if already logged in the action will simply resolve) console.debug('[E2E] Logging in for typing'); + const reportID = getConfigValueOrThrow('reportID', config); + E2ELogin().then((neededLogin) => { if (neededLogin) { return waitForAppLoaded().then(() => @@ -31,7 +35,7 @@ const test = () => { console.debug(`[E2E] Sidebar loaded, navigating to a report…`); // Crowded Policy (Do Not Delete) Report, has a input bar available: - Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute('8268282951170052')); + Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(reportID)); // Wait until keyboard is visible (so we are focused on the input): waitForKeyboard().then(() => { diff --git a/src/libs/E2E/types.ts b/src/libs/E2E/types.ts index 9d769cb40ed3..2d48813fa115 100644 --- a/src/libs/E2E/types.ts +++ b/src/libs/E2E/types.ts @@ -18,4 +18,9 @@ type NetworkCacheMap = Record< NetworkCacheEntry >; -export type {SigninParams, IsE2ETestSession, NetworkCacheMap, NetworkCacheEntry}; +type TestConfig = { + name: string; + [key: string]: string; +}; + +export type {SigninParams, IsE2ETestSession, NetworkCacheMap, NetworkCacheEntry, TestConfig}; diff --git a/src/libs/E2E/utils/getConfigValueOrThrow.ts b/src/libs/E2E/utils/getConfigValueOrThrow.ts index c29586b481a9..a694d6709ed6 100644 --- a/src/libs/E2E/utils/getConfigValueOrThrow.ts +++ b/src/libs/E2E/utils/getConfigValueOrThrow.ts @@ -1,10 +1,10 @@ import Config from 'react-native-config'; /** - * Gets a build-in config value or throws an error if the value is not defined. + * Gets a config value or throws an error if the value is not defined. */ -export default function getConfigValueOrThrow(key: string): string { - const value = Config[key]; +export default function getConfigValueOrThrow(key: string, config = Config): string { + const value = config[key]; if (value == null) { throw new Error(`Missing config value for ${key}`); } diff --git a/tests/e2e/config.js b/tests/e2e/config.js index d782ec4316e5..a7447a29c954 100644 --- a/tests/e2e/config.js +++ b/tests/e2e/config.js @@ -1,10 +1,5 @@ const OUTPUT_DIR = process.env.WORKING_DIRECTORY || './tests/e2e/results'; -/** - * @typedef TestConfig - * @property {string} name - */ - // add your test name here … const TEST_NAMES = { AppStartTime: 'App start time', @@ -82,9 +77,13 @@ module.exports = { reportScreen: { autoFocus: true, }, + // Crowded Policy (Do Not Delete) Report, has a input bar available: + reportID: '8268282951170052', }, [TEST_NAMES.ChatOpening]: { name: TEST_NAMES.ChatOpening, + // #announce Chat with many messages + reportID: '5421294415618529', }, }, }; From e24c2327552d89bfb337700808fd04947cef4b8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Tue, 23 Jan 2024 09:02:09 +0100 Subject: [PATCH 296/391] e2e tests doc update --- tests/e2e/README.md | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/tests/e2e/README.md b/tests/e2e/README.md index 8c7be011502d..a142530d4388 100644 --- a/tests/e2e/README.md +++ b/tests/e2e/README.md @@ -2,7 +2,7 @@ This directory contains the scripts and configuration files for running the performance regression tests. These tests are called E2E tests because they -run the app on a real device (physical or emulated). +run the actual app on a real device (physical or emulated). ![Example of a e2e test run](https://raw.githubusercontent.com/hannojg/expensify-app/5f945c25e2a0650753f47f3f541b984f4d114f6d/e2e/example.gif) @@ -116,6 +116,18 @@ from one test (e.g. measuring multiple things at the same time). To finish a test call `E2EClient.submitTestDone()`. +## Network calls + +Network calls can add a variance to the test results. To mitigate this in the past we used to provide mocks for the API +calls. However, this is not a realistic scenario, as we want to test the app in a realistic environment. + +Now we have a module called `NetworkInterceptor`. The interceptor will intercept all network calls and will +cache the request and response. The next time the same request is made, it will return the cached response. + +When writing a test you usually don't need to care about this, as the interceptor is enabled by default. +However, look out for "!!! Missed cache hit for url" logs when developing your test. This can indicate a bug +with the NetworkInterceptor where a request should have been cached but wasn't (which would introduce variance in your test!). + ## Android specifics From acdd24bb3ec97736feeaf4d666f7975294cb7709 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Tue, 23 Jan 2024 09:34:45 +0100 Subject: [PATCH 297/391] Fix typecheck after merging main --- src/ONYXKEYS.ts | 8 ++++---- src/components/Form/FormWrapper.tsx | 3 +++ src/components/Form/InputWrapper.tsx | 4 ++-- src/components/Form/types.ts | 6 +++--- src/pages/EditReportFieldDatePage.tsx | 17 ++++++++++------- src/pages/EditReportFieldTextPage.tsx | 14 ++++++++------ .../TeachersUnite/IntroSchoolPrincipalPage.tsx | 18 +++++------------- src/pages/TeachersUnite/KnowATeacherPage.tsx | 15 +++------------ src/types/onyx/Form.ts | 14 +++++++++++++- src/types/onyx/index.ts | 4 +++- 10 files changed, 54 insertions(+), 49 deletions(-) diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index aa5cd7fe06f1..ab9af6112693 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -518,10 +518,10 @@ type OnyxValues = { [ONYXKEYS.FORMS.SETTINGS_STATUS_SET_CLEAR_AFTER_FORM_DRAFT]: OnyxTypes.Form; [ONYXKEYS.FORMS.PRIVATE_NOTES_FORM]: OnyxTypes.Form; [ONYXKEYS.FORMS.PRIVATE_NOTES_FORM_DRAFT]: OnyxTypes.Form; - [ONYXKEYS.FORMS.I_KNOW_A_TEACHER_FORM]: OnyxTypes.Form; - [ONYXKEYS.FORMS.I_KNOW_A_TEACHER_FORM_DRAFT]: OnyxTypes.Form; - [ONYXKEYS.FORMS.INTRO_SCHOOL_PRINCIPAL_FORM]: OnyxTypes.Form; - [ONYXKEYS.FORMS.INTRO_SCHOOL_PRINCIPAL_FORM_DRAFT]: OnyxTypes.Form; + [ONYXKEYS.FORMS.I_KNOW_A_TEACHER_FORM]: OnyxTypes.IKnowATeacherForm; + [ONYXKEYS.FORMS.I_KNOW_A_TEACHER_FORM_DRAFT]: OnyxTypes.IKnowATeacherForm; + [ONYXKEYS.FORMS.INTRO_SCHOOL_PRINCIPAL_FORM]: OnyxTypes.IntroSchoolPrincipalForm; + [ONYXKEYS.FORMS.INTRO_SCHOOL_PRINCIPAL_FORM_DRAFT]: OnyxTypes.IntroSchoolPrincipalForm; [ONYXKEYS.FORMS.REPORT_VIRTUAL_CARD_FRAUD]: OnyxTypes.Form; [ONYXKEYS.FORMS.REPORT_VIRTUAL_CARD_FRAUD_DRAFT]: OnyxTypes.Form; [ONYXKEYS.FORMS.REPORT_PHYSICAL_CARD_FORM]: OnyxTypes.Form; diff --git a/src/components/Form/FormWrapper.tsx b/src/components/Form/FormWrapper.tsx index cdf66d986472..d5b47761e4c0 100644 --- a/src/components/Form/FormWrapper.tsx +++ b/src/components/Form/FormWrapper.tsx @@ -33,6 +33,9 @@ type FormWrapperProps = ChildrenProps & /** Assuming refs are React refs */ inputRefs: RefObject; + + /** Callback to submit the form */ + onSubmit: () => void; }; function FormWrapper({ diff --git a/src/components/Form/InputWrapper.tsx b/src/components/Form/InputWrapper.tsx index 68dd7219f96a..ae78e909753b 100644 --- a/src/components/Form/InputWrapper.tsx +++ b/src/components/Form/InputWrapper.tsx @@ -1,11 +1,11 @@ import type {ForwardedRef} from 'react'; import React, {forwardRef, useContext} from 'react'; +import type {AnimatedTextInputRef} from '@components/RNTextInput'; import TextInput from '@components/TextInput'; -import type {BaseTextInputRef} from '@components/TextInput/BaseTextInput/types'; import FormContext from './FormContext'; import type {InputWrapperProps, ValidInputs} from './types'; -function InputWrapper({InputComponent, inputID, valueType = 'string', ...rest}: InputWrapperProps, ref: ForwardedRef) { +function InputWrapper({InputComponent, inputID, valueType = 'string', ...rest}: InputWrapperProps, ref: ForwardedRef) { const {registerInput} = useContext(FormContext); // There are inputs that don't have onBlur methods, to simulate the behavior of onBlur in e.g. checkbox, we had to // use different methods like onPress. This introduced a problem that inputs that have the onBlur method were diff --git a/src/components/Form/types.ts b/src/components/Form/types.ts index 1418c900c022..447f3205ad68 100644 --- a/src/components/Form/types.ts +++ b/src/components/Form/types.ts @@ -36,13 +36,13 @@ type BaseInputProps = { shouldSaveDraft?: boolean; shouldUseDefaultValue?: boolean; key?: Key | null | undefined; - ref?: Ref; + ref?: Ref; isFocused?: boolean; measureLayout?: (ref: unknown, callback: MeasureLayoutOnSuccessCallback) => void; focus?: () => void; }; -type InputWrapperProps = BaseInputProps & +type InputWrapperProps = Omit & ComponentProps & { InputComponent: TInput; inputID: string; @@ -65,7 +65,7 @@ type FormProps = { isSubmitButtonVisible?: boolean; /** Callback to submit the form */ - onSubmit: (values?: Record) => void; + onSubmit: (values: OnyxFormValuesFields) => void; /** Should the button be enabled when offline */ enabledWhenOffline?: boolean; diff --git a/src/pages/EditReportFieldDatePage.tsx b/src/pages/EditReportFieldDatePage.tsx index 5ee86b2bf8e6..6faa84ef8b43 100644 --- a/src/pages/EditReportFieldDatePage.tsx +++ b/src/pages/EditReportFieldDatePage.tsx @@ -3,12 +3,15 @@ import {View} from 'react-native'; import DatePicker from '@components/DatePicker'; import FormProvider from '@components/Form/FormProvider'; import InputWrapper from '@components/Form/InputWrapper'; +import type {OnyxFormValuesFields} from '@components/Form/types'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import type {AnimatedTextInputRef} from '@components/RNTextInput'; import ScreenWrapper from '@components/ScreenWrapper'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import type {Errors} from '@src/types/onyx/OnyxCommon'; type EditReportFieldDatePageProps = { /** Value of the policy report field */ @@ -27,12 +30,13 @@ type EditReportFieldDatePageProps = { function EditReportFieldDatePage({fieldName, onSubmit, fieldValue, fieldID}: EditReportFieldDatePageProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); - const inputRef = useRef(null); + const inputRef = useRef(null); const validate = useCallback( - (value: Record) => { - const errors: Record = {}; - if (value[fieldID].trim() === '') { + (values: OnyxFormValuesFields) => { + const errors: Errors = {}; + const value = values[fieldID]; + if (typeof value === 'string' && value.trim() === '') { errors[fieldID] = 'common.error.fieldRequired'; } return errors; @@ -48,7 +52,6 @@ function EditReportFieldDatePage({fieldName, onSubmit, fieldValue, fieldID}: Edi testID={EditReportFieldDatePage.displayName} > - {/* @ts-expect-error TODO: TS migration */} - InputComponent={DatePicker} inputID={fieldID} name={fieldID} diff --git a/src/pages/EditReportFieldTextPage.tsx b/src/pages/EditReportFieldTextPage.tsx index b468861e9a27..80cc700fec69 100644 --- a/src/pages/EditReportFieldTextPage.tsx +++ b/src/pages/EditReportFieldTextPage.tsx @@ -2,13 +2,16 @@ import React, {useCallback, useRef} from 'react'; import {View} from 'react-native'; import FormProvider from '@components/Form/FormProvider'; import InputWrapper from '@components/Form/InputWrapper'; +import type {OnyxFormValuesFields} from '@components/Form/types'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import type {AnimatedTextInputRef} from '@components/RNTextInput'; import ScreenWrapper from '@components/ScreenWrapper'; import TextInput from '@components/TextInput'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import type {Errors} from '@src/types/onyx/OnyxCommon'; type EditReportFieldTextPageProps = { /** Value of the policy report field */ @@ -27,12 +30,13 @@ type EditReportFieldTextPageProps = { function EditReportFieldTextPage({fieldName, onSubmit, fieldValue, fieldID}: EditReportFieldTextPageProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); - const inputRef = useRef(null); + const inputRef = useRef(null); const validate = useCallback( - (value: Record) => { - const errors: Record = {}; - if (value[fieldID].trim() === '') { + (values: OnyxFormValuesFields<'policyReportFieldEditForm'>) => { + const errors: Errors = {}; + const value = values[fieldID]; + if (typeof value === 'string' && value.trim() === '') { errors[fieldID] = 'common.error.fieldRequired'; } return errors; @@ -48,7 +52,6 @@ function EditReportFieldTextPage({fieldName, onSubmit, fieldValue, fieldID}: Edi testID={EditReportFieldTextPage.displayName} > - {/* @ts-expect-error TODO: TS migration */} ; @@ -42,7 +38,7 @@ function IntroSchoolPrincipalPage(props: IntroSchoolPrincipalPageProps) { /** * Submit form to pass firstName, partnerUserID and lastName */ - const onSubmit = (values: IntroSchoolPrincipalFormData) => { + const onSubmit = (values: OnyxFormValuesFields) => { const policyID = isProduction ? CONST.TEACHERS_UNITE.PROD_POLICY_ID : CONST.TEACHERS_UNITE.TEST_POLICY_ID; TeachersUnite.addSchoolPrincipal(values.firstName.trim(), values.partnerUserID.trim(), values.lastName.trim(), policyID); }; @@ -51,8 +47,8 @@ function IntroSchoolPrincipalPage(props: IntroSchoolPrincipalPageProps) { * @returns - An object containing the errors for each inputID */ const validate = useCallback( - (values: IntroSchoolPrincipalFormData) => { - const errors = {}; + (values: OnyxFormValuesFields) => { + const errors: Errors = {}; if (!ValidationUtils.isValidLegalName(values.firstName)) { ErrorUtils.addErrorMessage(errors, 'firstName', 'privatePersonalDetails.error.hasInvalidCharacter'); @@ -91,7 +87,6 @@ function IntroSchoolPrincipalPage(props: IntroSchoolPrincipalPageProps) { title={translate('teachersUnitePage.introSchoolPrincipal')} onBackButtonPress={() => Navigation.goBack(ROUTES.TEACHERS_UNITE)} /> - {/* @ts-expect-error TODO: Remove this once FormProvider (https://github.com/Expensify/App/issues/31972) is migrated to TypeScript. */} {translate('teachersUnitePage.schoolPrincipalVerfiyExpense')} ; }; @@ -42,7 +37,7 @@ function KnowATeacherPage(props: KnowATeacherPageProps) { /** * Submit form to pass firstName, partnerUserID and lastName */ - const onSubmit = (values: KnowATeacherFormData) => { + const onSubmit = (values: OnyxFormValuesFields) => { const phoneLogin = LoginUtils.getPhoneLogin(values.partnerUserID); const validateIfnumber = LoginUtils.validateNumber(phoneLogin); const contactMethod = (validateIfnumber || values.partnerUserID).trim().toLowerCase(); @@ -58,7 +53,7 @@ function KnowATeacherPage(props: KnowATeacherPageProps) { * @returns - An object containing the errors for each inputID */ const validate = useCallback( - (values: KnowATeacherFormData) => { + (values: OnyxFormValuesFields) => { const errors = {}; const phoneLogin = LoginUtils.getPhoneLogin(values.partnerUserID); const validateIfnumber = LoginUtils.validateNumber(phoneLogin); @@ -97,7 +92,6 @@ function KnowATeacherPage(props: KnowATeacherPageProps) { title={translate('teachersUnitePage.iKnowATeacher')} onBackButtonPress={() => Navigation.goBack(ROUTES.TEACHERS_UNITE)} /> - {/* @ts-expect-error TODO: Remove this once FormProvider (https://github.com/Expensify/App/issues/31972) is migrated to TypeScript. */} {translate('teachersUnitePage.getInTouch')} ; +type IKnowATeacherForm = Form<{ + firstName: string; + lastName: string; + partnerUserID: string; +}>; + +type IntroSchoolPrincipalForm = Form<{ + firstName: string; + lastName: string; + partnerUserID: string; +}>; + export default Form; -export type {AddDebitCardForm, DateOfBirthForm, DisplayNameForm, FormValueType, NewRoomForm, BaseForm}; +export type {AddDebitCardForm, DateOfBirthForm, DisplayNameForm, FormValueType, NewRoomForm, BaseForm, IKnowATeacherForm, IntroSchoolPrincipalForm}; diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts index 50497667917e..2aa794ffc5a3 100644 --- a/src/types/onyx/index.ts +++ b/src/types/onyx/index.ts @@ -9,7 +9,7 @@ import type Credentials from './Credentials'; import type Currency from './Currency'; import type CustomStatusDraft from './CustomStatusDraft'; import type Download from './Download'; -import type {AddDebitCardForm, DateOfBirthForm, DisplayNameForm, NewRoomForm} from './Form'; +import type {AddDebitCardForm, DateOfBirthForm, DisplayNameForm, IKnowATeacherForm, IntroSchoolPrincipalForm, NewRoomForm} from './Form'; import type Form from './Form'; import type FrequentlyUsedEmoji from './FrequentlyUsedEmoji'; import type {FundList} from './Fund'; @@ -146,4 +146,6 @@ export type { PolicyReportFields, RecentlyUsedReportFields, NewRoomForm, + IKnowATeacherForm, + IntroSchoolPrincipalForm, }; From 4bc5494da62b0f4dcf4774a34e95a1dcdf0ed8e1 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Tue, 23 Jan 2024 09:46:31 +0100 Subject: [PATCH 298/391] Use Onyx key instead of plain string --- src/pages/EditReportFieldTextPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/EditReportFieldTextPage.tsx b/src/pages/EditReportFieldTextPage.tsx index 80cc700fec69..733bfd6e5fee 100644 --- a/src/pages/EditReportFieldTextPage.tsx +++ b/src/pages/EditReportFieldTextPage.tsx @@ -33,7 +33,7 @@ function EditReportFieldTextPage({fieldName, onSubmit, fieldValue, fieldID}: Edi const inputRef = useRef(null); const validate = useCallback( - (values: OnyxFormValuesFields<'policyReportFieldEditForm'>) => { + (values: OnyxFormValuesFields) => { const errors: Errors = {}; const value = values[fieldID]; if (typeof value === 'string' && value.trim() === '') { From b2b2fdf5bef424cc708af7dce497d449d902cb5f Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Tue, 23 Jan 2024 11:14:18 +0100 Subject: [PATCH 299/391] minor improvements --- src/libs/DateUtils.ts | 2 +- tests/unit/DateUtilsTest.js | 13 +++++++++---- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/libs/DateUtils.ts b/src/libs/DateUtils.ts index 188e0e54afe8..526769723531 100644 --- a/src/libs/DateUtils.ts +++ b/src/libs/DateUtils.ts @@ -734,7 +734,7 @@ function formatToSupportedTimezone(timezoneInput: Timezone): Timezone { } /** - * Returns the latest business day of input date month + * Returns the last business day of given date month * * param {Date} inputDate * returns {number} diff --git a/tests/unit/DateUtilsTest.js b/tests/unit/DateUtilsTest.js index a8bdc6c6b7bc..be38eae9251d 100644 --- a/tests/unit/DateUtilsTest.js +++ b/tests/unit/DateUtilsTest.js @@ -217,23 +217,28 @@ describe('DateUtils', () => { describe.only('getLastBusinessDayOfMonth', () => { const scenarios = [ { - // Last business of May in 2025 + // Last business day of May in 2025 inputDate: new Date(2025, 4), expectedResult: 30, }, { - // Last business of January in 2024 + // Last business day of February in 2024 + inputDate: new Date(2024, 2), + expectedResult: 29, + }, + { + // Last business day of January in 2024 inputDate: new Date(2024, 0), expectedResult: 31, }, { - // Last business of September in 2023 + // Last business day of September in 2023 inputDate: new Date(2023, 8), expectedResult: 29, }, ]; - test.each(scenarios)('returns a last business day of an input date', ({inputDate, expectedResult}) => { + test.each(scenarios)('returns a last business day based on the input date', ({inputDate, expectedResult}) => { const lastBusinessDay = DateUtils.getLastBusinessDayOfMonth(inputDate); expect(lastBusinessDay).toEqual(expectedResult); From 86bbc8ae39c18c59ba62dcd45da59518254a3e3e Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Tue, 23 Jan 2024 11:14:51 +0100 Subject: [PATCH 300/391] remove only --- tests/unit/DateUtilsTest.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/DateUtilsTest.js b/tests/unit/DateUtilsTest.js index be38eae9251d..a752eea1a990 100644 --- a/tests/unit/DateUtilsTest.js +++ b/tests/unit/DateUtilsTest.js @@ -214,7 +214,7 @@ describe('DateUtils', () => { }); }); - describe.only('getLastBusinessDayOfMonth', () => { + describe('getLastBusinessDayOfMonth', () => { const scenarios = [ { // Last business day of May in 2025 From b502e6456673606fd51a8ec0f8838adcc196e448 Mon Sep 17 00:00:00 2001 From: Rocio Perez-Cano Date: Tue, 23 Jan 2024 12:50:04 +0100 Subject: [PATCH 301/391] Update src/languages/es.ts Co-authored-by: Ionatan Wiznia --- src/languages/es.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/languages/es.ts b/src/languages/es.ts index 2c4070ad30ad..6970b905ff5e 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -2595,7 +2595,7 @@ export default { } if (!isTransactionOlderThan7Days) { return isAdmin - ? `Píde a ${member} que marque la transacción como efectivo o espera 7 días e inténtalo de nuevo` + ? `Pide a ${member} que marque la transacción como efectivo o espera 7 días e inténtalo de nuevo` : 'Esperando a adjuntar automáticamente la transacción de tarjeta de crédito'; } return ''; From 75287d8ec968656256cb47808b161084e27ac1bd Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Tue, 23 Jan 2024 13:39:20 +0100 Subject: [PATCH 302/391] fix: use || instead of ?? --- src/libs/OptionsListUtils.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 25c3b12f305e..ee56760f77cc 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -1801,9 +1801,12 @@ function formatMemberForList(member: ReportUtils.OptionData, config: ReportUtils const accountID = member.accountID; return { - text: member.text ?? member.displayName ?? '', - alternateText: member.alternateText ?? member.login ?? '', - keyForList: member.keyForList ?? String(accountID ?? 0) ?? '', + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + text: member.text || member.displayName || '', + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + alternateText: member.alternateText || member.login || '', + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + keyForList: member.keyForList || String(accountID ?? 0) || '', isSelected: false, isDisabled: false, accountID, From c3ca6cceddd7302a0eaf820b986792a93a015dad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Tue, 23 Jan 2024 14:34:44 +0100 Subject: [PATCH 303/391] provide github env --- .../composite/buildAndroidE2EAPK/action.yml | 21 +++++++++++++++++++ .github/workflows/e2ePerformanceTests.yml | 10 +++++++++ 2 files changed, 31 insertions(+) diff --git a/.github/actions/composite/buildAndroidE2EAPK/action.yml b/.github/actions/composite/buildAndroidE2EAPK/action.yml index b4fc05c7ebe9..217c5acdd506 100644 --- a/.github/actions/composite/buildAndroidE2EAPK/action.yml +++ b/.github/actions/composite/buildAndroidE2EAPK/action.yml @@ -14,6 +14,21 @@ inputs: MAPBOX_SDK_DOWNLOAD_TOKEN: description: The token to use to download the MapBox SDK required: true + EXPENSIFY_PARTNER_NAME: + description: The name of the Expensify partner to use for the build + required: true + EXPENSIFY_PARTNER_PASSWORD: + description: The password of the Expensify partner to use for the build + required: true + EXPENSIFY_PARTNER_USER_ID: + description: The user ID of the Expensify partner to use for the build + required: true + EXPENSIFY_PARTNER_USER_SECRET: + description: The user secret of the Expensify partner to use for the build + required: true + EXPENSIFY_PARTNER_PASSWORD_EMAIL: + description: The email address of the Expensify partner to use for the build + required: true runs: using: composite @@ -40,6 +55,12 @@ runs: - name: Build APK run: npm run ${{ inputs.PACKAGE_SCRIPT_NAME }} shell: bash + env: + EXPENSIFY_PARTNER_NAME: ${{ inputs.EXPENSIFY_PARTNER_NAME }} + EXPENSIFY_PARTNER_PASSWORD: ${{ inputs.EXPENSIFY_PARTNER_PASSWORD }} + EXPENSIFY_PARTNER_USER_ID: ${{ inputs.EXPENSIFY_PARTNER_USER_ID }} + EXPENSIFY_PARTNER_USER_SECRET: ${{ inputs.EXPENSIFY_PARTNER_USER_SECRET }} + EXPENSIFY_PARTNER_PASSWORD_EMAIL: ${{ inputs.EXPENSIFY_PARTNER_PASSWORD_EMAIL }} - name: Upload APK uses: actions/upload-artifact@65d862660abb392b8c4a3d1195a2108db131dd05 diff --git a/.github/workflows/e2ePerformanceTests.yml b/.github/workflows/e2ePerformanceTests.yml index bd3af08ae25e..1f28822a4a39 100644 --- a/.github/workflows/e2ePerformanceTests.yml +++ b/.github/workflows/e2ePerformanceTests.yml @@ -52,6 +52,11 @@ jobs: PACKAGE_SCRIPT_NAME: android-build-e2e APP_OUTPUT_PATH: android/app/build/outputs/apk/e2e/release/app-e2e-release.apk MAPBOX_SDK_DOWNLOAD_TOKEN: ${{ secrets.MAPBOX_SDK_DOWNLOAD_TOKEN }} + EXPENSIFY_PARTNER_NAME: ${{ secrets.EXPENSIFY_PARTNER_NAME }} + EXPENSIFY_PARTNER_PASSWORD: ${{ secrets.EXPENSIFY_PARTNER_PASSWORD }} + EXPENSIFY_PARTNER_USER_ID: ${{ secrets.EXPENSIFY_PARTNER_USER_ID }} + EXPENSIFY_PARTNER_USER_SECRET: ${{ secrets.EXPENSIFY_PARTNER_USER_SECRET }} + EXPENSIFY_PARTNER_PASSWORD_EMAIL: ${{ secrets.EXPENSIFY_PARTNER_PASSWORD_EMAIL }} buildDelta: runs-on: ubuntu-latest-xl @@ -114,6 +119,11 @@ jobs: PACKAGE_SCRIPT_NAME: android-build-e2edelta APP_OUTPUT_PATH: android/app/build/outputs/apk/e2edelta/release/app-e2edelta-release.apk MAPBOX_SDK_DOWNLOAD_TOKEN: ${{ secrets.MAPBOX_SDK_DOWNLOAD_TOKEN }} + EXPENSIFY_PARTNER_NAME: ${{ secrets.EXPENSIFY_PARTNER_NAME }} + EXPENSIFY_PARTNER_PASSWORD: ${{ secrets.EXPENSIFY_PARTNER_PASSWORD }} + EXPENSIFY_PARTNER_USER_ID: ${{ secrets.EXPENSIFY_PARTNER_USER_ID }} + EXPENSIFY_PARTNER_USER_SECRET: ${{ secrets.EXPENSIFY_PARTNER_USER_SECRET }} + EXPENSIFY_PARTNER_PASSWORD_EMAIL: ${{ secrets.EXPENSIFY_PARTNER_PASSWORD_EMAIL }} runTestsInAWS: runs-on: ubuntu-latest From 384899462aa897037390da596e559b72857e557f Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Tue, 23 Jan 2024 15:08:45 +0100 Subject: [PATCH 304/391] fix: bug that text was not displayed for room member invite , resolve comments --- src/libs/OptionsListUtils.ts | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index ee56760f77cc..1346b8e00908 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -310,7 +310,7 @@ function getPersonalDetailsForAccountIDs(accountIDs: number[] | undefined, perso */ function isPersonalDetailsReady(personalDetails: OnyxEntry): boolean { const personalDetailsKeys = Object.keys(personalDetails ?? {}); - return personalDetailsKeys.some((key) => personalDetails?.[Number(key)]?.accountID); + return personalDetailsKeys.some((key) => personalDetails?.[key]?.accountID); } /** @@ -318,7 +318,8 @@ function isPersonalDetailsReady(personalDetails: OnyxEntry) */ function getParticipantsOption(participant: ReportUtils.OptionData, personalDetails: OnyxEntry): Participant { const detail = getPersonalDetailsForAccountIDs([participant.accountID ?? -1], personalDetails)[participant.accountID ?? -1]; - const login = detail?.login ?? participant.login ?? ''; + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + const login = detail?.login || participant.login || ''; const displayName = PersonalDetailsUtils.getDisplayNameOrDefault(detail, LocalePhoneNumber.formatPhoneNumber(login)); return { keyForList: String(detail?.accountID), @@ -622,13 +623,14 @@ function createOption( const lastActorDetails = personalDetailMap[report.lastActorAccountID ?? 0] ?? null; const lastActorDisplayName = hasMultipleParticipants && lastActorDetails && lastActorDetails.accountID !== currentUserAccountID - ? lastActorDetails.firstName ?? PersonalDetailsUtils.getDisplayNameOrDefault(lastActorDetails) + ? // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + lastActorDetails.firstName || PersonalDetailsUtils.getDisplayNameOrDefault(lastActorDetails) : ''; let lastMessageText = lastMessageTextFromReport; const lastReportAction = lastReportActions[report.reportID ?? '']; if (result.isArchivedRoom && lastReportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.CLOSED) { - const archiveReason = lastReportAction.originalMessage?.reason ?? CONST.REPORT.ARCHIVE_REASON.DEFAULT; + const archiveReason = lastReportAction.originalMessage?.reason || CONST.REPORT.ARCHIVE_REASON.DEFAULT; if (archiveReason === CONST.REPORT.ARCHIVE_REASON.DEFAULT || archiveReason === CONST.REPORT.ARCHIVE_REASON.ACCOUNT_MERGED) { lastMessageText = Localize.translate(preferredLocale, 'reportArchiveReasons.default'); } else { @@ -657,7 +659,8 @@ function createOption( } reportName = ReportUtils.getReportName(report); } else { - reportName = ReportUtils.getDisplayNameForParticipant(accountIDs[0]) ?? LocalePhoneNumber.formatPhoneNumber(personalDetail.login ?? ''); + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + reportName = ReportUtils.getDisplayNameForParticipant(accountIDs[0]) || LocalePhoneNumber.formatPhoneNumber(personalDetail.login ?? ''); result.keyForList = String(accountIDs[0]); result.alternateText = LocalePhoneNumber.formatPhoneNumber(personalDetails?.[accountIDs[0]]?.login ?? ''); @@ -1520,7 +1523,11 @@ 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 && optionsToExclude.some((option) => 'login' in option && option.login === reportOption.login)) { + if ( + !includeThreads && + (!!reportOption.login || reportOption.reportID) && + optionsToExclude.some((option) => option.login === reportOption.login || option.reportID === reportOption.reportID) + ) { continue; } @@ -1604,8 +1611,10 @@ function getOptions( }); userToInvite.isOptimisticAccount = true; userToInvite.login = searchValue; - userToInvite.text = userToInvite.text ?? searchValue; - userToInvite.alternateText = userToInvite.alternateText ?? searchValue; + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + userToInvite.text = userToInvite.text || searchValue; + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + userToInvite.alternateText = userToInvite.alternateText || searchValue; // If user doesn't exist, use a default avatar userToInvite.icons = [ From c682722c94469a99af196cec2a4cf33cdad633ce Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Wed, 17 Jan 2024 12:22:35 +0100 Subject: [PATCH 305/391] add metrics for SearchPage --- src/CONST.ts | 2 ++ src/components/OptionsList/BaseOptionsList.tsx | 10 ++++++++++ src/libs/OptionsListUtils.js | 7 ++++++- src/pages/home/sidebar/SidebarLinks.js | 4 ++++ 4 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/CONST.ts b/src/CONST.ts index bc56e345de7c..552c94ceaf6c 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -716,6 +716,8 @@ const CONST = { REPORT_INITIAL_RENDER: 'report_initial_render', SWITCH_REPORT: 'switch_report', SIDEBAR_LOADED: 'sidebar_loaded', + OPEN_SEARCH: 'open_search', + LOAD_SEARCH_OPTIONS: 'load_search_options', COLD: 'cold', WARM: 'warm', REPORT_ACTION_ITEM_LAYOUT_DEBOUNCE_TIME: 1500, diff --git a/src/components/OptionsList/BaseOptionsList.tsx b/src/components/OptionsList/BaseOptionsList.tsx index c1e4562a0c2d..d056b858b3e8 100644 --- a/src/components/OptionsList/BaseOptionsList.tsx +++ b/src/components/OptionsList/BaseOptionsList.tsx @@ -9,6 +9,7 @@ import SectionList from '@components/SectionList'; import Text from '@components/Text'; import usePrevious from '@hooks/usePrevious'; import useThemeStyles from '@hooks/useThemeStyles'; +import Performance from '@libs/Performance'; import type {OptionData} from '@libs/ReportUtils'; import StringUtils from '@libs/StringUtils'; import variables from '@styles/variables'; @@ -108,6 +109,15 @@ function BaseOptionsList( flattenedData.current = buildFlatSectionArray(); }); + useEffect(() => { + if (isLoading) { + return; + } + + // Mark the end of the search page load time. This data is collected only for Search page. + Performance.markEnd(CONST.TIMING.OPEN_SEARCH); + }, [isLoading]); + const onViewableItemsChanged = () => { if (didLayout.current || !onLayout) { return; diff --git a/src/libs/OptionsListUtils.js b/src/libs/OptionsListUtils.js index 2973228af51f..b0a13fda9df6 100644 --- a/src/libs/OptionsListUtils.js +++ b/src/libs/OptionsListUtils.js @@ -23,6 +23,7 @@ import * as ReportUtils from './ReportUtils'; import * as TaskUtils from './TaskUtils'; import * as TransactionUtils from './TransactionUtils'; import * as UserUtils from './UserUtils'; +import Performance from './Performance'; /** * OptionsListUtils is used to build a list options passed to the OptionsList component. Several different UI views can @@ -1651,7 +1652,8 @@ function getOptions( * @returns {Object} */ function getSearchOptions(reports, personalDetails, searchValue = '', betas) { - return getOptions(reports, personalDetails, { + Performance.markStart(CONST.TIMING.LOAD_SEARCH_OPTIONS); + const options = getOptions(reports, personalDetails, { betas, searchInputValue: searchValue.trim(), includeRecentReports: true, @@ -1666,6 +1668,9 @@ function getSearchOptions(reports, personalDetails, searchValue = '', betas) { includeMoneyRequests: true, includeTasks: true, }); + Performance.markEnd(CONST.TIMING.LOAD_SEARCH_OPTIONS); + + return options; } /** diff --git a/src/pages/home/sidebar/SidebarLinks.js b/src/pages/home/sidebar/SidebarLinks.js index ffcba2048d18..d2a80e713f5a 100644 --- a/src/pages/home/sidebar/SidebarLinks.js +++ b/src/pages/home/sidebar/SidebarLinks.js @@ -29,6 +29,7 @@ import * as Session from '@userActions/Session'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import Performance from '@libs/Performance'; import SignInOrAvatarWithOptionalStatus from './SignInOrAvatarWithOptionalStatus'; const basePropTypes = { @@ -123,6 +124,9 @@ function SidebarLinks({onLinkClick, insets, optionListItems, isLoading, priority return; } + // Capture metric for opening the search page + Performance.markStart(CONST.TIMING.OPEN_SEARCH) + Navigation.navigate(ROUTES.SEARCH); }, [isCreateMenuOpen]); From 9f56657e8d15c36ae96b71094f339825437dce2c Mon Sep 17 00:00:00 2001 From: Fitsum Abebe Date: Tue, 23 Jan 2024 18:09:32 +0300 Subject: [PATCH 306/391] fix condition to consider usage on different device --- src/pages/home/report/ReportActionsList.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/pages/home/report/ReportActionsList.js b/src/pages/home/report/ReportActionsList.js index 5803e97aaf63..ce8dcb10ef5f 100644 --- a/src/pages/home/report/ReportActionsList.js +++ b/src/pages/home/report/ReportActionsList.js @@ -417,7 +417,9 @@ function ReportActionsList({ userInactiveSince.current = DateUtils.getDBTime(); return; } - + // In case the user read new messages (after being inactive) with other device we should + // show marker based on report.lastReadTime + const newMessageTimeReference = userInactiveSince.current > report.lastReadTime ? userActiveSince.current : report.lastReadTime; if ( scrollingVerticalOffset.current >= MSG_VISIBLE_THRESHOLD || !( @@ -425,8 +427,8 @@ function ReportActionsList({ _.some( sortedVisibleReportActions, (reportAction) => - userInactiveSince.current < reportAction.created && - (ReportActionsUtils.isReportPreviewAction(reportAction) ? !reportAction.childLastActorAccountID : reportAction.actorAccountID) !== Report.getCurrentUserAccountID(), + newMessageTimeReference < reportAction.created && + (ReportActionsUtils.isReportPreviewAction(reportAction) ? reportAction.childLastActorAccountID : reportAction.actorAccountID) !== Report.getCurrentUserAccountID(), ) ) ) { @@ -435,7 +437,7 @@ function ReportActionsList({ Report.readNewestAction(report.reportID, false); userActiveSince.current = DateUtils.getDBTime(); - lastReadTimeRef.current = userInactiveSince.current; + lastReadTimeRef.current = newMessageTimeReference; setCurrentUnreadMarker(null); cacheUnreadMarkers.delete(report.reportID); calculateUnreadMarker(); From ed557d57d7bf9f27b04c388614f52741d1192e0e Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Wed, 17 Jan 2024 12:22:36 +0100 Subject: [PATCH 307/391] lint files --- src/libs/OptionsListUtils.js | 2 +- src/pages/home/sidebar/SidebarLinks.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/libs/OptionsListUtils.js b/src/libs/OptionsListUtils.js index b0a13fda9df6..5b494cb09254 100644 --- a/src/libs/OptionsListUtils.js +++ b/src/libs/OptionsListUtils.js @@ -14,6 +14,7 @@ import * as Localize from './Localize'; import * as LoginUtils from './LoginUtils'; import ModifiedExpenseMessage from './ModifiedExpenseMessage'; import Navigation from './Navigation/Navigation'; +import Performance from './Performance'; import Permissions from './Permissions'; import * as PersonalDetailsUtils from './PersonalDetailsUtils'; import * as PhoneNumber from './PhoneNumber'; @@ -23,7 +24,6 @@ import * as ReportUtils from './ReportUtils'; import * as TaskUtils from './TaskUtils'; import * as TransactionUtils from './TransactionUtils'; import * as UserUtils from './UserUtils'; -import Performance from './Performance'; /** * OptionsListUtils is used to build a list options passed to the OptionsList component. Several different UI views can diff --git a/src/pages/home/sidebar/SidebarLinks.js b/src/pages/home/sidebar/SidebarLinks.js index d2a80e713f5a..08791f4e16fd 100644 --- a/src/pages/home/sidebar/SidebarLinks.js +++ b/src/pages/home/sidebar/SidebarLinks.js @@ -20,6 +20,7 @@ import useWindowDimensions from '@hooks/useWindowDimensions'; import KeyboardShortcut from '@libs/KeyboardShortcut'; import Navigation from '@libs/Navigation/Navigation'; import onyxSubscribe from '@libs/onyxSubscribe'; +import Performance from '@libs/Performance'; import SidebarUtils from '@libs/SidebarUtils'; import * as ReportActionContextMenu from '@pages/home/report/ContextMenu/ReportActionContextMenu'; import safeAreaInsetPropTypes from '@pages/safeAreaInsetPropTypes'; @@ -29,7 +30,6 @@ import * as Session from '@userActions/Session'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import Performance from '@libs/Performance'; import SignInOrAvatarWithOptionalStatus from './SignInOrAvatarWithOptionalStatus'; const basePropTypes = { @@ -125,7 +125,7 @@ function SidebarLinks({onLinkClick, insets, optionListItems, isLoading, priority } // Capture metric for opening the search page - Performance.markStart(CONST.TIMING.OPEN_SEARCH) + Performance.markStart(CONST.TIMING.OPEN_SEARCH); Navigation.navigate(ROUTES.SEARCH); }, [isCreateMenuOpen]); From e4c678f36fb92fa281a093d28abddb6f637c3479 Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Wed, 17 Jan 2024 13:40:27 +0100 Subject: [PATCH 308/391] use Timing to track metric --- src/components/OptionsList/BaseOptionsList.tsx | 2 ++ src/libs/OptionsListUtils.js | 3 +++ src/pages/home/sidebar/SidebarLinks.js | 2 ++ 3 files changed, 7 insertions(+) diff --git a/src/components/OptionsList/BaseOptionsList.tsx b/src/components/OptionsList/BaseOptionsList.tsx index d056b858b3e8..8cac059436b5 100644 --- a/src/components/OptionsList/BaseOptionsList.tsx +++ b/src/components/OptionsList/BaseOptionsList.tsx @@ -9,6 +9,7 @@ import SectionList from '@components/SectionList'; import Text from '@components/Text'; import usePrevious from '@hooks/usePrevious'; import useThemeStyles from '@hooks/useThemeStyles'; +import Timing from '@libs/actions/Timing'; import Performance from '@libs/Performance'; import type {OptionData} from '@libs/ReportUtils'; import StringUtils from '@libs/StringUtils'; @@ -115,6 +116,7 @@ function BaseOptionsList( } // Mark the end of the search page load time. This data is collected only for Search page. + Timing.end(CONST.TIMING.OPEN_SEARCH); Performance.markEnd(CONST.TIMING.OPEN_SEARCH); }, [isLoading]); diff --git a/src/libs/OptionsListUtils.js b/src/libs/OptionsListUtils.js index 5b494cb09254..15e2e5ca269c 100644 --- a/src/libs/OptionsListUtils.js +++ b/src/libs/OptionsListUtils.js @@ -24,6 +24,7 @@ import * as ReportUtils from './ReportUtils'; import * as TaskUtils from './TaskUtils'; import * as TransactionUtils from './TransactionUtils'; import * as UserUtils from './UserUtils'; +import Timing from './actions/Timing'; /** * OptionsListUtils is used to build a list options passed to the OptionsList component. Several different UI views can @@ -1652,6 +1653,7 @@ function getOptions( * @returns {Object} */ function getSearchOptions(reports, personalDetails, searchValue = '', betas) { + Timing.start(CONST.TIMING.LOAD_SEARCH_OPTIONS); Performance.markStart(CONST.TIMING.LOAD_SEARCH_OPTIONS); const options = getOptions(reports, personalDetails, { betas, @@ -1668,6 +1670,7 @@ function getSearchOptions(reports, personalDetails, searchValue = '', betas) { includeMoneyRequests: true, includeTasks: true, }); + Timing.end(CONST.TIMING.LOAD_SEARCH_OPTIONS); Performance.markEnd(CONST.TIMING.LOAD_SEARCH_OPTIONS); return options; diff --git a/src/pages/home/sidebar/SidebarLinks.js b/src/pages/home/sidebar/SidebarLinks.js index 08791f4e16fd..3c7aa4352911 100644 --- a/src/pages/home/sidebar/SidebarLinks.js +++ b/src/pages/home/sidebar/SidebarLinks.js @@ -30,6 +30,7 @@ import * as Session from '@userActions/Session'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import Timing from '@libs/actions/Timing'; import SignInOrAvatarWithOptionalStatus from './SignInOrAvatarWithOptionalStatus'; const basePropTypes = { @@ -125,6 +126,7 @@ function SidebarLinks({onLinkClick, insets, optionListItems, isLoading, priority } // Capture metric for opening the search page + Timing.start(CONST.TIMING.OPEN_SEARCH); Performance.markStart(CONST.TIMING.OPEN_SEARCH); Navigation.navigate(ROUTES.SEARCH); From ff3da8489fff129872ffc3603d0f82fcb060121a Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Wed, 17 Jan 2024 13:50:50 +0100 Subject: [PATCH 309/391] lint files --- src/libs/OptionsListUtils.js | 2 +- src/pages/home/sidebar/SidebarLinks.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libs/OptionsListUtils.js b/src/libs/OptionsListUtils.js index 15e2e5ca269c..d44df3c6c39c 100644 --- a/src/libs/OptionsListUtils.js +++ b/src/libs/OptionsListUtils.js @@ -7,6 +7,7 @@ import Onyx from 'react-native-onyx'; import _ from 'underscore'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import Timing from './actions/Timing'; import * as CollectionUtils from './CollectionUtils'; import * as ErrorUtils from './ErrorUtils'; import * as LocalePhoneNumber from './LocalePhoneNumber'; @@ -24,7 +25,6 @@ import * as ReportUtils from './ReportUtils'; import * as TaskUtils from './TaskUtils'; import * as TransactionUtils from './TransactionUtils'; import * as UserUtils from './UserUtils'; -import Timing from './actions/Timing'; /** * OptionsListUtils is used to build a list options passed to the OptionsList component. Several different UI views can diff --git a/src/pages/home/sidebar/SidebarLinks.js b/src/pages/home/sidebar/SidebarLinks.js index 3c7aa4352911..09362d88555c 100644 --- a/src/pages/home/sidebar/SidebarLinks.js +++ b/src/pages/home/sidebar/SidebarLinks.js @@ -17,6 +17,7 @@ import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; +import Timing from '@libs/actions/Timing'; import KeyboardShortcut from '@libs/KeyboardShortcut'; import Navigation from '@libs/Navigation/Navigation'; import onyxSubscribe from '@libs/onyxSubscribe'; @@ -30,7 +31,6 @@ import * as Session from '@userActions/Session'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import Timing from '@libs/actions/Timing'; import SignInOrAvatarWithOptionalStatus from './SignInOrAvatarWithOptionalStatus'; const basePropTypes = { From 706336a7a4d1396f44161b54822ba78b32c617f6 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Tue, 23 Jan 2024 17:24:21 +0100 Subject: [PATCH 310/391] update Podfile.lock --- ios/Podfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 776dcb544ee6..4cdf61554a6b 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1967,7 +1967,7 @@ SPEC CHECKSUMS: SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17 Turf: 13d1a92d969ca0311bbc26e8356cca178ce95da2 VisionCamera: 7d13aae043ffb38b224a0f725d1e23ca9c190fe7 - Yoga: e64aa65de36c0832d04e8c7bd614396c77a80047 + Yoga: 13c8ef87792450193e117976337b8527b49e8c03 PODFILE CHECKSUM: 0ccbb4f2406893c6e9f266dc1e7470dcd72885d2 From 4971adf81f2236b33ac4f05567a0c8dde9cc65a3 Mon Sep 17 00:00:00 2001 From: someone-here Date: Tue, 23 Jan 2024 21:58:49 +0530 Subject: [PATCH 311/391] Prettify --- src/components/AvatarCropModal/Slider.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/AvatarCropModal/Slider.tsx b/src/components/AvatarCropModal/Slider.tsx index f69fba776718..9a9da65befa0 100644 --- a/src/components/AvatarCropModal/Slider.tsx +++ b/src/components/AvatarCropModal/Slider.tsx @@ -10,7 +10,6 @@ import useThemeStyles from '@hooks/useThemeStyles'; import ControlSelection from '@libs/ControlSelection'; type SliderProps = { - /** React-native-reanimated lib handler which executes when the user is panning slider */ gestureCallbacks: { onBegin: () => void; From 65202dfca711dcb426a28ada45f8c52a14a88369 Mon Sep 17 00:00:00 2001 From: Rocio Perez-Cano Date: Tue, 23 Jan 2024 18:38:19 +0100 Subject: [PATCH 312/391] Use Localize --- .../EnablePayments/TermsPage/LongTermsForm.js | 152 +++++++++--------- .../TermsPage/ShortTermsForm.js | 58 +++---- 2 files changed, 107 insertions(+), 103 deletions(-) diff --git a/src/pages/EnablePayments/TermsPage/LongTermsForm.js b/src/pages/EnablePayments/TermsPage/LongTermsForm.js index b29cb0c777f7..4147d38a98c0 100644 --- a/src/pages/EnablePayments/TermsPage/LongTermsForm.js +++ b/src/pages/EnablePayments/TermsPage/LongTermsForm.js @@ -8,95 +8,97 @@ import Text from '@components/Text'; import TextLink from '@components/TextLink'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; -import * as Localize from '@libs/Localize'; +import useLocalize from "@hooks/useLocalize"; import CONST from '@src/CONST'; -const termsData = [ - { - title: Localize.translateLocal('termsStep.longTermsForm.openingAccountTitle'), - rightText: Localize.translateLocal('termsStep.feeAmountZero'), - details: Localize.translateLocal('termsStep.longTermsForm.openingAccountDetails'), - }, - { - title: Localize.translateLocal('termsStep.monthlyFee'), - rightText: Localize.translateLocal('termsStep.feeAmountZero'), - details: Localize.translateLocal('termsStep.longTermsForm.monthlyFeeDetails'), - }, - { - title: Localize.translateLocal('termsStep.longTermsForm.customerServiceTitle'), - subTitle: Localize.translateLocal('termsStep.longTermsForm.automated'), - rightText: Localize.translateLocal('termsStep.feeAmountZero'), - details: Localize.translateLocal('termsStep.longTermsForm.customerServiceDetails'), - }, - { - title: Localize.translateLocal('termsStep.longTermsForm.customerServiceTitle'), - subTitle: Localize.translateLocal('termsStep.longTermsForm.liveAgent'), - rightText: Localize.translateLocal('termsStep.feeAmountZero'), - details: Localize.translateLocal('termsStep.longTermsForm.customerServiceDetails'), - }, - { - title: Localize.translateLocal('termsStep.inactivity'), - rightText: Localize.translateLocal('termsStep.feeAmountZero'), - details: Localize.translateLocal('termsStep.longTermsForm.inactivityDetails'), - }, - { - title: Localize.translateLocal('termsStep.longTermsForm.sendingFundsTitle'), - rightText: Localize.translateLocal('termsStep.feeAmountZero'), - details: Localize.translateLocal('termsStep.longTermsForm.sendingFundsDetails'), - }, - { - title: Localize.translateLocal('termsStep.electronicFundsWithdrawal'), - subTitle: Localize.translateLocal('termsStep.standard'), - rightText: Localize.translateLocal('termsStep.feeAmountZero'), - details: Localize.translateLocal('termsStep.longTermsForm.electronicFundsStandardDetails'), - }, - { - title: Localize.translateLocal('termsStep.electronicFundsWithdrawal'), - subTitle: Localize.translateLocal('termsStep.longTermsForm.instant'), - rightText: Localize.translateLocal('termsStep.electronicFundsInstantFee'), - subRightText: Localize.translateLocal('termsStep.longTermsForm.electronicFundsInstantFeeMin'), - details: Localize.translateLocal('termsStep.longTermsForm.electronicFundsInstantDetails'), - }, -]; +function LongTermsForm() { + const theme = useTheme(); + const styles = useThemeStyles(); + const {translate} = useLocalize(); -const getLongTermsSections = (styles) => - _.map(termsData, (section, index) => ( - // eslint-disable-next-line react/no-array-index-key - - - - {section.title} - {Boolean(section.subTitle) && {section.subTitle}} - - - {section.rightText} - {Boolean(section.subRightText) && {section.subRightText}} + const termsData = [ + { + title: translate('termsStep.longTermsForm.openingAccountTitle'), + rightText: translate('termsStep.feeAmountZero'), + details: translate('termsStep.longTermsForm.openingAccountDetails'), + }, + { + title: translate('termsStep.monthlyFee'), + rightText: translate('termsStep.feeAmountZero'), + details: translate('termsStep.longTermsForm.monthlyFeeDetails'), + }, + { + title: translate('termsStep.longTermsForm.customerServiceTitle'), + subTitle: translate('termsStep.longTermsForm.automated'), + rightText: translate('termsStep.feeAmountZero'), + details: translate('termsStep.longTermsForm.customerServiceDetails'), + }, + { + title: translate('termsStep.longTermsForm.customerServiceTitle'), + subTitle: translate('termsStep.longTermsForm.liveAgent'), + rightText: translate('termsStep.feeAmountZero'), + details: translate('termsStep.longTermsForm.customerServiceDetails'), + }, + { + title: translate('termsStep.inactivity'), + rightText: translate('termsStep.feeAmountZero'), + details: translate('termsStep.longTermsForm.inactivityDetails'), + }, + { + title: translate('termsStep.longTermsForm.sendingFundsTitle'), + rightText: translate('termsStep.feeAmountZero'), + details: translate('termsStep.longTermsForm.sendingFundsDetails'), + }, + { + title: translate('termsStep.electronicFundsWithdrawal'), + subTitle: translate('termsStep.standard'), + rightText: translate('termsStep.feeAmountZero'), + details: translate('termsStep.longTermsForm.electronicFundsStandardDetails'), + }, + { + title: translate('termsStep.electronicFundsWithdrawal'), + subTitle: translate('termsStep.longTermsForm.instant'), + rightText: translate('termsStep.electronicFundsInstantFee'), + subRightText: translate('termsStep.longTermsForm.electronicFundsInstantFeeMin'), + details: translate('termsStep.longTermsForm.electronicFundsInstantDetails'), + }, + ]; + + const getLongTermsSections = () => + _.map(termsData, (section, index) => ( + // eslint-disable-next-line react/no-array-index-key + + + + {section.title} + {Boolean(section.subTitle) && {section.subTitle}} + + + {section.rightText} + {Boolean(section.subRightText) && {section.subRightText}} + + {section.details} - {section.details} - - )); + )); -function LongTermsForm() { - const theme = useTheme(); - const styles = useThemeStyles(); return ( <> - {getLongTermsSections(styles)} + {getLongTermsSections()} - {Localize.translateLocal('termsStep.longTermsForm.fdicInsuranceBancorp')} {CONST.TERMS.FDIC_PREPAID}{' '} - {Localize.translateLocal('termsStep.longTermsForm.fdicInsuranceBancorp2')} + {translate('termsStep.longTermsForm.fdicInsuranceBancorp')} {CONST.TERMS.FDIC_PREPAID}{' '} + {translate('termsStep.longTermsForm.fdicInsuranceBancorp2')} - {Localize.translateLocal('termsStep.noOverdraftOrCredit')} + {translate('termsStep.noOverdraftOrCredit')} - {Localize.translateLocal('termsStep.longTermsForm.contactExpensifyPayments')} {CONST.EMAIL.CONCIERGE}{' '} - {Localize.translateLocal('termsStep.longTermsForm.contactExpensifyPayments2')} {CONST.NEW_EXPENSIFY_URL}. + {translate('termsStep.longTermsForm.contactExpensifyPayments')} {CONST.EMAIL.CONCIERGE}{' '} + {translate('termsStep.longTermsForm.contactExpensifyPayments2')} {CONST.NEW_EXPENSIFY_URL}. - {Localize.translateLocal('termsStep.longTermsForm.generalInformation')} {CONST.TERMS.CFPB_PREPAID} + {translate('termsStep.longTermsForm.generalInformation')} {CONST.TERMS.CFPB_PREPAID} {'. '} - {Localize.translateLocal('termsStep.longTermsForm.generalInformation2')} {CONST.TERMS.CFPB_COMPLAINT}. + {translate('termsStep.longTermsForm.generalInformation2')} {CONST.TERMS.CFPB_COMPLAINT}. @@ -109,7 +111,7 @@ function LongTermsForm() { style={styles.ml1} href={CONST.FEES_URL} > - {Localize.translateLocal('termsStep.longTermsForm.printerFriendlyView')} + {translate('termsStep.longTermsForm.printerFriendlyView')} diff --git a/src/pages/EnablePayments/TermsPage/ShortTermsForm.js b/src/pages/EnablePayments/TermsPage/ShortTermsForm.js index 77f77f3cb34b..c42b3b23ec2f 100644 --- a/src/pages/EnablePayments/TermsPage/ShortTermsForm.js +++ b/src/pages/EnablePayments/TermsPage/ShortTermsForm.js @@ -3,9 +3,10 @@ import {View} from 'react-native'; import Text from '@components/Text'; import TextLink from '@components/TextLink'; import useThemeStyles from '@hooks/useThemeStyles'; -import * as Localize from '@libs/Localize'; import userWalletPropTypes from '@pages/EnablePayments/userWalletPropTypes'; import CONST from '@src/CONST'; +import useLocalize from "@hooks/useLocalize"; +import * as CurrencyUtils from "@libs/CurrencyUtils"; const propTypes = { /** The user's wallet */ @@ -18,10 +19,11 @@ const defaultProps = { function ShortTermsForm(props) { const styles = useThemeStyles(); + const {translate} = useLocalize(); return ( <> - {Localize.translateLocal('termsStep.shortTermsForm.expensifyPaymentsAccount', { + {translate('termsStep.shortTermsForm.expensifyPaymentsAccount', { walletProgram: props.userWallet.walletProgramID === CONST.WALLET.MTL_WALLET_PROGRAM_ID ? CONST.WALLET.PROGRAM_ISSUERS.EXPENSIFY_PAYMENTS : CONST.WALLET.PROGRAM_ISSUERS.BANCORP_BANK, })} @@ -31,19 +33,19 @@ function ShortTermsForm(props) { - {Localize.translateLocal('termsStep.monthlyFee')} + {translate('termsStep.monthlyFee')} - {Localize.translateLocal('termsStep.feeAmountZero')} + {CurrencyUtils.convertToDisplayString(0, 'USD')} - {Localize.translateLocal('termsStep.shortTermsForm.perPurchase')} + {translate('termsStep.shortTermsForm.perPurchase')} - {Localize.translateLocal('termsStep.feeAmountZero')} + {CurrencyUtils.convertToDisplayString(0, 'USD')} @@ -52,28 +54,28 @@ function ShortTermsForm(props) { - {Localize.translateLocal('termsStep.shortTermsForm.atmWithdrawal')} + {translate('termsStep.shortTermsForm.atmWithdrawal')} - {Localize.translateLocal('common.na')} + {translate('common.na')} - {Localize.translateLocal('termsStep.shortTermsForm.inNetwork')} + {translate('termsStep.shortTermsForm.inNetwork')} - {Localize.translateLocal('common.na')} + {translate('common.na')} - {Localize.translateLocal('termsStep.shortTermsForm.outOfNetwork')} + {translate('termsStep.shortTermsForm.outOfNetwork')} - {Localize.translateLocal('termsStep.shortTermsForm.cashReload')} + {translate('termsStep.shortTermsForm.cashReload')} - {Localize.translateLocal('common.na')} + {translate('common.na')} @@ -83,11 +85,11 @@ function ShortTermsForm(props) { - {Localize.translateLocal('termsStep.shortTermsForm.atmBalanceInquiry')} {Localize.translateLocal('termsStep.shortTermsForm.inOrOutOfNetwork')} + {translate('termsStep.shortTermsForm.atmBalanceInquiry')} {translate('termsStep.shortTermsForm.inOrOutOfNetwork')} - {Localize.translateLocal('common.na')} + {translate('common.na')} @@ -95,11 +97,11 @@ function ShortTermsForm(props) { - {Localize.translateLocal('termsStep.shortTermsForm.customerService')} {Localize.translateLocal('termsStep.shortTermsForm.automatedOrLive')} + {translate('termsStep.shortTermsForm.customerService')} {translate('termsStep.shortTermsForm.automatedOrLive')} - {Localize.translateLocal('termsStep.feeAmountZero')} + {CurrencyUtils.convertToDisplayString(0, 'USD')} @@ -107,40 +109,40 @@ function ShortTermsForm(props) { - {Localize.translateLocal('termsStep.inactivity')} {Localize.translateLocal('termsStep.shortTermsForm.afterTwelveMonths')} + {translate('termsStep.inactivity')} {translate('termsStep.shortTermsForm.afterTwelveMonths')} - {Localize.translateLocal('termsStep.feeAmountZero')} + {CurrencyUtils.convertToDisplayString(0, 'USD')} - {Localize.translateLocal('termsStep.shortTermsForm.weChargeOneFee')} + {translate('termsStep.shortTermsForm.weChargeOneFee')} - {Localize.translateLocal('termsStep.electronicFundsWithdrawal')} {Localize.translateLocal('termsStep.shortTermsForm.instant')} + {translate('termsStep.electronicFundsWithdrawal')} {translate('termsStep.shortTermsForm.instant')} - {Localize.translateLocal('termsStep.electronicFundsInstantFee')} - {Localize.translateLocal('termsStep.shortTermsForm.electronicFundsInstantFeeMin')} + {translate('termsStep.electronicFundsInstantFee')} + {translate('termsStep.shortTermsForm.electronicFundsInstantFeeMin')} - {Localize.translateLocal('termsStep.noOverdraftOrCredit')} - {Localize.translateLocal('termsStep.shortTermsForm.fdicInsurance')} + {translate('termsStep.noOverdraftOrCredit')} + {translate('termsStep.shortTermsForm.fdicInsurance')} - {Localize.translateLocal('termsStep.shortTermsForm.generalInfo')} {CONST.TERMS.CFPB_PREPAID}. + {translate('termsStep.shortTermsForm.generalInfo')} {CONST.TERMS.CFPB_PREPAID}. - {Localize.translateLocal('termsStep.shortTermsForm.conditionsDetails')} {CONST.TERMS.USE_EXPENSIFY_FEES}{' '} - {Localize.translateLocal('termsStep.shortTermsForm.conditionsPhone')} + {translate('termsStep.shortTermsForm.conditionsDetails')} {CONST.TERMS.USE_EXPENSIFY_FEES}{' '} + {translate('termsStep.shortTermsForm.conditionsPhone')} From 155457c07dfec28b0bcf2ef75b8993551561940f Mon Sep 17 00:00:00 2001 From: Rocio Perez-Cano Date: Tue, 23 Jan 2024 19:30:11 +0100 Subject: [PATCH 313/391] More keys --- src/languages/en.ts | 7 +++---- src/languages/es.ts | 7 +++---- src/languages/types.ts | 3 +++ .../EnablePayments/TermsPage/LongTermsForm.js | 21 ++++++++++--------- .../TermsPage/ShortTermsForm.js | 6 +++--- 5 files changed, 23 insertions(+), 21 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index b6da38df21a0..7db0c9be37d1 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -66,6 +66,7 @@ import type { StepCounterParams, TagSelectionParams, TaskCreatedActionParams, + TermsParams, ThreadRequestReportNameParams, ThreadSentMoneyReportNameParams, ToValidateLoginParams, @@ -1357,10 +1358,8 @@ export default { agreeToThe: 'I agree to the', walletAgreement: 'Wallet agreement', enablePayments: 'Enable payments', - feeAmountZero: '$0', monthlyFee: 'Monthly fee', inactivity: 'Inactivity', - electronicFundsInstantFee: '1.5%', noOverdraftOrCredit: 'No overdraft/credit feature.', electronicFundsWithdrawal: 'Electronic funds withdrawal', standard: 'Standard', @@ -1382,7 +1381,7 @@ export default { conditionsDetails: 'Find details and conditions for all fees and services by visiting', conditionsPhone: 'or calling +1 833-400-0904.', instant: '(instant)', - electronicFundsInstantFeeMin: '(min $0.25)', + electronicFundsInstantFeeMin: ({amount}: TermsParams) => `(min ${amount})`, }, longTermsForm: { listOfAllFees: 'A list of all Expensify Wallet fees', @@ -1418,7 +1417,7 @@ export default { automated: 'Automated', liveAgent: 'Live Agent', instant: 'Instant', - electronicFundsInstantFeeMin: 'Min $0.25', + electronicFundsInstantFeeMin: ({amount}: TermsParams) => `Min ${amount}`, }, }, activateStep: { diff --git a/src/languages/es.ts b/src/languages/es.ts index 6970b905ff5e..2d2a0c0165b8 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -66,6 +66,7 @@ import type { StepCounterParams, TagSelectionParams, TaskCreatedActionParams, + TermsParams, ThreadRequestReportNameParams, ThreadSentMoneyReportNameParams, ToValidateLoginParams, @@ -1378,10 +1379,8 @@ export default { agreeToThe: 'Estoy de acuerdo con el ', walletAgreement: 'Acuerdo de la billetera', enablePayments: 'Habilitar pagos', - feeAmountZero: 'US$0', monthlyFee: 'Cuota mensual', inactivity: 'Inactividad', - electronicFundsInstantFee: '1,5%', noOverdraftOrCredit: 'Sin función de sobregiro/crédito', electronicFundsWithdrawal: 'Retiro electrónico de fondos', standard: 'Estándar', @@ -1403,7 +1402,7 @@ export default { conditionsDetails: 'Encuentra detalles y condiciones para todas las tarifas y servicios visitando', conditionsPhone: 'o llamando al +1 833-400-0904.', instant: '(instantáneo)', - electronicFundsInstantFeeMin: '(mínimo US$0,25)', + electronicFundsInstantFeeMin: ({amount}: TermsParams) => `(mínimo ${amount})`, }, longTermsForm: { listOfAllFees: 'Una lista de todas las tarifas de la billetera Expensify', @@ -1440,7 +1439,7 @@ export default { automated: 'Automatizado', liveAgent: 'Agente en vivo', instant: 'Instantáneo', - electronicFundsInstantFeeMin: 'Mínimo US$0,25', + electronicFundsInstantFeeMin: ({amount}: TermsParams) => `Mínimo ${amount}`, }, }, activateStep: { diff --git a/src/languages/types.ts b/src/languages/types.ts index 3185b7a8f6f1..b288ccaeb703 100644 --- a/src/languages/types.ts +++ b/src/languages/types.ts @@ -287,6 +287,8 @@ type TranslationFlatObject = { [TKey in TranslationPaths]: TranslateType; }; +type TermsParams = {amount: string}; + export type { ApprovedAmountParams, AddressLineParams, @@ -353,6 +355,7 @@ export type { StepCounterParams, TagSelectionParams, TaskCreatedActionParams, + TermsParams, ThreadRequestReportNameParams, ThreadSentMoneyReportNameParams, ToValidateLoginParams, diff --git a/src/pages/EnablePayments/TermsPage/LongTermsForm.js b/src/pages/EnablePayments/TermsPage/LongTermsForm.js index 4147d38a98c0..bdbe87f27b4d 100644 --- a/src/pages/EnablePayments/TermsPage/LongTermsForm.js +++ b/src/pages/EnablePayments/TermsPage/LongTermsForm.js @@ -10,56 +10,57 @@ import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import useLocalize from "@hooks/useLocalize"; import CONST from '@src/CONST'; +import * as CurrencyUtils from "@libs/CurrencyUtils"; function LongTermsForm() { const theme = useTheme(); const styles = useThemeStyles(); - const {translate} = useLocalize(); + const {translate, numberFormat} = useLocalize(); const termsData = [ { title: translate('termsStep.longTermsForm.openingAccountTitle'), - rightText: translate('termsStep.feeAmountZero'), + rightText: CurrencyUtils.convertToDisplayString(0, 'USD'), details: translate('termsStep.longTermsForm.openingAccountDetails'), }, { title: translate('termsStep.monthlyFee'), - rightText: translate('termsStep.feeAmountZero'), + rightText: CurrencyUtils.convertToDisplayString(0, 'USD'), details: translate('termsStep.longTermsForm.monthlyFeeDetails'), }, { title: translate('termsStep.longTermsForm.customerServiceTitle'), subTitle: translate('termsStep.longTermsForm.automated'), - rightText: translate('termsStep.feeAmountZero'), + rightText: CurrencyUtils.convertToDisplayString(0, 'USD'), details: translate('termsStep.longTermsForm.customerServiceDetails'), }, { title: translate('termsStep.longTermsForm.customerServiceTitle'), subTitle: translate('termsStep.longTermsForm.liveAgent'), - rightText: translate('termsStep.feeAmountZero'), + rightText: CurrencyUtils.convertToDisplayString(0, 'USD'), details: translate('termsStep.longTermsForm.customerServiceDetails'), }, { title: translate('termsStep.inactivity'), - rightText: translate('termsStep.feeAmountZero'), + rightText: CurrencyUtils.convertToDisplayString(0, 'USD'), details: translate('termsStep.longTermsForm.inactivityDetails'), }, { title: translate('termsStep.longTermsForm.sendingFundsTitle'), - rightText: translate('termsStep.feeAmountZero'), + rightText: CurrencyUtils.convertToDisplayString(0, 'USD'), details: translate('termsStep.longTermsForm.sendingFundsDetails'), }, { title: translate('termsStep.electronicFundsWithdrawal'), subTitle: translate('termsStep.standard'), - rightText: translate('termsStep.feeAmountZero'), + rightText: CurrencyUtils.convertToDisplayString(0, 'USD'), details: translate('termsStep.longTermsForm.electronicFundsStandardDetails'), }, { title: translate('termsStep.electronicFundsWithdrawal'), subTitle: translate('termsStep.longTermsForm.instant'), - rightText: translate('termsStep.electronicFundsInstantFee'), - subRightText: translate('termsStep.longTermsForm.electronicFundsInstantFeeMin'), + rightText: `${numberFormat(1.5)}%`, + subRightText: translate('termsStep.longTermsForm.electronicFundsInstantFeeMin', {amount: CurrencyUtils.convertToDisplayString(25, 'USD')}), details: translate('termsStep.longTermsForm.electronicFundsInstantDetails'), }, ]; diff --git a/src/pages/EnablePayments/TermsPage/ShortTermsForm.js b/src/pages/EnablePayments/TermsPage/ShortTermsForm.js index c42b3b23ec2f..3d96d2209dd5 100644 --- a/src/pages/EnablePayments/TermsPage/ShortTermsForm.js +++ b/src/pages/EnablePayments/TermsPage/ShortTermsForm.js @@ -19,7 +19,7 @@ const defaultProps = { function ShortTermsForm(props) { const styles = useThemeStyles(); - const {translate} = useLocalize(); + const {translate, numberFormat} = useLocalize(); return ( <> @@ -130,8 +130,8 @@ function ShortTermsForm(props) { - {translate('termsStep.electronicFundsInstantFee')} - {translate('termsStep.shortTermsForm.electronicFundsInstantFeeMin')} + {numberFormat(1.5)}% + {translate('termsStep.shortTermsForm.electronicFundsInstantFeeMin', {amount: CurrencyUtils.convertToDisplayString(25, 'USD')})} From c3222d8e7803bda371f4c9810343a1a0cc1f3a6a Mon Sep 17 00:00:00 2001 From: Rocio Perez-Cano Date: Tue, 23 Jan 2024 19:38:04 +0100 Subject: [PATCH 314/391] Another one --- src/languages/en.ts | 7 +++---- src/languages/es.ts | 7 +++---- src/pages/EnablePayments/TermsPage/LongTermsForm.js | 2 +- 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index 7db0c9be37d1..43972bd7a023 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -1404,10 +1404,9 @@ export default { 'There is a fee to transfer funds from your Expensify Wallet to ' + 'your linked debit card using the instant transfer option. This transfer usually completes within ' + 'several minutes. The fee is 1.5% of the transfer amount (with a minimum fee of $0.25).', - fdicInsuranceBancorp: - 'Your funds are eligible for FDIC insurance. Your funds will be held at or ' + - `transferred to ${CONST.WALLET.PROGRAM_ISSUERS.BANCORP_BANK}, an FDIC-insured institution. Once there, your funds are insured up ` + - `to $250,000 by the FDIC in the event ${CONST.WALLET.PROGRAM_ISSUERS.BANCORP_BANK} fails. See`, + fdicInsuranceBancorp: ({amount}: TermsParams) => 'Your funds are eligible for FDIC insurance. Your funds will be held at or ' + + `transferred to ${CONST.WALLET.PROGRAM_ISSUERS.BANCORP_BANK}, an FDIC-insured institution. Once there, your funds are insured up ` + + `to ${amount} by the FDIC in the event ${CONST.WALLET.PROGRAM_ISSUERS.BANCORP_BANK} fails. See`, fdicInsuranceBancorp2: 'for details.', contactExpensifyPayments: `Contact ${CONST.WALLET.PROGRAM_ISSUERS.EXPENSIFY_PAYMENTS} by calling +1 833-400-0904, by email at`, contactExpensifyPayments2: 'or sign in at', diff --git a/src/languages/es.ts b/src/languages/es.ts index 2d2a0c0165b8..cc3b09d491f0 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -1426,10 +1426,9 @@ export default { 'la tarjeta de débito vinculada utilizando la opción de transferencia instantánea. Esta transferencia ' + 'generalmente se completa dentro de varios minutos. La tarifa es el 1,5% del importe de la ' + 'transferencia (con una tarifa mínima de US$0,25). ', - fdicInsuranceBancorp: - 'Tus fondos pueden acogerse al seguro de la FDIC. Tus fondos se mantendrán o serán ' + - `transferidos a ${CONST.WALLET.PROGRAM_ISSUERS.BANCORP_BANK}, una institución asegurada por la FDIC. Una vez allí, tus fondos ` + - `están asegurados hasta US$250.000 por la FDIC en caso de que ${CONST.WALLET.PROGRAM_ISSUERS.BANCORP_BANK} quiebre. Ver`, + fdicInsuranceBancorp: ({amount}: TermsParams) => 'Tus fondos pueden acogerse al seguro de la FDIC. Tus fondos se mantendrán o serán ' + + `transferidos a ${CONST.WALLET.PROGRAM_ISSUERS.BANCORP_BANK}, una institución asegurada por la FDIC. Una vez allí, tus fondos ` + + `están asegurados hasta ${amount} por la FDIC en caso de que ${CONST.WALLET.PROGRAM_ISSUERS.BANCORP_BANK} quiebre. Ver`, fdicInsuranceBancorp2: 'para más detalles.', contactExpensifyPayments: `Comunícate con ${CONST.WALLET.PROGRAM_ISSUERS.EXPENSIFY_PAYMENTS} llamando al + 1833-400-0904, o por correo electrónico a`, contactExpensifyPayments2: 'o inicie sesión en', diff --git a/src/pages/EnablePayments/TermsPage/LongTermsForm.js b/src/pages/EnablePayments/TermsPage/LongTermsForm.js index bdbe87f27b4d..17015df2de52 100644 --- a/src/pages/EnablePayments/TermsPage/LongTermsForm.js +++ b/src/pages/EnablePayments/TermsPage/LongTermsForm.js @@ -88,7 +88,7 @@ function LongTermsForm() { {getLongTermsSections()} - {translate('termsStep.longTermsForm.fdicInsuranceBancorp')} {CONST.TERMS.FDIC_PREPAID}{' '} + {translate('termsStep.longTermsForm.fdicInsuranceBancorp', {amount: CurrencyUtils.convertToDisplayString(25000000, 'USD')})} {CONST.TERMS.FDIC_PREPAID}{' '} {translate('termsStep.longTermsForm.fdicInsuranceBancorp2')} {translate('termsStep.noOverdraftOrCredit')} From da41efb6a64e444c7611ad528c9d702e42012dbf Mon Sep 17 00:00:00 2001 From: Sibtain Ali Date: Tue, 23 Jan 2024 23:47:20 +0500 Subject: [PATCH 315/391] feat: merge with main --- src/components/ReportActionItem/MoneyReportView.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ReportActionItem/MoneyReportView.tsx b/src/components/ReportActionItem/MoneyReportView.tsx index ae3cc4c91b86..ed7c05b828a9 100644 --- a/src/components/ReportActionItem/MoneyReportView.tsx +++ b/src/components/ReportActionItem/MoneyReportView.tsx @@ -21,8 +21,8 @@ import * as ReportUtils from '@libs/ReportUtils'; import AnimatedEmptyStateBackground from '@pages/home/report/AnimatedEmptyStateBackground'; import variables from '@styles/variables'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {Policy, PolicyReportField, Report} from '@src/types/onyx'; import ROUTES from '@src/ROUTES'; +import type {Policy, PolicyReportField, Report} from '@src/types/onyx'; type MoneyReportViewComponentProps = { /** The report currently being looked at */ From f8dc358cdc564546208b34c954a00b7c496f359b Mon Sep 17 00:00:00 2001 From: Rocio Perez-Cano Date: Tue, 23 Jan 2024 19:57:37 +0100 Subject: [PATCH 316/391] Last one --- src/languages/en.ts | 6 +++--- src/languages/es.ts | 8 ++++---- src/languages/types.ts | 3 +++ src/pages/EnablePayments/TermsPage/LongTermsForm.js | 2 +- 4 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index 43972bd7a023..f49befad2c94 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -19,6 +19,7 @@ import type { DeleteConfirmationParams, DidSplitAmountMessageParams, EditActionParams, + ElectronicFundsParams, EnterMagicCodeParams, FormattedMaxLengthParams, GoBackMessageParams, @@ -1400,10 +1401,9 @@ export default { 'There is no fee to transfer funds from your Expensify Wallet ' + 'to your bank account using the standard option. This transfer usually completes within 1-3 business' + ' days.', - electronicFundsInstantDetails: - 'There is a fee to transfer funds from your Expensify Wallet to ' + + electronicFundsInstantDetails: ({percentage, amount}: ElectronicFundsParams) => 'There is a fee to transfer funds from your Expensify Wallet to ' + 'your linked debit card using the instant transfer option. This transfer usually completes within ' + - 'several minutes. The fee is 1.5% of the transfer amount (with a minimum fee of $0.25).', + `several minutes. The fee is ${percentage}% of the transfer amount (with a minimum fee of ${amount}).`, fdicInsuranceBancorp: ({amount}: TermsParams) => 'Your funds are eligible for FDIC insurance. Your funds will be held at or ' + `transferred to ${CONST.WALLET.PROGRAM_ISSUERS.BANCORP_BANK}, an FDIC-insured institution. Once there, your funds are insured up ` + `to ${amount} by the FDIC in the event ${CONST.WALLET.PROGRAM_ISSUERS.BANCORP_BANK} fails. See`, diff --git a/src/languages/es.ts b/src/languages/es.ts index cc3b09d491f0..20a0d6042019 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -18,6 +18,7 @@ import type { DeleteConfirmationParams, DidSplitAmountMessageParams, EditActionParams, + ElectronicFundsParams, EnglishTranslation, EnterMagicCodeParams, FormattedMaxLengthParams, @@ -1421,11 +1422,10 @@ export default { 'No hay cargo por transferir fondos desde tu billetera Expensify ' + 'a tu cuenta bancaria utilizando la opción estándar. Esta transferencia generalmente se completa en' + '1-3 días laborables.', - electronicFundsInstantDetails: - 'Hay una tarifa para transferir fondos desde tu billetera Expensify a ' + + electronicFundsInstantDetails: ({percentage, amount}: ElectronicFundsParams) => 'Hay una tarifa para transferir fondos desde tu billetera Expensify a ' + 'la tarjeta de débito vinculada utilizando la opción de transferencia instantánea. Esta transferencia ' + - 'generalmente se completa dentro de varios minutos. La tarifa es el 1,5% del importe de la ' + - 'transferencia (con una tarifa mínima de US$0,25). ', + `generalmente se completa dentro de varios minutos. La tarifa es el ${percentage}% del importe de la ` + + `transferencia (con una tarifa mínima de ${amount}). `, fdicInsuranceBancorp: ({amount}: TermsParams) => 'Tus fondos pueden acogerse al seguro de la FDIC. Tus fondos se mantendrán o serán ' + `transferidos a ${CONST.WALLET.PROGRAM_ISSUERS.BANCORP_BANK}, una institución asegurada por la FDIC. Una vez allí, tus fondos ` + `están asegurados hasta ${amount} por la FDIC en caso de que ${CONST.WALLET.PROGRAM_ISSUERS.BANCORP_BANK} quiebre. Ver`, diff --git a/src/languages/types.ts b/src/languages/types.ts index b288ccaeb703..11adf01ac252 100644 --- a/src/languages/types.ts +++ b/src/languages/types.ts @@ -289,6 +289,8 @@ type TranslationFlatObject = { type TermsParams = {amount: string}; +type ElectronicFundsParams = {percentage: string; amount: string}; + export type { ApprovedAmountParams, AddressLineParams, @@ -307,6 +309,7 @@ export type { DeleteConfirmationParams, DidSplitAmountMessageParams, EditActionParams, + ElectronicFundsParams, EnglishTranslation, EnterMagicCodeParams, FormattedMaxLengthParams, diff --git a/src/pages/EnablePayments/TermsPage/LongTermsForm.js b/src/pages/EnablePayments/TermsPage/LongTermsForm.js index 17015df2de52..0f96e5a10417 100644 --- a/src/pages/EnablePayments/TermsPage/LongTermsForm.js +++ b/src/pages/EnablePayments/TermsPage/LongTermsForm.js @@ -61,7 +61,7 @@ function LongTermsForm() { subTitle: translate('termsStep.longTermsForm.instant'), rightText: `${numberFormat(1.5)}%`, subRightText: translate('termsStep.longTermsForm.electronicFundsInstantFeeMin', {amount: CurrencyUtils.convertToDisplayString(25, 'USD')}), - details: translate('termsStep.longTermsForm.electronicFundsInstantDetails'), + details: translate('termsStep.longTermsForm.electronicFundsInstantDetails', {percentage: numberFormat(1.5), amount: CurrencyUtils.convertToDisplayString(25, 'USD')}), }, ]; From caaee618b6a31a70edd3fccc32367e6860420631 Mon Sep 17 00:00:00 2001 From: Rocio Perez-Cano Date: Tue, 23 Jan 2024 20:05:14 +0100 Subject: [PATCH 317/391] Prettier --- src/languages/en.ts | 10 ++++++---- src/languages/es.ts | 10 ++++++---- src/pages/EnablePayments/TermsPage/LongTermsForm.js | 8 ++++---- src/pages/EnablePayments/TermsPage/ShortTermsForm.js | 4 ++-- 4 files changed, 18 insertions(+), 14 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index f49befad2c94..121214fa7493 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -1401,12 +1401,14 @@ export default { 'There is no fee to transfer funds from your Expensify Wallet ' + 'to your bank account using the standard option. This transfer usually completes within 1-3 business' + ' days.', - electronicFundsInstantDetails: ({percentage, amount}: ElectronicFundsParams) => 'There is a fee to transfer funds from your Expensify Wallet to ' + + electronicFundsInstantDetails: ({percentage, amount}: ElectronicFundsParams) => + 'There is a fee to transfer funds from your Expensify Wallet to ' + 'your linked debit card using the instant transfer option. This transfer usually completes within ' + `several minutes. The fee is ${percentage}% of the transfer amount (with a minimum fee of ${amount}).`, - fdicInsuranceBancorp: ({amount}: TermsParams) => 'Your funds are eligible for FDIC insurance. Your funds will be held at or ' + - `transferred to ${CONST.WALLET.PROGRAM_ISSUERS.BANCORP_BANK}, an FDIC-insured institution. Once there, your funds are insured up ` + - `to ${amount} by the FDIC in the event ${CONST.WALLET.PROGRAM_ISSUERS.BANCORP_BANK} fails. See`, + fdicInsuranceBancorp: ({amount}: TermsParams) => + 'Your funds are eligible for FDIC insurance. Your funds will be held at or ' + + `transferred to ${CONST.WALLET.PROGRAM_ISSUERS.BANCORP_BANK}, an FDIC-insured institution. Once there, your funds are insured up ` + + `to ${amount} by the FDIC in the event ${CONST.WALLET.PROGRAM_ISSUERS.BANCORP_BANK} fails. See`, fdicInsuranceBancorp2: 'for details.', contactExpensifyPayments: `Contact ${CONST.WALLET.PROGRAM_ISSUERS.EXPENSIFY_PAYMENTS} by calling +1 833-400-0904, by email at`, contactExpensifyPayments2: 'or sign in at', diff --git a/src/languages/es.ts b/src/languages/es.ts index 20a0d6042019..dca38136a02d 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -1422,13 +1422,15 @@ export default { 'No hay cargo por transferir fondos desde tu billetera Expensify ' + 'a tu cuenta bancaria utilizando la opción estándar. Esta transferencia generalmente se completa en' + '1-3 días laborables.', - electronicFundsInstantDetails: ({percentage, amount}: ElectronicFundsParams) => 'Hay una tarifa para transferir fondos desde tu billetera Expensify a ' + + electronicFundsInstantDetails: ({percentage, amount}: ElectronicFundsParams) => + 'Hay una tarifa para transferir fondos desde tu billetera Expensify a ' + 'la tarjeta de débito vinculada utilizando la opción de transferencia instantánea. Esta transferencia ' + `generalmente se completa dentro de varios minutos. La tarifa es el ${percentage}% del importe de la ` + `transferencia (con una tarifa mínima de ${amount}). `, - fdicInsuranceBancorp: ({amount}: TermsParams) => 'Tus fondos pueden acogerse al seguro de la FDIC. Tus fondos se mantendrán o serán ' + - `transferidos a ${CONST.WALLET.PROGRAM_ISSUERS.BANCORP_BANK}, una institución asegurada por la FDIC. Una vez allí, tus fondos ` + - `están asegurados hasta ${amount} por la FDIC en caso de que ${CONST.WALLET.PROGRAM_ISSUERS.BANCORP_BANK} quiebre. Ver`, + fdicInsuranceBancorp: ({amount}: TermsParams) => + 'Tus fondos pueden acogerse al seguro de la FDIC. Tus fondos se mantendrán o serán ' + + `transferidos a ${CONST.WALLET.PROGRAM_ISSUERS.BANCORP_BANK}, una institución asegurada por la FDIC. Una vez allí, tus fondos ` + + `están asegurados hasta ${amount} por la FDIC en caso de que ${CONST.WALLET.PROGRAM_ISSUERS.BANCORP_BANK} quiebre. Ver`, fdicInsuranceBancorp2: 'para más detalles.', contactExpensifyPayments: `Comunícate con ${CONST.WALLET.PROGRAM_ISSUERS.EXPENSIFY_PAYMENTS} llamando al + 1833-400-0904, o por correo electrónico a`, contactExpensifyPayments2: 'o inicie sesión en', diff --git a/src/pages/EnablePayments/TermsPage/LongTermsForm.js b/src/pages/EnablePayments/TermsPage/LongTermsForm.js index 0f96e5a10417..fad19c5ecf6f 100644 --- a/src/pages/EnablePayments/TermsPage/LongTermsForm.js +++ b/src/pages/EnablePayments/TermsPage/LongTermsForm.js @@ -6,11 +6,11 @@ import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; import Text from '@components/Text'; import TextLink from '@components/TextLink'; +import useLocalize from '@hooks/useLocalize'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; -import useLocalize from "@hooks/useLocalize"; +import * as CurrencyUtils from '@libs/CurrencyUtils'; import CONST from '@src/CONST'; -import * as CurrencyUtils from "@libs/CurrencyUtils"; function LongTermsForm() { const theme = useTheme(); @@ -93,8 +93,8 @@ function LongTermsForm() { {translate('termsStep.noOverdraftOrCredit')} - {translate('termsStep.longTermsForm.contactExpensifyPayments')} {CONST.EMAIL.CONCIERGE}{' '} - {translate('termsStep.longTermsForm.contactExpensifyPayments2')} {CONST.NEW_EXPENSIFY_URL}. + {translate('termsStep.longTermsForm.contactExpensifyPayments')} {CONST.EMAIL.CONCIERGE} {translate('termsStep.longTermsForm.contactExpensifyPayments2')}{' '} + {CONST.NEW_EXPENSIFY_URL}. {translate('termsStep.longTermsForm.generalInformation')} {CONST.TERMS.CFPB_PREPAID} diff --git a/src/pages/EnablePayments/TermsPage/ShortTermsForm.js b/src/pages/EnablePayments/TermsPage/ShortTermsForm.js index 3d96d2209dd5..40824f47b036 100644 --- a/src/pages/EnablePayments/TermsPage/ShortTermsForm.js +++ b/src/pages/EnablePayments/TermsPage/ShortTermsForm.js @@ -2,11 +2,11 @@ import React from 'react'; import {View} from 'react-native'; import Text from '@components/Text'; import TextLink from '@components/TextLink'; +import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; +import * as CurrencyUtils from '@libs/CurrencyUtils'; import userWalletPropTypes from '@pages/EnablePayments/userWalletPropTypes'; import CONST from '@src/CONST'; -import useLocalize from "@hooks/useLocalize"; -import * as CurrencyUtils from "@libs/CurrencyUtils"; const propTypes = { /** The user's wallet */ From fe4fd495e59e083a6d42cca64e11f0a1de5dafa4 Mon Sep 17 00:00:00 2001 From: Filip Solecki Date: Tue, 23 Jan 2024 20:58:50 +0100 Subject: [PATCH 318/391] Requested review changes --- src/libs/SuggestionUtils.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/libs/SuggestionUtils.ts b/src/libs/SuggestionUtils.ts index a51acbdab579..96379ce49ef3 100644 --- a/src/libs/SuggestionUtils.ts +++ b/src/libs/SuggestionUtils.ts @@ -1,10 +1,14 @@ import CONST from '@src/CONST'; -/** Trims first character of the string if it is a space */ +/** + * Trims first character of the string if it is a space + */ function trimLeadingSpace(str: string): string { return str.startsWith(' ') ? str.slice(1) : str; } -/** Checks if space is available to render large suggestion menu */ +/** + * Checks if space is available to render large suggestion menu + */ function hasEnoughSpaceForLargeSuggestionMenu(listHeight: number, composerHeight: number, totalSuggestions: number): boolean { const maxSuggestions = CONST.AUTO_COMPLETE_SUGGESTER.MAX_AMOUNT_OF_VISIBLE_SUGGESTIONS_IN_CONTAINER; const chatFooterHeight = CONST.CHAT_FOOTER_SECONDARY_ROW_HEIGHT + 2 * CONST.CHAT_FOOTER_SECONDARY_ROW_PADDING; From 0f43b03c4a8572de1a5a15120e527388ebcc5cf0 Mon Sep 17 00:00:00 2001 From: caitlinwhite1 Date: Tue, 23 Jan 2024 15:08:13 -0600 Subject: [PATCH 319/391] Delete docs/articles/expensify-classic/getting-started/Plan-Types.md this resource exists on UseDot, so let's delete it --- .../getting-started/Plan-Types.md | 34 ------------------- 1 file changed, 34 deletions(-) delete mode 100644 docs/articles/expensify-classic/getting-started/Plan-Types.md diff --git a/docs/articles/expensify-classic/getting-started/Plan-Types.md b/docs/articles/expensify-classic/getting-started/Plan-Types.md deleted file mode 100644 index 4f8c52c2e1a1..000000000000 --- a/docs/articles/expensify-classic/getting-started/Plan-Types.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -title: Plan Types -description: Learn which Expensify plan is the best fit for you ---- -# Overview -You can access comprehensive information about Expensify's plans and pricing by visiting www.expensify.com/pricing. Below, we provide an overview of each plan type to assist you in selecting the one that best suits your business or personal requirements. - -## Free Plan -The Free plan is suited for small businesses, offering a dedicated workspace for efficiently handling Expensify card management, expense reimbursement, invoicing, and bill payment. This plan includes unlimited receipt scanning for all users within the company and the potential to earn up to 1% cashback on card spending exceeding $25,000 per month (across all cards). - -## Collect Workspace Plan -The Collect Workspace Plan is designed with small companies in mind, providing essential features like a single layer of expense approvals, reimbursement capabilities, corporate card management, and basic integration options such as QuickBooks Online, QuickBooks Desktop, and Xero. This plan is ideal for those who require simple expense management functions. - -## Control Workspace Plan -Our most popular option, the Control Workspace plan, offers a heightened level of control and Workspace customization. With a Control Workspace, you gain access to multi-level approval workflows, comprehensive corporate card management, advanced accounting integration, tax tracking capabilities, and advanced expense rules that facilitate the enforcement of your internal expense policy. This plan provides a robust set of features for effective expense management. - -## Individual Track Plan -The Track plan is tailored for solo Expensify users who don't require expense submission to others. Individuals or sole proprietors can choose the Track plan to customize their Individual Workspace to align with their personal expense tracking requirements. - -## Individual Submit Plan -The Submit plan is designed for individuals who need to keep track of their expenses and share them with someone else, such as their boss, accountant, or even a housemate. It's specifically tailored for single users who want to both track and submit their expenses efficiently. - -{% include faq-begin.md %} - -## How can I change Individual plans? -You have the flexibility to switch between a Track and Submit plan, or vice versa, at any time by navigating to **Settings > Workspaces > Individual > *Workspace Name* > Plan**. This allows you to adapt your expense management approach as needed. - -## How can I upgrade Group plans? -You can easily upgrade from a Collect to a Control plan at any time by going to **Settings > Workspaces > Group > *Workspace Name* > Plan**. However, it's important to note that if you have an active Annual Subscription, downgrading from Control to Collect is not possible until your current commitment period expires. - -## How does pricing work if I have two types of Group Workspace plans? -If you have a Control and Collect Workspace, you will be charged at the Control Workspace rate. - -{% include faq-end.md %} From 970183a3393eef074145d88e511c45bd87b454fb Mon Sep 17 00:00:00 2001 From: rory Date: Tue, 23 Jan 2024 16:11:46 -0800 Subject: [PATCH 320/391] Fix typo in merge conflict resolution --- src/components/LHNOptionsList/OptionRowLHN.tsx | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/components/LHNOptionsList/OptionRowLHN.tsx b/src/components/LHNOptionsList/OptionRowLHN.tsx index 09e10ab98c12..b085625c2914 100644 --- a/src/components/LHNOptionsList/OptionRowLHN.tsx +++ b/src/components/LHNOptionsList/OptionRowLHN.tsx @@ -61,15 +61,13 @@ function OptionRowLHN({reportID, isFocused = false, onSelectRow = () => {}, opti const textStyle = isFocused ? styles.sidebarLinkActiveText : styles.sidebarLinkText; const textUnreadStyle = optionItem?.isUnread && optionItem.notificationPreference !== CONST.REPORT.NOTIFICATION_PREFERENCE.MUTE ? [textStyle, styles.sidebarLinkTextBold] : [textStyle]; const displayNameStyle = [styles.optionDisplayName, styles.optionDisplayNameCompact, styles.pre, textUnreadStyle, style]; - const alternateTextStyle = - isInFocusMode - ? [textStyle, styles.optionAlternateText, styles.pre, styles.textLabelSupporting, styles.optionAlternateTextCompact, styles.ml2, style] - : [textStyle, styles.optionAlternateText, styles.pre, styles.textLabelSupporting, style]; + const alternateTextStyle = isInFocusMode + ? [textStyle, styles.optionAlternateText, styles.pre, styles.textLabelSupporting, styles.optionAlternateTextCompact, styles.ml2, style] + : [textStyle, styles.optionAlternateText, styles.pre, styles.textLabelSupporting, style]; - const contentContainerStyles = - isInFocusMode ? [styles.flex1, styles.flexRow, styles.overflowHidden, StyleUtils.getCompactContentContainerStyles()] : [styles.flex1]; + const contentContainerStyles = isInFocusMode ? [styles.flex1, styles.flexRow, styles.overflowHidden, StyleUtils.getCompactContentContainerStyles()] : [styles.flex1]; const sidebarInnerRowStyle = StyleSheet.flatten( - IsInFocusMode + isInFocusMode ? [styles.chatLinkRowPressable, styles.flexGrow1, styles.optionItemAvatarNameWrapper, styles.optionRowCompact, styles.justifyContentCenter] : [styles.chatLinkRowPressable, styles.flexGrow1, styles.optionItemAvatarNameWrapper, styles.optionRow, styles.justifyContentCenter], ); From 9c9c654f7996df03a781eb16b0c40dfcc6b89fe6 Mon Sep 17 00:00:00 2001 From: rory Date: Tue, 23 Jan 2024 16:14:47 -0800 Subject: [PATCH 321/391] Don't include muted reports in unread count --- src/libs/UnreadIndicatorUpdater/index.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/libs/UnreadIndicatorUpdater/index.ts b/src/libs/UnreadIndicatorUpdater/index.ts index 2a7019686308..b4f3cd34a8c4 100644 --- a/src/libs/UnreadIndicatorUpdater/index.ts +++ b/src/libs/UnreadIndicatorUpdater/index.ts @@ -26,8 +26,12 @@ export default function getUnreadReportsForUnreadIndicator(reports: OnyxCollecti * Chats with hidden preference remain invisible in the LHN and are not considered "unread." * They are excluded from the LHN rendering, but not filtered from the "option list." * This ensures they appear in Search, but not in the LHN or unread count. + * + * Furthermore, muted reports may or may not appear in the LHN depending on priority mode, + * but they should not be considered in the unread indicator count. */ - report?.notificationPreference !== CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN, + report?.notificationPreference !== CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN && + report?.notificationPreference !== CONST.REPORT.NOTIFICATION_PREFERENCE.MUTE, ); } From 129b801cccdb81c96fa30ec12d12dd40092f30cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Wed, 24 Jan 2024 09:13:05 +0100 Subject: [PATCH 322/391] update env files correctly --- .../actions/composite/buildAndroidE2EAPK/action.yml | 12 ++++++++++++ .github/workflows/e2ePerformanceTests.yml | 2 ++ 2 files changed, 14 insertions(+) diff --git a/.github/actions/composite/buildAndroidE2EAPK/action.yml b/.github/actions/composite/buildAndroidE2EAPK/action.yml index 217c5acdd506..0c5f70929c27 100644 --- a/.github/actions/composite/buildAndroidE2EAPK/action.yml +++ b/.github/actions/composite/buildAndroidE2EAPK/action.yml @@ -14,6 +14,9 @@ inputs: MAPBOX_SDK_DOWNLOAD_TOKEN: description: The token to use to download the MapBox SDK required: true + PATH_ENV_FILE: + description: The path to the .env file to use for the build + required: true EXPENSIFY_PARTNER_NAME: description: The name of the Expensify partner to use for the build required: true @@ -52,6 +55,15 @@ runs: - uses: gradle/gradle-build-action@3fbe033aaae657f011f88f29be9e65ed26bd29ef + - name: Append environment variables to env file + shell: bash + run: | + echo "EXPENSIFY_PARTNER_NAME=${EXPENSIFY_PARTNER_NAME}" >> ${{ inputs.PATH_ENV_FILE }} + echo "EXPENSIFY_PARTNER_PASSWORD=${EXPENSIFY_PARTNER_PASSWORD}" >> ${{ inputs.PATH_ENV_FILE }} + echo "EXPENSIFY_PARTNER_USER_ID=${EXPENSIFY_PARTNER_USER_ID}" >> ${{ inputs.PATH_ENV_FILE }} + echo "EXPENSIFY_PARTNER_USER_SECRET=${EXPENSIFY_PARTNER_USER_SECRET}" >> ${{ inputs.PATH_ENV_FILE }} + echo "EXPENSIFY_PARTNER_PASSWORD_EMAIL=${EXPENSIFY_PARTNER_PASSWORD_EMAIL}" >> ${{ inputs.PATH_ENV_FILE }} + - name: Build APK run: npm run ${{ inputs.PACKAGE_SCRIPT_NAME }} shell: bash diff --git a/.github/workflows/e2ePerformanceTests.yml b/.github/workflows/e2ePerformanceTests.yml index 1f28822a4a39..70f70fca60de 100644 --- a/.github/workflows/e2ePerformanceTests.yml +++ b/.github/workflows/e2ePerformanceTests.yml @@ -57,6 +57,7 @@ jobs: EXPENSIFY_PARTNER_USER_ID: ${{ secrets.EXPENSIFY_PARTNER_USER_ID }} EXPENSIFY_PARTNER_USER_SECRET: ${{ secrets.EXPENSIFY_PARTNER_USER_SECRET }} EXPENSIFY_PARTNER_PASSWORD_EMAIL: ${{ secrets.EXPENSIFY_PARTNER_PASSWORD_EMAIL }} + PATH_ENV_FILE: tests/e2e/.env.e2e buildDelta: runs-on: ubuntu-latest-xl @@ -124,6 +125,7 @@ jobs: EXPENSIFY_PARTNER_USER_ID: ${{ secrets.EXPENSIFY_PARTNER_USER_ID }} EXPENSIFY_PARTNER_USER_SECRET: ${{ secrets.EXPENSIFY_PARTNER_USER_SECRET }} EXPENSIFY_PARTNER_PASSWORD_EMAIL: ${{ secrets.EXPENSIFY_PARTNER_PASSWORD_EMAIL }} + PATH_ENV_FILE: tests/e2e/.env.e2edelta runTestsInAWS: runs-on: ubuntu-latest From 2ff79c0afc5bf287b8ae9eeae04029bf0b910e6f Mon Sep 17 00:00:00 2001 From: VickyStash Date: Wed, 24 Jan 2024 10:10:34 +0100 Subject: [PATCH 323/391] Replace nullish coalescing with logical or --- src/components/StatePicker/StateSelectorModal.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/components/StatePicker/StateSelectorModal.tsx b/src/components/StatePicker/StateSelectorModal.tsx index 5be88a77f887..cc6f88617907 100644 --- a/src/components/StatePicker/StateSelectorModal.tsx +++ b/src/components/StatePicker/StateSelectorModal.tsx @@ -82,14 +82,18 @@ function StateSelectorModal({currentState, isVisible, onClose = () => {}, onStat testID={StateSelectorModal.displayName} > Date: Wed, 24 Jan 2024 15:16:15 +0530 Subject: [PATCH 324/391] removed comment --- src/pages/PrivateNotes/PrivateNotesEditPage.tsx | 2 -- src/pages/PrivateNotes/PrivateNotesListPage.tsx | 2 -- 2 files changed, 4 deletions(-) diff --git a/src/pages/PrivateNotes/PrivateNotesEditPage.tsx b/src/pages/PrivateNotes/PrivateNotesEditPage.tsx index 6292a2e3c412..6b96ee657d65 100644 --- a/src/pages/PrivateNotes/PrivateNotesEditPage.tsx +++ b/src/pages/PrivateNotes/PrivateNotesEditPage.tsx @@ -31,8 +31,6 @@ import type {PersonalDetails, Report} from '@src/types/onyx'; import type {Note} from '@src/types/onyx/Report'; type PrivateNotesEditPageOnyxProps = { - /* Onyx Props */ - /** All of the personal details for everyone */ personalDetailsList: OnyxCollection; }; diff --git a/src/pages/PrivateNotes/PrivateNotesListPage.tsx b/src/pages/PrivateNotes/PrivateNotesListPage.tsx index 30bd90bed5b6..d7fb1f6497be 100644 --- a/src/pages/PrivateNotes/PrivateNotesListPage.tsx +++ b/src/pages/PrivateNotes/PrivateNotesListPage.tsx @@ -17,8 +17,6 @@ import ROUTES from '@src/ROUTES'; import type {PersonalDetails, Report, Session} from '@src/types/onyx'; type PrivateNotesListPageOnyxProps = { - /* Onyx Props */ - /** All of the personal details for everyone */ personalDetailsList: OnyxCollection; From 260f78e3be8550dd01ac78b8a91d255bfa75d3b0 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Wed, 24 Jan 2024 10:59:51 +0100 Subject: [PATCH 325/391] fix: add correct condition --- src/components/LHNOptionsList/OptionRowLHN.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/LHNOptionsList/OptionRowLHN.tsx b/src/components/LHNOptionsList/OptionRowLHN.tsx index 1932cf6c6b7f..a36a9b2b8451 100644 --- a/src/components/LHNOptionsList/OptionRowLHN.tsx +++ b/src/components/LHNOptionsList/OptionRowLHN.tsx @@ -113,7 +113,7 @@ function OptionRowLHN({reportID, isFocused = false, onSelectRow = () => {}, opti const report = ReportUtils.getReport(optionItem.reportID ?? ''); const isStatusVisible = !!emojiCode && ReportUtils.isOneOnOneChat(!isEmptyObject(report) ? report : null); - const isGroupChat = optionItem.type === CONST.REPORT.TYPE.CHAT && optionItem.chatType && !optionItem.isThread && (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(!isEmptyObject(report) ? report : null) : optionItem.text; const subscriptAvatarBorderColor = isFocused ? focusedBackgroundColor : theme.sidebar; From a9685ca2c8e3dc4f8c1314f9916b2d4c17001501 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Wed, 24 Jan 2024 11:20:33 +0100 Subject: [PATCH 326/391] fix: add more strict comparision --- src/components/LHNOptionsList/OptionRowLHN.tsx | 2 +- src/types/onyx/Report.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/LHNOptionsList/OptionRowLHN.tsx b/src/components/LHNOptionsList/OptionRowLHN.tsx index a36a9b2b8451..218cdc70a86e 100644 --- a/src/components/LHNOptionsList/OptionRowLHN.tsx +++ b/src/components/LHNOptionsList/OptionRowLHN.tsx @@ -113,7 +113,7 @@ function OptionRowLHN({reportID, isFocused = false, onSelectRow = () => {}, opti const report = ReportUtils.getReport(optionItem.reportID ?? ''); const isStatusVisible = !!emojiCode && ReportUtils.isOneOnOneChat(!isEmptyObject(report) ? report : null); - const isGroupChat = optionItem.type === CONST.REPORT.TYPE.CHAT && !optionItem.chatType && !optionItem.isThread && (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(!isEmptyObject(report) ? report : null) : optionItem.text; const subscriptAvatarBorderColor = isFocused ? focusedBackgroundColor : theme.sidebar; diff --git a/src/types/onyx/Report.ts b/src/types/onyx/Report.ts index b1571e7514e4..ab04126ff782 100644 --- a/src/types/onyx/Report.ts +++ b/src/types/onyx/Report.ts @@ -15,7 +15,7 @@ type Note = { type Report = { /** The specific type of chat */ - chatType?: ValueOf; + chatType?: ValueOf | ''; /** Whether the report has a child that is an outstanding money request that is awaiting action from the current user */ hasOutstandingChildRequest?: boolean; From 7617114e87e97f4c7a4dd0cea0ba2b82155cba92 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Wed, 24 Jan 2024 11:30:59 +0100 Subject: [PATCH 327/391] Remove outdated TODO --- src/components/StatePicker/StateSelectorModal.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/StatePicker/StateSelectorModal.tsx b/src/components/StatePicker/StateSelectorModal.tsx index cc6f88617907..798d3be7a698 100644 --- a/src/components/StatePicker/StateSelectorModal.tsx +++ b/src/components/StatePicker/StateSelectorModal.tsx @@ -89,7 +89,6 @@ function StateSelectorModal({currentState, isVisible, onClose = () => {}, onStat onBackButtonPress={onClose} /> Date: Wed, 24 Jan 2024 11:43:57 +0100 Subject: [PATCH 328/391] Add approval mode for policies --- src/CONST.ts | 8 ++++++++ src/types/onyx/Policy.ts | 3 +++ tests/utils/LHNTestUtils.js | 1 + tests/utils/collections/policies.ts | 1 + 4 files changed, 13 insertions(+) diff --git a/src/CONST.ts b/src/CONST.ts index 5fee60e57617..29527abf3111 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -1305,6 +1305,14 @@ const CONST = { LAST_BUSINESS_DAY_OF_MONTH: 'lastBusinessDayOfMonth', LAST_DAY_OF_MONTH: 'lastDayOfMonth', }, + APPROVAL_MODE: { + OPTIONAL: 'OPTIONAL', + BASIC: 'BASIC', + ADVANCED: 'ADVANCED', + DYNAMICEXTERNAL: 'DYNAMIC_EXTERNAL', + SMARTREPORT: 'SMARTREPORT', + BILLCOM: 'BILLCOM', + }, ROOM_PREFIX: '#', CUSTOM_UNIT_RATE_BASE_OFFSET: 100, OWNER_EMAIL_FAKE: '_FAKE_', diff --git a/src/types/onyx/Policy.ts b/src/types/onyx/Policy.ts index 97e6597c6444..b56e4b05aba8 100644 --- a/src/types/onyx/Policy.ts +++ b/src/types/onyx/Policy.ts @@ -118,6 +118,9 @@ type Policy = { /** Informative messages about which policy members were added with primary logins when invited with their secondary login */ primaryLoginsInvited?: Record; + + /** The approval mode set up on this policy */ + approvalMode?: string; }; export default Policy; diff --git a/tests/utils/LHNTestUtils.js b/tests/utils/LHNTestUtils.js index 67831bd32bca..6c72558e5df3 100644 --- a/tests/utils/LHNTestUtils.js +++ b/tests/utils/LHNTestUtils.js @@ -262,6 +262,7 @@ function getFakePolicy(id = 1, name = 'Workspace-Test-001') { submitsTo: 123456, defaultBillable: false, disabledFields: {defaultBillable: true, reimbursable: false}, + approvalMode: 'BASIC', }; } diff --git a/tests/utils/collections/policies.ts b/tests/utils/collections/policies.ts index acdd22253173..8547c171c7a7 100644 --- a/tests/utils/collections/policies.ts +++ b/tests/utils/collections/policies.ts @@ -26,5 +26,6 @@ export default function createRandomPolicy(index: number): Policy { errors: {}, customUnits: {}, errorFields: {}, + approvalMode: rand(Object.values(CONST.POLICY.APPROVAL_MODE)), }; } From 7734c9a52f7742d3450d1c29b89c84f7ff44c2d8 Mon Sep 17 00:00:00 2001 From: Alberto Date: Wed, 24 Jan 2024 11:56:12 +0100 Subject: [PATCH 329/391] stricter --- src/types/onyx/Policy.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types/onyx/Policy.ts b/src/types/onyx/Policy.ts index b56e4b05aba8..9e6af632353e 100644 --- a/src/types/onyx/Policy.ts +++ b/src/types/onyx/Policy.ts @@ -120,7 +120,7 @@ type Policy = { primaryLoginsInvited?: Record; /** The approval mode set up on this policy */ - approvalMode?: string; + approvalMode?: ValueOf; }; export default Policy; From f7b827523115d38eb54ccbd060d3f841ead74c5e Mon Sep 17 00:00:00 2001 From: rayane-djouah <77965000+rayane-djouah@users.noreply.github.com> Date: Wed, 24 Jan 2024 11:57:11 +0100 Subject: [PATCH 330/391] add 'undefined' safety --- src/hooks/useResponsiveLayout.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/hooks/useResponsiveLayout.ts b/src/hooks/useResponsiveLayout.ts index 10f1bccf15bd..42af016bef29 100644 --- a/src/hooks/useResponsiveLayout.ts +++ b/src/hooks/useResponsiveLayout.ts @@ -10,8 +10,8 @@ type ResponsiveLayoutResult = { */ export default function useResponsiveLayout(): ResponsiveLayoutResult { const {isSmallScreenWidth} = useWindowDimensions(); - const state = navigationRef.getRootState(); - const lastRoute = state.routes.at(-1); + const state = navigationRef?.getRootState(); + const lastRoute = state?.routes?.at(-1); const lastRouteName = lastRoute?.name; const isInModal = lastRouteName === NAVIGATORS.LEFT_MODAL_NAVIGATOR || lastRouteName === NAVIGATORS.RIGHT_MODAL_NAVIGATOR; const shouldUseNarrowLayout = isSmallScreenWidth || isInModal; From 2c7cf2b4a8533075c6b68e2df3c87635b8ffa758 Mon Sep 17 00:00:00 2001 From: tienifr Date: Wed, 24 Jan 2024 17:59:29 +0700 Subject: [PATCH 331/391] update comment --- .../HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.js b/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.js index a5172125dc3e..bae4258461a7 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.js +++ b/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.js @@ -42,15 +42,17 @@ function MentionUserRenderer(props) { const tnode = cloneDeep(props.tnode); const getMentionDisplayText = (displayText, userAccountID, userLogin = '') => { - // if the userAccountID does not exist, this is email-based mention so the displayText must be an email. + // If the userAccountID does not exist, this is an email-based mention so the displayText must be an email. // If the userAccountID exists but userLogin is different from displayText, this means the displayText is either user display name, Hidden, or phone number, in which case we should return it as is. if (userAccountID && userLogin !== displayText) { return displayText; } + // If the emails are not in the same private domain, we also return the displayText if (!LoginUtils.areEmailsFromSamePrivateDomain(displayText, props.currentUserPersonalDetails.login)) { return displayText; } + // Otherwise, the emails must be of the same private domain, so we should remove the domain part return displayText.split('@')[0]; }; From dfcbaf7334785cb9f08d3ac25419394b0107fe72 Mon Sep 17 00:00:00 2001 From: rayane-djouah <77965000+rayane-djouah@users.noreply.github.com> Date: Wed, 24 Jan 2024 12:02:23 +0100 Subject: [PATCH 332/391] return isSmallScreenWidth and isInModal from useResponsiveLayout --- src/hooks/useResponsiveLayout.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/hooks/useResponsiveLayout.ts b/src/hooks/useResponsiveLayout.ts index 42af016bef29..a825acd1039c 100644 --- a/src/hooks/useResponsiveLayout.ts +++ b/src/hooks/useResponsiveLayout.ts @@ -3,7 +3,8 @@ import NAVIGATORS from '@src/NAVIGATORS'; import useWindowDimensions from './useWindowDimensions'; type ResponsiveLayoutResult = { - shouldUseNarrowLayout: boolean; + isSmallScreenWidth: boolean; + isInModal: boolean; }; /** * Hook to determine if we are on mobile devices or in the Modal Navigator @@ -14,6 +15,5 @@ export default function useResponsiveLayout(): ResponsiveLayoutResult { const lastRoute = state?.routes?.at(-1); const lastRouteName = lastRoute?.name; const isInModal = lastRouteName === NAVIGATORS.LEFT_MODAL_NAVIGATOR || lastRouteName === NAVIGATORS.RIGHT_MODAL_NAVIGATOR; - const shouldUseNarrowLayout = isSmallScreenWidth || isInModal; - return {shouldUseNarrowLayout}; + return {isSmallScreenWidth, isInModal}; } From c5ba73717fb3c1c8d5d4b4e80459840d171bbb4a Mon Sep 17 00:00:00 2001 From: Alberto Date: Wed, 24 Jan 2024 12:03:08 +0100 Subject: [PATCH 333/391] prettier --- src/types/onyx/Policy.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types/onyx/Policy.ts b/src/types/onyx/Policy.ts index 9e6af632353e..2d8bbb7924bd 100644 --- a/src/types/onyx/Policy.ts +++ b/src/types/onyx/Policy.ts @@ -120,7 +120,7 @@ type Policy = { primaryLoginsInvited?: Record; /** The approval mode set up on this policy */ - approvalMode?: ValueOf; + approvalMode?: ValueOf; }; export default Policy; From 433eedad6b96705b4d3de59d6d30ea8987c623a7 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Wed, 24 Jan 2024 13:01:24 +0100 Subject: [PATCH 334/391] fix: revert last change --- src/components/LHNOptionsList/OptionRowLHN.tsx | 2 +- src/types/onyx/Report.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/LHNOptionsList/OptionRowLHN.tsx b/src/components/LHNOptionsList/OptionRowLHN.tsx index 218cdc70a86e..a36a9b2b8451 100644 --- a/src/components/LHNOptionsList/OptionRowLHN.tsx +++ b/src/components/LHNOptionsList/OptionRowLHN.tsx @@ -113,7 +113,7 @@ function OptionRowLHN({reportID, isFocused = false, onSelectRow = () => {}, opti const report = ReportUtils.getReport(optionItem.reportID ?? ''); const isStatusVisible = !!emojiCode && ReportUtils.isOneOnOneChat(!isEmptyObject(report) ? report : null); - const isGroupChat = optionItem.type === CONST.REPORT.TYPE.CHAT && optionItem.chatType !== '' && !optionItem.isThread && (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(!isEmptyObject(report) ? report : null) : optionItem.text; const subscriptAvatarBorderColor = isFocused ? focusedBackgroundColor : theme.sidebar; diff --git a/src/types/onyx/Report.ts b/src/types/onyx/Report.ts index ab04126ff782..b1571e7514e4 100644 --- a/src/types/onyx/Report.ts +++ b/src/types/onyx/Report.ts @@ -15,7 +15,7 @@ type Note = { type Report = { /** The specific type of chat */ - chatType?: ValueOf | ''; + chatType?: ValueOf; /** Whether the report has a child that is an outstanding money request that is awaiting action from the current user */ hasOutstandingChildRequest?: boolean; From e933a83d32cdeab0d51b4c1b373bd7cdb4819c64 Mon Sep 17 00:00:00 2001 From: Fitsum Abebe Date: Wed, 24 Jan 2024 16:11:26 +0300 Subject: [PATCH 335/391] clean tag for old money confirmation list --- src/components/MoneyRequestConfirmationList.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/MoneyRequestConfirmationList.js b/src/components/MoneyRequestConfirmationList.js index 590154b48bca..d967d04ab94b 100755 --- a/src/components/MoneyRequestConfirmationList.js +++ b/src/components/MoneyRequestConfirmationList.js @@ -758,7 +758,7 @@ function MoneyRequestConfirmationList(props) { {shouldShowTags && ( { From 50c6a645d7c0428c45ade406a6805016a09ab821 Mon Sep 17 00:00:00 2001 From: Conor Pendergrast Date: Thu, 25 Jan 2024 02:49:38 +1300 Subject: [PATCH 336/391] Update Set-Up-the-Card-for-Your-Company.md --- .../expensify-card/Set-Up-the-Card-for-Your-Company.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/articles/expensify-classic/expensify-card/Set-Up-the-Card-for-Your-Company.md b/docs/articles/expensify-classic/expensify-card/Set-Up-the-Card-for-Your-Company.md index 464f2129d800..4228c4f8618c 100644 --- a/docs/articles/expensify-classic/expensify-card/Set-Up-the-Card-for-Your-Company.md +++ b/docs/articles/expensify-classic/expensify-card/Set-Up-the-Card-for-Your-Company.md @@ -58,7 +58,7 @@ Applying for or using the Expensify Card will never have any positive or negativ ## How much does the Expensify Card cost? -The Expensify Card is a free corporate card, and no fees are associated with it. In addition, if you use the Expensify Card, you can save money on your Expensify subscription. +The Expensify Card is a free corporate card, and no fees are associated with it. In addition, if you use the Expensify Card, you can save money on your Expensify subscription (based on how much of your approved spend happens on the Expensify Card compared with reimbursable spend in each month). ## If I have staff outside the US, can they use the Expensify Card? From f1352d3f0b17947d30a86f5acfc038d9ab91dd45 Mon Sep 17 00:00:00 2001 From: Conor Pendergrast Date: Thu, 25 Jan 2024 02:52:52 +1300 Subject: [PATCH 337/391] Update Receipt-Breakdown.md --- .../billing-and-subscriptions/Receipt-Breakdown.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/docs/articles/expensify-classic/billing-and-subscriptions/Receipt-Breakdown.md b/docs/articles/expensify-classic/billing-and-subscriptions/Receipt-Breakdown.md index d4181735298e..dad0b5fbb6c5 100644 --- a/docs/articles/expensify-classic/billing-and-subscriptions/Receipt-Breakdown.md +++ b/docs/articles/expensify-classic/billing-and-subscriptions/Receipt-Breakdown.md @@ -16,8 +16,7 @@ Your receipt is broken up into multiple sections that include: The top section will show the total amount you paid as the billing owner of Expensify workspaces and give you a breakdown of price per member. Every member of your workspace(s) gets to store data, review data, and access free features like Expensify Chat. Thus, we show the total price and then use all of the members across all of the workspaces you own to calculate the price per member. Further down in the receipt, and in this article, we break down the members who generated billable activity. ## How-to reduce your bill and get paid to use Expensify -Chances are you can actually get paid to use Expensify with the Expensify Card. In this section of the receipt, we outline how much money you're leaving on the table by not using the Expensify Card. You can click `Get started` to connect with your account manager (if you have one) or Concierge, both of whom can help get you started with the card. - +Chances are you can actually get paid to use Expensify with the Expensify Card. In this section of the receipt, we outline how much money you're leaving on the table by not using the Expensify Card. You can click `Get started` to connect with your account manager (if you have one) or Concierge, both of whom can help get you started with the card. _Note: Currently, we offer Expensify Cards to companies with USD bank accounts._ ## How-to understand your billing breakdown @@ -37,9 +36,9 @@ Your receipt will have a detailed breakdown of activity and discounts across all - [Number of] Free members @ $0.00 - All members across any of your Free workspaces. - X% Expensify Card discount with $Y spend - - This shows the % discount you're getting based on total spend across your Expensify Cards. This is only available in the US. + - This shows the % discount you're getting based on total approved spend across your Expensify Cards. This is only available in the US. - X% Expensify Card cash back credit for $Y spend - - The amount of cash back you've earned based on total spend across your Expensify Cards. This is only available in the US. + - The amount of cash back you've earned based on total approved spend across your Expensify Cards. This is only available in the US. - 50% ExpensifyApproved! partner discount - If you're part of an accounting firm, you get an additional discount for being our partner. [Learn more about our ExpensifyApproved! accountants program.](https://use.expensify.com/accountants-program) - Total From e911ee0611d8a51a1adf7d81a0e12365fbde9bc3 Mon Sep 17 00:00:00 2001 From: Conor Pendergrast Date: Thu, 25 Jan 2024 02:54:39 +1300 Subject: [PATCH 338/391] Update Receipt-Breakdown.md --- .../billing-and-subscriptions/Receipt-Breakdown.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/articles/expensify-classic/billing-and-subscriptions/Receipt-Breakdown.md b/docs/articles/expensify-classic/billing-and-subscriptions/Receipt-Breakdown.md index dad0b5fbb6c5..65618d816eff 100644 --- a/docs/articles/expensify-classic/billing-and-subscriptions/Receipt-Breakdown.md +++ b/docs/articles/expensify-classic/billing-and-subscriptions/Receipt-Breakdown.md @@ -16,7 +16,8 @@ Your receipt is broken up into multiple sections that include: The top section will show the total amount you paid as the billing owner of Expensify workspaces and give you a breakdown of price per member. Every member of your workspace(s) gets to store data, review data, and access free features like Expensify Chat. Thus, we show the total price and then use all of the members across all of the workspaces you own to calculate the price per member. Further down in the receipt, and in this article, we break down the members who generated billable activity. ## How-to reduce your bill and get paid to use Expensify -Chances are you can actually get paid to use Expensify with the Expensify Card. In this section of the receipt, we outline how much money you're leaving on the table by not using the Expensify Card. You can click `Get started` to connect with your account manager (if you have one) or Concierge, both of whom can help get you started with the card. +Chances are you can actually get paid to use Expensify with the Expensify Card. In this section of the receipt, we outline how much money you're leaving on the table by not using the Expensify Card. You can click `Get started` to connect with your account manager (if you have one) or Concierge, both of whom can help get you started with the card. + _Note: Currently, we offer Expensify Cards to companies with USD bank accounts._ ## How-to understand your billing breakdown From 3f8930c150b0f06cd980f0aa3e07dc079a8a7430 Mon Sep 17 00:00:00 2001 From: Conor Pendergrast Date: Thu, 25 Jan 2024 02:57:45 +1300 Subject: [PATCH 339/391] Update Set-Up-the-Card-for-Your-Company.md --- .../expensify-card/Set-Up-the-Card-for-Your-Company.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/articles/expensify-classic/expensify-card/Set-Up-the-Card-for-Your-Company.md b/docs/articles/expensify-classic/expensify-card/Set-Up-the-Card-for-Your-Company.md index 4228c4f8618c..531648b10350 100644 --- a/docs/articles/expensify-classic/expensify-card/Set-Up-the-Card-for-Your-Company.md +++ b/docs/articles/expensify-classic/expensify-card/Set-Up-the-Card-for-Your-Company.md @@ -58,7 +58,7 @@ Applying for or using the Expensify Card will never have any positive or negativ ## How much does the Expensify Card cost? -The Expensify Card is a free corporate card, and no fees are associated with it. In addition, if you use the Expensify Card, you can save money on your Expensify subscription (based on how much of your approved spend happens on the Expensify Card compared with reimbursable spend in each month). +The Expensify Card is a free corporate card, and no fees are associated with it. In addition, if you use the Expensify Card, you can save money on your Expensify subscription (based on how much of your approved spend happens on the Expensify Card, compared with other approved spend, in each month). ## If I have staff outside the US, can they use the Expensify Card? From 5547b80395cc53c20623b67854471f0d610db0e0 Mon Sep 17 00:00:00 2001 From: Conor Pendergrast Date: Thu, 25 Jan 2024 03:05:38 +1300 Subject: [PATCH 340/391] Update Receipt-Breakdown.md --- .../billing-and-subscriptions/Receipt-Breakdown.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/articles/expensify-classic/billing-and-subscriptions/Receipt-Breakdown.md b/docs/articles/expensify-classic/billing-and-subscriptions/Receipt-Breakdown.md index 65618d816eff..fd137aab62fb 100644 --- a/docs/articles/expensify-classic/billing-and-subscriptions/Receipt-Breakdown.md +++ b/docs/articles/expensify-classic/billing-and-subscriptions/Receipt-Breakdown.md @@ -25,7 +25,7 @@ Your receipt will have a detailed breakdown of activity and discounts across all - [Number of] Inactive workspace members @ $0.00 - All inactive members from any of your workspaces. - [Number of] Chat-only members @ $0.00 - - Any workspace members who chatted but didn't generate any other billable activity. Learn more about [chatting for free.](https://help.expensify.com/articles/new-expensify/getting-started/chat/Everything-About-Chat) + - Any workspace members who chatted but didn't generate any other billable activity. Learn more about [chatting for free.](https://help.expensify.com/articles/new-expensify/chat/Introducing-Expensify-Chat) - [Number of] Annual Control members @ $18.00 - Any members included in your annual subscription on the Control plan. - [Number of] Pay-per-use Control members @ $36.00 From 5b286ad685f7caab305eabc3a3eb0147c4d039db Mon Sep 17 00:00:00 2001 From: Tomasz Lesniakiewicz Date: Wed, 24 Jan 2024 15:15:57 +0100 Subject: [PATCH 341/391] fix: remove unnecessary providers from reassure tests --- tests/perf-test/SearchPage.perf-test.js | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/tests/perf-test/SearchPage.perf-test.js b/tests/perf-test/SearchPage.perf-test.js index 046d651469f1..e948ade014dc 100644 --- a/tests/perf-test/SearchPage.perf-test.js +++ b/tests/perf-test/SearchPage.perf-test.js @@ -5,11 +5,7 @@ import {measurePerformance} from 'reassure'; import _ from 'underscore'; import SearchPage from '@pages/SearchPage'; import ComposeProviders from '../../src/components/ComposeProviders'; -import {LocaleContextProvider} from '../../src/components/LocaleContextProvider'; import OnyxProvider from '../../src/components/OnyxProvider'; -import {CurrentReportIDContextProvider} from '../../src/components/withCurrentReportID'; -import {KeyboardStateProvider} from '../../src/components/withKeyboardState'; -import {WindowDimensionsProvider} from '../../src/components/withWindowDimensions'; import CONST from '../../src/CONST'; import ONYXKEYS from '../../src/ONYXKEYS'; import createCollection from '../utils/collections/createCollection'; @@ -81,7 +77,7 @@ afterEach(() => { function SearchPageWrapper(args) { return ( - + { .then(() => measurePerformance(, {scenario, runs})); }); -test.skip('[Search Page] should search in options list', async () => { +test('[Search Page] should search in options list', async () => { const {triggerTransitionEnd, addListener} = TestHelper.createAddListenerMock(); const scenario = async () => { @@ -156,10 +152,6 @@ test.skip('[Search Page] should search in options list', async () => { fireEvent.changeText(input, mockedPersonalDetails['88'].login); await act(triggerTransitionEnd); await screen.findByText(mockedPersonalDetails['88'].login); - - fireEvent.changeText(input, mockedPersonalDetails['45'].login); - await act(triggerTransitionEnd); - await screen.findByText(mockedPersonalDetails['45'].login); }; const navigation = {addListener}; @@ -177,16 +169,16 @@ test.skip('[Search Page] should search in options list', async () => { .then(() => measurePerformance(, {scenario, runs})); }); -test.skip('[Search Page] should click on list item', async () => { +test('[Search Page] should click on list item', async () => { const {triggerTransitionEnd, addListener} = TestHelper.createAddListenerMock(); const scenario = async () => { await screen.findByTestId('SearchPage'); const input = screen.getByTestId('options-selector-input'); - fireEvent.changeText(input, mockedPersonalDetails['6'].login); + fireEvent.changeText(input, mockedPersonalDetails['4'].login); await act(triggerTransitionEnd); - const optionButton = await screen.findByText(mockedPersonalDetails['6'].login); + const optionButton = await screen.findByText(mockedPersonalDetails['4'].login); fireEvent.press(optionButton); }; From 8479f4f20580acdf20eefda84c7ddf2a270de4cc Mon Sep 17 00:00:00 2001 From: Tomasz Lesniakiewicz Date: Wed, 24 Jan 2024 15:16:47 +0100 Subject: [PATCH 342/391] docs: update readme --- tests/perf-test/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/perf-test/README.md b/tests/perf-test/README.md index a9b1643d191d..35af9dc35b7d 100644 --- a/tests/perf-test/README.md +++ b/tests/perf-test/README.md @@ -60,6 +60,7 @@ We use Reassure for monitoring performance regression. It helps us check if our - Investigate the code changes that might be causing this and address them to maintain a stable render count. More info [here](https://github.com/Expensify/App/blob/fe9e9e3e31bae27c2398678aa632e808af2690b5/tests/perf-test/README.md?plain=1#L32). - It is important to run Reassure tests locally and see if our changes caused a regression. + - One of the potential factors that may influence variation in the number of renders is adding unnecesary providers to the component we want to test using `````` . Ensure that all providers are necessary for running the test. ## What can be tested (scenarios) From cf1ca25e405ca9accd8980f9f996e6a9294a0c96 Mon Sep 17 00:00:00 2001 From: maddylewis <38016013+maddylewis@users.noreply.github.com> Date: Wed, 24 Jan 2024 11:42:53 -0500 Subject: [PATCH 343/391] Delete docs/articles/expensify-classic/getting-started/approved-accountants/Card-Revenue-Share-For-Expensify-Approved-Partners.md Moving this info to a new section of the help site / deleting for now. --- ...nue-Share-For-Expensify-Approved-Partners.md | 17 ----------------- 1 file changed, 17 deletions(-) delete mode 100644 docs/articles/expensify-classic/getting-started/approved-accountants/Card-Revenue-Share-For-Expensify-Approved-Partners.md diff --git a/docs/articles/expensify-classic/getting-started/approved-accountants/Card-Revenue-Share-For-Expensify-Approved-Partners.md b/docs/articles/expensify-classic/getting-started/approved-accountants/Card-Revenue-Share-For-Expensify-Approved-Partners.md deleted file mode 100644 index 189ff671b213..000000000000 --- a/docs/articles/expensify-classic/getting-started/approved-accountants/Card-Revenue-Share-For-Expensify-Approved-Partners.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -title: Expensify Card revenue share for ExpensifyApproved! partners -description: Earn money when your clients adopt the Expensify Card -redirect_from: articles/other/Card-Revenue-Share-for-ExpensifyApproved!-Partners/ ---- - - -Start making more with us! We're thrilled to announce a new incentive for our US-based ExpensifyApproved! partner accountants. You can now earn additional income for your firm every time your client uses their Expensify Card. **In short, your firm gets 0.5% of your clients’ total Expensify Card spend as cash back**. The more your clients spend, the more cashback your firm receives!
-
This program is currently only available to US-based ExpensifyApproved! partner accountants. - -# How-to -To benefit from this program, all you need to do is ensure that you are listed as a domain admin on your client's Expensify account. If you're not currently a domain admin, your client can follow the instructions outlined in [our help article](https://community.expensify.com/discussion/5749/how-to-add-and-remove-domain-admins#:~:text=Domain%20Admins%20have%20total%20control,a%20member%20of%20the%20domain.) to assign you this role. -{% include faq-begin.md %} -- What if my firm is not permitted to accept revenue share from our clients?
-
We understand that different firms may have different policies. If your firm is unable to accept this revenue share, you can pass the revenue share back to your client to give them an additional 0.5% of cash back using your own internal payment tools.

-- What if my firm does not wish to participate in the program?
-
Please reach out to your assigned partner manager at new.expensify.com to inform them you would not like to accept the revenue share nor do you want to pass the revenue share to your clients. \ No newline at end of file From 8ad78dcc023332f935b280a0b35a86d13667ce20 Mon Sep 17 00:00:00 2001 From: OSBotify Date: Wed, 24 Jan 2024 16:58:18 +0000 Subject: [PATCH 344/391] Update version to 1.4.31-3 --- android/app/build.gradle | 4 ++-- ios/NewExpensify/Info.plist | 2 +- ios/NewExpensifyTests/Info.plist | 2 +- ios/NotificationServiceExtension/Info.plist | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 49f1b017d5e0..4ba3eaad3b62 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -98,8 +98,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1001043102 - versionName "1.4.31-2" + versionCode 1001043103 + versionName "1.4.31-3" } flavorDimensions "default" diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index 34341662d137..e19e6d1b7da4 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -40,7 +40,7 @@ CFBundleVersion - 1.4.31.2 + 1.4.31.3 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 38073f64d814..b9cf15046c3f 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -19,6 +19,6 @@ CFBundleSignature ???? CFBundleVersion - 1.4.31.2 + 1.4.31.3 diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index 8550e23db7b1..8d4d3d5e2083 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -5,7 +5,7 @@ CFBundleShortVersionString 1.4.31 CFBundleVersion - 1.4.31.2 + 1.4.31.3 NSExtension NSExtensionPointIdentifier diff --git a/package-lock.json b/package-lock.json index f05837e853ba..799fa956d263 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "1.4.31-2", + "version": "1.4.31-3", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.4.31-2", + "version": "1.4.31-3", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 2ba358b438e6..e9449a288220 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.4.31-2", + "version": "1.4.31-3", "author": "Expensify, Inc.", "homepage": "https://new.expensify.com", "description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.", From b0e2a92a3d856109b6cce916370ec8435770e2af Mon Sep 17 00:00:00 2001 From: OSBotify Date: Wed, 24 Jan 2024 17:09:51 +0000 Subject: [PATCH 345/391] Update version to 1.4.31-4 --- android/app/build.gradle | 4 ++-- ios/NewExpensify/Info.plist | 2 +- ios/NewExpensifyTests/Info.plist | 2 +- ios/NotificationServiceExtension/Info.plist | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 4ba3eaad3b62..759abc31c058 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -98,8 +98,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1001043103 - versionName "1.4.31-3" + versionCode 1001043104 + versionName "1.4.31-4" } flavorDimensions "default" diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index e19e6d1b7da4..31e13ef3d283 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -40,7 +40,7 @@ CFBundleVersion - 1.4.31.3 + 1.4.31.4 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index b9cf15046c3f..557832679cd6 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -19,6 +19,6 @@ CFBundleSignature ???? CFBundleVersion - 1.4.31.3 + 1.4.31.4 diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index 8d4d3d5e2083..6d1c222e3ab9 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -5,7 +5,7 @@ CFBundleShortVersionString 1.4.31 CFBundleVersion - 1.4.31.3 + 1.4.31.4 NSExtension NSExtensionPointIdentifier diff --git a/package-lock.json b/package-lock.json index 799fa956d263..be3f8f18bff3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "1.4.31-3", + "version": "1.4.31-4", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.4.31-3", + "version": "1.4.31-4", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index e9449a288220..255261713c0a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.4.31-3", + "version": "1.4.31-4", "author": "Expensify, Inc.", "homepage": "https://new.expensify.com", "description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.", From 18a10b909d3a122ff35015b372cd5a514aa974e9 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Wed, 24 Jan 2024 18:16:07 +0100 Subject: [PATCH 346/391] Fix that user wasn't able to focus errors in the form --- src/components/Form/FormProvider.tsx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/components/Form/FormProvider.tsx b/src/components/Form/FormProvider.tsx index b71b611e60e5..424fd989291a 100644 --- a/src/components/Form/FormProvider.tsx +++ b/src/components/Form/FormProvider.tsx @@ -234,11 +234,9 @@ function FormProvider( ...inputProps, ref: typeof inputRef === 'function' - ? (node) => { + ? (node: BaseInputProps) => { inputRef(node); - if (node && typeof newRef !== 'function') { - newRef.current = node; - } + newRef.current = node; } : newRef, inputID, From 6f6474cf7d6c18cfc45fafa35d101e1989eda898 Mon Sep 17 00:00:00 2001 From: maddylewis <38016013+maddylewis@users.noreply.github.com> Date: Wed, 24 Jan 2024 12:24:34 -0500 Subject: [PATCH 347/391] Delete docs/articles/expensify-classic/getting-started/approved-accountants/Your-Expensify-Partner-Manager.md This resource already exists under the Expensify Partner Program category. We can delete this one. --- .../Your-Expensify-Partner-Manager.md | 37 ------------------- 1 file changed, 37 deletions(-) delete mode 100644 docs/articles/expensify-classic/getting-started/approved-accountants/Your-Expensify-Partner-Manager.md diff --git a/docs/articles/expensify-classic/getting-started/approved-accountants/Your-Expensify-Partner-Manager.md b/docs/articles/expensify-classic/getting-started/approved-accountants/Your-Expensify-Partner-Manager.md deleted file mode 100644 index fb3cb5341f61..000000000000 --- a/docs/articles/expensify-classic/getting-started/approved-accountants/Your-Expensify-Partner-Manager.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -title: Your Expensify Partner Manager -description: Everything you need to know about your Expensify Partner Manager -redirect_from: articles/other/Your-Expensify-Partner-Manager/ ---- - - -# What is a Partner Manager? -A Partner Manager is a dedicated point of contact to support our ExpensifyApproved! Accountants with questions about their Expensify account. Partner Managers support our accounting partners by providing recommendations for client's accounts, assisting with firm-wide training, and ensuring partners receive the full benefits of our partnership program. They will actively monitor open technical issues and be proactive with recommendations to increase efficiency and minimize time spent on expense management. - -Unlike Concierge, a Partner Manager’s support will not be real-time, 24 hours a day. A benefit of Concierge is that you get real-time support every day. Your partner manager will be super responsive when online, but anything sent when they’re offline will not be responded to until they’re online again. - -For real-time responses and simple troubleshooting issues, you can always message our general support by writing to Concierge via the in-product chat or by emailing concierge@expensify.com. - -# How do I know if I have a Partner Manager? -For your firm to be assigned a Partner Manager, you must complete the [ExpensifyApproved! University](https://use.expensify.com/accountants) training course. Every external accountant or bookkeeper who completes the training is automatically enrolled in our program and receives all the benefits, including access to the Partner Manager. So everyone at your firm must complete the training to receive the maximum benefit. - -You can check to see if you’ve completed the course and enrolled in the ExpensifyApproved! Accountants program simply by logging into your Expensify account. In the bottom left-hand corner of the website, you will see the ExpensifyApproved! logo. - -# How do I contact my Partner Manager? -You can contact your Partner Manager by: -- Signing in to new.expensify.com and searching for your Partner Manager -- Replying to or clicking the chat link on any email you get from your Partner Manager - -{% include faq-begin.md %} -## How do I know if my Partner Manager is online? -You will be able to see if they are online via their status in new.expensify.com, which will either say “online” or have their working hours. - -## What if I’m unable to reach my Partner Manager? -If you’re unable to contact your Partner Manager (i.e., they're out of office for the day) you can reach out to Concierge for assistance. Your Partner Manager will get back to you when they’re online again. - -## Can I get on a call with my Partner Manager? -Of course! You can ask your Partner Manager to schedule a call whenever you think one might be helpful. Partner Managers can discuss client onboarding strategies, firm wide training, and client setups. - -We recommend continuing to work with Concierge for **general support questions**, as this team is always online and available to help immediately. - -{% include faq-end.md %} From 0ce476092a6efa36fba45983869064299c6e6b0f Mon Sep 17 00:00:00 2001 From: Brandon Stites Date: Wed, 24 Jan 2024 11:57:02 -0700 Subject: [PATCH 348/391] Update copy --- src/languages/en.ts | 2 +- src/languages/es.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index 46d9e90e84d9..8a959b5da550 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -583,7 +583,7 @@ export default { canceled: 'Canceled', posted: 'Posted', deleteReceipt: 'Delete receipt', - receiptScanning: 'Receipt scan in progress…', + receiptScanning: 'Scan in progress…', receiptMissingDetails: 'Receipt missing details', receiptStatusTitle: 'Scanning…', receiptStatusText: "Only you can see this receipt when it's scanning. Check back later or enter the details now.", diff --git a/src/languages/es.ts b/src/languages/es.ts index db010d3266c2..271e564c9b1f 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -575,7 +575,7 @@ export default { canceled: 'Canceló', posted: 'Contabilizado', deleteReceipt: 'Eliminar recibo', - receiptScanning: 'Escaneo de recibo en curso…', + receiptScanning: 'Escaneo en curso…', receiptMissingDetails: 'Recibo con campos vacíos', receiptStatusTitle: 'Escaneando…', receiptStatusText: 'Solo tú puedes ver este recibo cuando se está escaneando. Vuelve más tarde o introduce los detalles ahora.', From 25b802e9c3ad886c2255dc286ff01ad6d11c3687 Mon Sep 17 00:00:00 2001 From: Cristi Paval Date: Wed, 24 Jan 2024 22:22:16 +0200 Subject: [PATCH 349/391] Default shouldShowLoading to false in WorkspacePageWithSections --- src/pages/workspace/WorkspacePageWithSections.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/workspace/WorkspacePageWithSections.tsx b/src/pages/workspace/WorkspacePageWithSections.tsx index 8817f813a990..46fa8f14fac7 100644 --- a/src/pages/workspace/WorkspacePageWithSections.tsx +++ b/src/pages/workspace/WorkspacePageWithSections.tsx @@ -84,7 +84,7 @@ function WorkspacePageWithSections({ shouldUseScrollView = false, shouldSkipVBBACall = false, user, - shouldShowLoading = true, + shouldShowLoading = false, }: WorkspacePageWithSectionsProps) { const styles = useThemeStyles(); useNetwork({onReconnect: () => fetchData(shouldSkipVBBACall)}); From ee1b7c826c6e5c75cfb6035f192ce6fd1641d858 Mon Sep 17 00:00:00 2001 From: Andrew Rosiclair Date: Wed, 24 Jan 2024 15:33:34 -0500 Subject: [PATCH 350/391] Revert "Merge pull request #34237 from FitseTLT/fix-include-current-user-on-members-list" This reverts commit 5d41d84a6a24910affaf224351971991a177e034, reversing changes made to c0fbb9798af00257b3d2fb504be63d797a622087. --- src/libs/ReportUtils.ts | 146 ++++++++++++---------------- src/pages/ReportParticipantsPage.js | 3 +- src/pages/home/HeaderView.js | 2 +- 3 files changed, 66 insertions(+), 85 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 1d888b087e53..e9c3b1710cc0 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -1460,84 +1460,6 @@ function getWorkspaceIcon(report: OnyxEntry, policy: OnyxEntry = return workspaceIcon; } -/** - * Checks if a report is a group chat. - * - * A report is a group chat if it meets the following conditions: - * - Not a chat thread. - * - Not a task report. - * - Not a money request / IOU report. - * - Not an archived room. - * - Not a public / admin / announce chat room (chat type doesn't match any of the specified types). - * - More than 1 participant (note that participantAccountIDs excludes the current user). - * - */ -function isGroupChat(report: OnyxEntry): boolean { - return Boolean( - report && - !isChatThread(report) && - !isTaskReport(report) && - !isMoneyRequestReport(report) && - !isArchivedRoom(report) && - !Object.values(CONST.REPORT.CHAT_TYPE).some((chatType) => chatType === getChatType(report)) && - (report.participantAccountIDs?.length ?? 0) > 1, - ); -} - -function getGroupChatParticipantIDs(participants: number[]): number[] { - return [...new Set([...participants, ...(currentUserAccountID ? [currentUserAccountID] : [])])]; -} - -/** - * Returns an array of the participants Ids of a report - * - * @deprecated Use getVisibleMemberIDs instead - */ -function getParticipantsIDs(report: OnyxEntry): number[] { - if (!report) { - return []; - } - - const participants = report.participantAccountIDs ?? []; - - // Build participants list for IOU/expense reports - if (isMoneyRequestReport(report)) { - const onlyTruthyValues = [report.managerID, report.ownerAccountID, ...participants].filter(Boolean) as number[]; - const onlyUnique = [...new Set([...onlyTruthyValues])]; - return onlyUnique; - } - - if (isGroupChat(report)) { - return getGroupChatParticipantIDs(participants); - } - - return participants; -} - -/** - * Returns an array of the visible member accountIDs for a report - */ -function getVisibleMemberIDs(report: OnyxEntry): number[] { - if (!report) { - return []; - } - - const visibleChatMemberAccountIDs = report.visibleChatMemberAccountIDs ?? []; - - // Build participants list for IOU/expense reports - if (isMoneyRequestReport(report)) { - const onlyTruthyValues = [report.managerID, report.ownerAccountID, ...visibleChatMemberAccountIDs].filter(Boolean) as number[]; - const onlyUnique = [...new Set([...onlyTruthyValues])]; - return onlyUnique; - } - - if (isGroupChat(report)) { - return getGroupChatParticipantIDs(visibleChatMemberAccountIDs); - } - - return visibleChatMemberAccountIDs; -} - /** * Returns the appropriate icons for the given chat report using the stored personalDetails. * The Avatar sources can be URLs or Icon components according to the chat type. @@ -1654,10 +1576,6 @@ function getIcons( return isPayer ? [managerIcon, ownerIcon] : [ownerIcon, managerIcon]; } - if (isGroupChat(report)) { - return getIconsForParticipants(getVisibleMemberIDs(report), personalDetails); - } - return getIconsForParticipants(report?.participantAccountIDs ?? [], personalDetails); } @@ -4449,6 +4367,46 @@ function getTaskAssigneeChatOnyxData( }; } +/** + * Returns an array of the participants Ids of a report + * + * @deprecated Use getVisibleMemberIDs instead + */ +function getParticipantsIDs(report: OnyxEntry): number[] { + if (!report) { + return []; + } + + const participants = report.participantAccountIDs ?? []; + + // Build participants list for IOU/expense reports + if (isMoneyRequestReport(report)) { + const onlyTruthyValues = [report.managerID, report.ownerAccountID, ...participants].filter(Boolean) as number[]; + const onlyUnique = [...new Set([...onlyTruthyValues])]; + return onlyUnique; + } + return participants; +} + +/** + * Returns an array of the visible member accountIDs for a report* + */ +function getVisibleMemberIDs(report: OnyxEntry): number[] { + if (!report) { + return []; + } + + const visibleChatMemberAccountIDs = report.visibleChatMemberAccountIDs ?? []; + + // Build participants list for IOU/expense reports + if (isMoneyRequestReport(report)) { + const onlyTruthyValues = [report.managerID, report.ownerAccountID, ...visibleChatMemberAccountIDs].filter(Boolean) as number[]; + const onlyUnique = [...new Set([...onlyTruthyValues])]; + return onlyUnique; + } + return visibleChatMemberAccountIDs; +} + /** * Return iou report action display message */ @@ -4505,6 +4463,30 @@ function getIOUReportActionDisplayMessage(reportAction: OnyxEntry) }); } +/** + * Checks if a report is a group chat. + * + * A report is a group chat if it meets the following conditions: + * - Not a chat thread. + * - Not a task report. + * - Not a money request / IOU report. + * - Not an archived room. + * - Not a public / admin / announce chat room (chat type doesn't match any of the specified types). + * - More than 2 participants. + * + */ +function isGroupChat(report: OnyxEntry): boolean { + return Boolean( + report && + !isChatThread(report) && + !isTaskReport(report) && + !isMoneyRequestReport(report) && + !isArchivedRoom(report) && + !Object.values(CONST.REPORT.CHAT_TYPE).some((chatType) => chatType === getChatType(report)) && + (report.participantAccountIDs?.length ?? 0) > 2, + ); +} + function shouldUseFullTitleToDisplay(report: OnyxEntry): boolean { return isMoneyRequestReport(report) || isPolicyExpenseChat(report) || isChatRoom(report) || isChatThread(report) || isTaskReport(report); } diff --git a/src/pages/ReportParticipantsPage.js b/src/pages/ReportParticipantsPage.js index 65238fd5ea8c..7dbc1c7036c4 100755 --- a/src/pages/ReportParticipantsPage.js +++ b/src/pages/ReportParticipantsPage.js @@ -100,8 +100,7 @@ function ReportParticipantsPage(props) { 1; const displayNamesWithTooltips = ReportUtils.getDisplayNamesWithTooltips(participantPersonalDetails, isMultipleParticipant); From 1ba471fca14b8ff5a6a8a59991233afd5c393fd2 Mon Sep 17 00:00:00 2001 From: Cristi Paval Date: Wed, 24 Jan 2024 22:43:13 +0200 Subject: [PATCH 351/391] revert bad initial change --- src/pages/workspace/WorkspacePageWithSections.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/pages/workspace/WorkspacePageWithSections.tsx b/src/pages/workspace/WorkspacePageWithSections.tsx index 46fa8f14fac7..67c31c5329a8 100644 --- a/src/pages/workspace/WorkspacePageWithSections.tsx +++ b/src/pages/workspace/WorkspacePageWithSections.tsx @@ -84,7 +84,7 @@ function WorkspacePageWithSections({ shouldUseScrollView = false, shouldSkipVBBACall = false, user, - shouldShowLoading = false, + shouldShowLoading = true, }: WorkspacePageWithSectionsProps) { const styles = useThemeStyles(); useNetwork({onReconnect: () => fetchData(shouldSkipVBBACall)}); @@ -115,6 +115,10 @@ function WorkspacePageWithSections({ return !PolicyUtils.isPolicyAdmin(policy) || PolicyUtils.isPendingDeletePolicy(policy); }, [policy]); + console.debug("CRISTI - isLoading: " + isLoading); + console.debug("CRISTI - firstRender.current: " + firstRender.current); + console.debug("CRISTI - shouldShowLoading: " + shouldShowLoading); + return ( Date: Wed, 24 Jan 2024 22:54:11 +0200 Subject: [PATCH 352/391] Fix deploy blocker --- src/pages/workspace/WorkspacePageWithSections.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/workspace/WorkspacePageWithSections.tsx b/src/pages/workspace/WorkspacePageWithSections.tsx index 67c31c5329a8..d516f024aef0 100644 --- a/src/pages/workspace/WorkspacePageWithSections.tsx +++ b/src/pages/workspace/WorkspacePageWithSections.tsx @@ -79,7 +79,7 @@ function WorkspacePageWithSections({ guidesCallTaskID = '', headerText, policy, - reimbursementAccount = {}, + reimbursementAccount = {isLoading: false}, route, shouldUseScrollView = false, shouldSkipVBBACall = false, From fd6abead14a08df9d8d7814fc758d4476789db12 Mon Sep 17 00:00:00 2001 From: Cristi Paval Date: Wed, 24 Jan 2024 22:56:08 +0200 Subject: [PATCH 353/391] Remove console debug --- src/pages/workspace/WorkspacePageWithSections.tsx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/pages/workspace/WorkspacePageWithSections.tsx b/src/pages/workspace/WorkspacePageWithSections.tsx index d516f024aef0..c9c371a6a0c7 100644 --- a/src/pages/workspace/WorkspacePageWithSections.tsx +++ b/src/pages/workspace/WorkspacePageWithSections.tsx @@ -115,10 +115,6 @@ function WorkspacePageWithSections({ return !PolicyUtils.isPolicyAdmin(policy) || PolicyUtils.isPendingDeletePolicy(policy); }, [policy]); - console.debug("CRISTI - isLoading: " + isLoading); - console.debug("CRISTI - firstRender.current: " + firstRender.current); - console.debug("CRISTI - shouldShowLoading: " + shouldShowLoading); - return ( Date: Wed, 24 Jan 2024 23:16:37 +0200 Subject: [PATCH 354/391] make currentStep option in ACHData --- src/types/onyx/ReimbursementAccount.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types/onyx/ReimbursementAccount.ts b/src/types/onyx/ReimbursementAccount.ts index c0ade25e4d79..8ab10e79cb09 100644 --- a/src/types/onyx/ReimbursementAccount.ts +++ b/src/types/onyx/ReimbursementAccount.ts @@ -8,7 +8,7 @@ type BankAccountSubStep = ValueOf; type ACHData = { /** Step of the setup flow that we are on. Determines which view is presented. */ - currentStep: BankAccountStep; + currentStep?: BankAccountStep; /** Optional subStep we would like the user to start back on */ subStep?: BankAccountSubStep; From ba79d4e78db87dd9208d312796c1b47d226a9f8c Mon Sep 17 00:00:00 2001 From: Cristi Paval Date: Wed, 24 Jan 2024 23:26:39 +0200 Subject: [PATCH 355/391] Use ReimbursementAccountProps.reimbursementAccountDefaultProps --- src/pages/workspace/WorkspacePageWithSections.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/pages/workspace/WorkspacePageWithSections.tsx b/src/pages/workspace/WorkspacePageWithSections.tsx index c9c371a6a0c7..8b98d29245d5 100644 --- a/src/pages/workspace/WorkspacePageWithSections.tsx +++ b/src/pages/workspace/WorkspacePageWithSections.tsx @@ -22,6 +22,7 @@ import type {Policy, ReimbursementAccount, User} from '@src/types/onyx'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import type {WithPolicyAndFullscreenLoadingProps} from './withPolicyAndFullscreenLoading'; import withPolicyAndFullscreenLoading from './withPolicyAndFullscreenLoading'; +import * as ReimbursementAccountProps from '@pages/ReimbursementAccount/reimbursementAccountPropTypes'; type WorkspacePageWithSectionsOnyxProps = { /** From Onyx */ @@ -79,7 +80,7 @@ function WorkspacePageWithSections({ guidesCallTaskID = '', headerText, policy, - reimbursementAccount = {isLoading: false}, + reimbursementAccount = ReimbursementAccountProps.reimbursementAccountDefaultProps, route, shouldUseScrollView = false, shouldSkipVBBACall = false, From 20f70c64b4aa61f6dacc0c90ad92e09eb68ee2b3 Mon Sep 17 00:00:00 2001 From: Cristi Paval Date: Wed, 24 Jan 2024 23:27:21 +0200 Subject: [PATCH 356/391] Run prettier --- src/pages/workspace/WorkspacePageWithSections.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/workspace/WorkspacePageWithSections.tsx b/src/pages/workspace/WorkspacePageWithSections.tsx index 8b98d29245d5..7a4d9c1f4106 100644 --- a/src/pages/workspace/WorkspacePageWithSections.tsx +++ b/src/pages/workspace/WorkspacePageWithSections.tsx @@ -14,6 +14,7 @@ import useThemeStyles from '@hooks/useThemeStyles'; import BankAccount from '@libs/models/BankAccount'; import Navigation from '@libs/Navigation/Navigation'; import * as PolicyUtils from '@libs/PolicyUtils'; +import * as ReimbursementAccountProps from '@pages/ReimbursementAccount/reimbursementAccountPropTypes'; import * as BankAccounts from '@userActions/BankAccounts'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; @@ -22,7 +23,6 @@ import type {Policy, ReimbursementAccount, User} from '@src/types/onyx'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import type {WithPolicyAndFullscreenLoadingProps} from './withPolicyAndFullscreenLoading'; import withPolicyAndFullscreenLoading from './withPolicyAndFullscreenLoading'; -import * as ReimbursementAccountProps from '@pages/ReimbursementAccount/reimbursementAccountPropTypes'; type WorkspacePageWithSectionsOnyxProps = { /** From Onyx */ From 9a9ccab2051f36d3d2a3cfde847976919b9db4aa Mon Sep 17 00:00:00 2001 From: Cristi Paval Date: Wed, 24 Jan 2024 23:46:38 +0200 Subject: [PATCH 357/391] Use report.reportID when updating notification preferences --- src/pages/settings/Report/NotificationPreferencePage.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/settings/Report/NotificationPreferencePage.js b/src/pages/settings/Report/NotificationPreferencePage.js index 488afa7c5a71..307a539942c7 100644 --- a/src/pages/settings/Report/NotificationPreferencePage.js +++ b/src/pages/settings/Report/NotificationPreferencePage.js @@ -43,7 +43,7 @@ function NotificationPreferencePage(props) { /> Report.updateNotificationPreference(props.reportID, props.report.notificationPreference, option.value, true, undefined, undefined, props.report)} + onSelectRow={(option) => Report.updateNotificationPreference(props.report.reportID, props.report.notificationPreference, option.value, true, undefined, undefined, props.report)} initiallyFocusedOptionKey={_.find(notificationPreferenceOptions, (locale) => locale.isSelected).keyForList} /> From 00abcd8e8b41caea4d6ffec0add0d532f023bbe0 Mon Sep 17 00:00:00 2001 From: Cristi Paval Date: Wed, 24 Jan 2024 23:59:22 +0200 Subject: [PATCH 358/391] Run prettier --- src/pages/settings/Report/NotificationPreferencePage.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/pages/settings/Report/NotificationPreferencePage.js b/src/pages/settings/Report/NotificationPreferencePage.js index 307a539942c7..c6044bd81efe 100644 --- a/src/pages/settings/Report/NotificationPreferencePage.js +++ b/src/pages/settings/Report/NotificationPreferencePage.js @@ -43,7 +43,9 @@ function NotificationPreferencePage(props) { /> Report.updateNotificationPreference(props.report.reportID, props.report.notificationPreference, option.value, true, undefined, undefined, props.report)} + onSelectRow={(option) => + Report.updateNotificationPreference(props.report.reportID, props.report.notificationPreference, option.value, true, undefined, undefined, props.report) + } initiallyFocusedOptionKey={_.find(notificationPreferenceOptions, (locale) => locale.isSelected).keyForList} /> From 149eda177f73fd9e6c0c214652d8a6533d543638 Mon Sep 17 00:00:00 2001 From: OSBotify Date: Wed, 24 Jan 2024 22:01:11 +0000 Subject: [PATCH 359/391] Update version to 1.4.31-5 --- android/app/build.gradle | 4 ++-- ios/NewExpensify/Info.plist | 2 +- ios/NewExpensifyTests/Info.plist | 2 +- ios/NotificationServiceExtension/Info.plist | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 759abc31c058..ac2399b76b00 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -98,8 +98,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1001043104 - versionName "1.4.31-4" + versionCode 1001043105 + versionName "1.4.31-5" } flavorDimensions "default" diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index 31e13ef3d283..c3fe8e5b3e84 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -40,7 +40,7 @@ CFBundleVersion - 1.4.31.4 + 1.4.31.5 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 557832679cd6..3fe876e853a9 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -19,6 +19,6 @@ CFBundleSignature ???? CFBundleVersion - 1.4.31.4 + 1.4.31.5 diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index 6d1c222e3ab9..4df7a4feae72 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -5,7 +5,7 @@ CFBundleShortVersionString 1.4.31 CFBundleVersion - 1.4.31.4 + 1.4.31.5 NSExtension NSExtensionPointIdentifier diff --git a/package-lock.json b/package-lock.json index be3f8f18bff3..471e9e8dd439 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "1.4.31-4", + "version": "1.4.31-5", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.4.31-4", + "version": "1.4.31-5", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 255261713c0a..01838db6bcf4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.4.31-4", + "version": "1.4.31-5", "author": "Expensify, Inc.", "homepage": "https://new.expensify.com", "description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.", From 08e9276da00387cc2352f5a748bec8175ecf526f Mon Sep 17 00:00:00 2001 From: OSBotify Date: Wed, 24 Jan 2024 22:51:36 +0000 Subject: [PATCH 360/391] Update version to 1.4.31-6 --- android/app/build.gradle | 4 ++-- ios/NewExpensify/Info.plist | 2 +- ios/NewExpensifyTests/Info.plist | 2 +- ios/NotificationServiceExtension/Info.plist | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index ac2399b76b00..ae418094ace6 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -98,8 +98,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1001043105 - versionName "1.4.31-5" + versionCode 1001043106 + versionName "1.4.31-6" } flavorDimensions "default" diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index c3fe8e5b3e84..9b1906646e2d 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -40,7 +40,7 @@ CFBundleVersion - 1.4.31.5 + 1.4.31.6 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 3fe876e853a9..78a6f4c0f922 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -19,6 +19,6 @@ CFBundleSignature ???? CFBundleVersion - 1.4.31.5 + 1.4.31.6 diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index 4df7a4feae72..91aa2b889fe7 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -5,7 +5,7 @@ CFBundleShortVersionString 1.4.31 CFBundleVersion - 1.4.31.5 + 1.4.31.6 NSExtension NSExtensionPointIdentifier diff --git a/package-lock.json b/package-lock.json index 471e9e8dd439..ab9d3f9c6967 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "1.4.31-5", + "version": "1.4.31-6", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.4.31-5", + "version": "1.4.31-6", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 01838db6bcf4..271909ef3f65 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.4.31-5", + "version": "1.4.31-6", "author": "Expensify, Inc.", "homepage": "https://new.expensify.com", "description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.", From 6dddf628419853577d05996abae5e38df9fc087e Mon Sep 17 00:00:00 2001 From: Cole Eason Date: Wed, 24 Jan 2024 15:44:48 -0800 Subject: [PATCH 361/391] Make redirects work for help and use dot --- .github/scripts/createHelpRedirects.sh | 8 ++++---- docs/redirects.csv | 12 ++++++------ 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/scripts/createHelpRedirects.sh b/.github/scripts/createHelpRedirects.sh index 04f55e19b4fb..1ae2220253c4 100755 --- a/.github/scripts/createHelpRedirects.sh +++ b/.github/scripts/createHelpRedirects.sh @@ -41,13 +41,13 @@ while read -r line; do # Basic sanity checking to make sure that the source and destination are in expected # subdomains. - if ! [[ $SOURCE_URL =~ ^https://community\.expensify\.com ]]; then - error "Found source URL that is not a community URL: $SOURCE_URL" + if ! [[ $SOURCE_URL =~ ^https://(community|help)\.expensify\.com ]]; then + error "Found source URL that is not a communityDot or helpDot URL: $SOURCE_URL" exit 1 fi - if ! [[ $DEST_URL =~ ^https://help\.expensify\.com ]]; then - error "Found destination URL that is not a help URL: $DEST_URL" + if ! [[ $DEST_URL =~ ^https://(help|use)\.expensify\.com ]]; then + error "Found destination URL that is not a helpDot or useDot URL: $DEST_URL" exit 1 fi diff --git a/docs/redirects.csv b/docs/redirects.csv index d3a7fdd695a3..74667e967f7f 100644 --- a/docs/redirects.csv +++ b/docs/redirects.csv @@ -24,9 +24,9 @@ https://community.expensify.com/discussion/5655/deep-dive-what-is-a-vacation-del https://community.expensify.com/discussion/5194/how-to-assign-a-vacation-delegate-for-an-employee-through-domains,https://help.expensify.com/articles/expensify-classic/manage-employees-and-report-approvals/Vacation-Delegate https://community.expensify.com/discussion/5190/how-to-individually-assign-a-vacation-delegate-from-account-settings,https://help.expensify.com/articles/expensify-classic/manage-employees-and-report-approvals/Vacation-Delegate https://community.expensify.com/discussion/5274/how-to-set-up-an-adp-indirect-integration-with-expensify,https://help.expensify.com/articles/expensify-classic/integrations/HR-integrations/ADP -https://community.expensify.com/discussion/5776/how-to-create-mileage-expenses-in-expensify,https://help.expensify.com/articles/expensify-classic/get-paid-back/Distance-Tracking#gsc.tab=0 -https://community.expensify.com/discussion/7385/how-to-enable-two-factor-authentication-in-your-account,https://help.expensify.com/articles/expensify-classic/account-settings/Account-Details#gsc.tab=0 -https://community.expensify.com/discussion/5124/how-to-add-your-name-and-photo-to-your-account,https://help.expensify.com/articles/expensify-classic/account-settings/Account-Details#gsc.tab=0 -https://community.expensify.com/discussion/5149/how-to-manage-your-devices-in-expensify,https://help.expensify.com/articles/expensify-classic/account-settings/Account-Details#gsc.tab=0 -https://community.expensify.com/discussion/4432/how-to-add-a-secondary-login,https://help.expensify.com/articles/expensify-classic/account-settings/Account-Details#gsc.tab=0 -https://community.expensify.com/discussion/6794/how-to-change-your-email-in-expensify,https://help.expensify.com/articles/expensify-classic/account-settings/Account-Details#gsc.tab=0 +https://community.expensify.com/discussion/5776/how-to-create-mileage-expenses-in-expensify,https://help.expensify.com/articles/expensify-classic/get-paid-back/Distance-Tracking +https://community.expensify.com/discussion/7385/how-to-enable-two-factor-authentication-in-your-account,https://help.expensify.com/articles/expensify-classic/account-settings/Account-Details +https://community.expensify.com/discussion/5124/how-to-add-your-name-and-photo-to-your-account,https://help.expensify.com/articles/expensify-classic/account-settings/Account-Details +https://community.expensify.com/discussion/5149/how-to-manage-your-devices-in-expensify,https://help.expensify.com/articles/expensify-classic/account-settings/Account-Details +https://community.expensify.com/discussion/4432/how-to-add-a-secondary-login,https://help.expensify.com/articles/expensify-classic/account-settings/Account-Details +https://community.expensify.com/discussion/6794/how-to-change-your-email-in-expensify,https://help.expensify.com/articles/expensify-classic/account-settings/Account-Details From 2d894d82889cfc1626831f344f272ed82ea19798 Mon Sep 17 00:00:00 2001 From: Wildan M Date: Thu, 25 Jan 2024 06:57:09 +0700 Subject: [PATCH 362/391] Fix selection handle hidden in mobile chrome --- src/components/TextInput/BaseTextInput/index.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/TextInput/BaseTextInput/index.tsx b/src/components/TextInput/BaseTextInput/index.tsx index 9c3899979aaa..97d2d0866479 100644 --- a/src/components/TextInput/BaseTextInput/index.tsx +++ b/src/components/TextInput/BaseTextInput/index.tsx @@ -435,6 +435,7 @@ function BaseTextInput( */} {(!!autoGrow || autoGrowHeight) && ( // Add +2 to width on Safari browsers so that text is not cut off due to the cursor or when changing the value + // For mobile Chrome, this will ensure that the text selection handle (blue bubble down) is shown. // https://github.com/Expensify/App/issues/8158 // https://github.com/Expensify/App/issues/26628 { let additionalWidth = 0; - if (Browser.isMobileSafari() || Browser.isSafari()) { + if (Browser.isMobileSafari() || Browser.isSafari() || Browser.isMobileChrome()) { additionalWidth = 2; } setTextInputWidth(e.nativeEvent.layout.width + additionalWidth); From a207382c11e2b8699d907b25fff38765c160dc0e Mon Sep 17 00:00:00 2001 From: Sophie Pinto-Raetz <42940078+sophiepintoraetz@users.noreply.github.com> Date: Thu, 25 Jan 2024 14:49:56 +1300 Subject: [PATCH 363/391] Update Set-Up-the-Card-for-Your-Company.md --- .../expensify-card/Set-Up-the-Card-for-Your-Company.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/articles/expensify-classic/expensify-card/Set-Up-the-Card-for-Your-Company.md b/docs/articles/expensify-classic/expensify-card/Set-Up-the-Card-for-Your-Company.md index 531648b10350..1cf29531f696 100644 --- a/docs/articles/expensify-classic/expensify-card/Set-Up-the-Card-for-Your-Company.md +++ b/docs/articles/expensify-classic/expensify-card/Set-Up-the-Card-for-Your-Company.md @@ -58,7 +58,7 @@ Applying for or using the Expensify Card will never have any positive or negativ ## How much does the Expensify Card cost? -The Expensify Card is a free corporate card, and no fees are associated with it. In addition, if you use the Expensify Card, you can save money on your Expensify subscription (based on how much of your approved spend happens on the Expensify Card, compared with other approved spend, in each month). +The Expensify Card is a free corporate card, and no fees are associated with it. In addition, if you use the Expensify Card, you can save money on your Expensify subscription (based on how much of your approved spend occurs on the Expensify Card, compared with other approved spend, in each month). ## If I have staff outside the US, can they use the Expensify Card? From bb2e0098b7bf4d213bc193ef010a8873a20acd00 Mon Sep 17 00:00:00 2001 From: OSBotify Date: Thu, 25 Jan 2024 01:56:08 +0000 Subject: [PATCH 364/391] Update version to 1.4.31-7 --- android/app/build.gradle | 4 ++-- ios/NewExpensify/Info.plist | 2 +- ios/NewExpensifyTests/Info.plist | 2 +- ios/NotificationServiceExtension/Info.plist | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index ae418094ace6..1872168bc2a0 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -98,8 +98,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1001043106 - versionName "1.4.31-6" + versionCode 1001043107 + versionName "1.4.31-7" } flavorDimensions "default" diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index 9b1906646e2d..a977be3acc70 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -40,7 +40,7 @@ CFBundleVersion - 1.4.31.6 + 1.4.31.7 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 78a6f4c0f922..2fb6a26d633d 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -19,6 +19,6 @@ CFBundleSignature ???? CFBundleVersion - 1.4.31.6 + 1.4.31.7 diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index 91aa2b889fe7..d438940a2d1b 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -5,7 +5,7 @@ CFBundleShortVersionString 1.4.31 CFBundleVersion - 1.4.31.6 + 1.4.31.7 NSExtension NSExtensionPointIdentifier diff --git a/package-lock.json b/package-lock.json index ab9d3f9c6967..2bfec0ea4577 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "1.4.31-6", + "version": "1.4.31-7", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.4.31-6", + "version": "1.4.31-7", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 271909ef3f65..7f09c49ad2bb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.4.31-6", + "version": "1.4.31-7", "author": "Expensify, Inc.", "homepage": "https://new.expensify.com", "description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.", From d84ba69d5ae43f1673e1b428a59fddb65905f9e2 Mon Sep 17 00:00:00 2001 From: Wildan Muhlis Date: Thu, 25 Jan 2024 09:48:13 +0700 Subject: [PATCH 365/391] Add relevant link for mobile chrome issue --- src/components/TextInput/BaseTextInput/index.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/TextInput/BaseTextInput/index.tsx b/src/components/TextInput/BaseTextInput/index.tsx index 97d2d0866479..585ca2a96eea 100644 --- a/src/components/TextInput/BaseTextInput/index.tsx +++ b/src/components/TextInput/BaseTextInput/index.tsx @@ -438,6 +438,7 @@ function BaseTextInput( // For mobile Chrome, this will ensure that the text selection handle (blue bubble down) is shown. // https://github.com/Expensify/App/issues/8158 // https://github.com/Expensify/App/issues/26628 + // https://github.com/Expensify/App/issues/34921 Date: Thu, 25 Jan 2024 10:42:11 +0700 Subject: [PATCH 366/391] Update src/components/TextInput/BaseTextInput/index.tsx Co-authored-by: Ishpaul Singh <104348397+ishpaul777@users.noreply.github.com> --- src/components/TextInput/BaseTextInput/index.tsx | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/components/TextInput/BaseTextInput/index.tsx b/src/components/TextInput/BaseTextInput/index.tsx index 585ca2a96eea..92db8d2fc5f1 100644 --- a/src/components/TextInput/BaseTextInput/index.tsx +++ b/src/components/TextInput/BaseTextInput/index.tsx @@ -435,10 +435,9 @@ function BaseTextInput( */} {(!!autoGrow || autoGrowHeight) && ( // Add +2 to width on Safari browsers so that text is not cut off due to the cursor or when changing the value - // For mobile Chrome, this will ensure that the text selection handle (blue bubble down) is shown. - // https://github.com/Expensify/App/issues/8158 - // https://github.com/Expensify/App/issues/26628 - // https://github.com/Expensify/App/issues/34921 + // Reference: https://github.com/Expensify/App/issues/8158, https://github.com/Expensify/App/issues/26628 + // For mobile Chrome, ensure proper display of the text selection handle (blue bubble down). + // Reference: https://github.com/Expensify/App/issues/34921``` Date: Thu, 25 Jan 2024 10:56:42 +0700 Subject: [PATCH 367/391] Remove unnecessary backtick --- src/components/TextInput/BaseTextInput/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/TextInput/BaseTextInput/index.tsx b/src/components/TextInput/BaseTextInput/index.tsx index 92db8d2fc5f1..685f7bceb732 100644 --- a/src/components/TextInput/BaseTextInput/index.tsx +++ b/src/components/TextInput/BaseTextInput/index.tsx @@ -437,7 +437,7 @@ function BaseTextInput( // Add +2 to width on Safari browsers so that text is not cut off due to the cursor or when changing the value // Reference: https://github.com/Expensify/App/issues/8158, https://github.com/Expensify/App/issues/26628 // For mobile Chrome, ensure proper display of the text selection handle (blue bubble down). - // Reference: https://github.com/Expensify/App/issues/34921``` + // Reference: https://github.com/Expensify/App/issues/34921 Date: Thu, 25 Jan 2024 11:05:56 +0700 Subject: [PATCH 368/391] Fix typecheck Signed-off-by: Tsaqif --- src/components/ThreeDotsMenu/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/ThreeDotsMenu/index.tsx b/src/components/ThreeDotsMenu/index.tsx index 3c90f4df813b..7384874a2746 100644 --- a/src/components/ThreeDotsMenu/index.tsx +++ b/src/components/ThreeDotsMenu/index.tsx @@ -71,7 +71,7 @@ function ThreeDotsMenu({ const theme = useTheme(); const styles = useThemeStyles(); const [isPopupMenuVisible, setPopupMenuVisible] = useState(false); - const buttonRef = useRef(null); + const buttonRef = useRef(null); const {translate} = useLocalize(); const showPopoverMenu = () => { @@ -92,7 +92,7 @@ function ThreeDotsMenu({ hidePopoverMenu(); return; } - buttonRef.current.blur(); + buttonRef.current?.blur(); showPopoverMenu(); if (onIconPress) { onIconPress(); From d64895f0258d260a9f0f6b96fda27a65c572f54c Mon Sep 17 00:00:00 2001 From: s-alves10 Date: Wed, 24 Jan 2024 22:30:34 -0600 Subject: [PATCH 369/391] fix: show parent action message or deleted message when original message doesn't exist --- src/libs/ReportUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index e9c3b1710cc0..fa51ca06e68e 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -2336,7 +2336,7 @@ function isChangeLogObject(originalMessage?: ChangeLog): ChangeLog | undefined { */ function getAdminRoomInvitedParticipants(parentReportAction: ReportAction | Record, parentReportActionMessage: string) { if (!parentReportAction?.originalMessage) { - return ''; + return parentReportActionMessage || Localize.translateLocal('parentReportAction.deletedMessage'); } const originalMessage = isChangeLogObject(parentReportAction.originalMessage); const participantAccountIDs = originalMessage?.targetAccountIDs ?? []; From 999d576e8673171cc4bde83d5ae0c662db7cb57a Mon Sep 17 00:00:00 2001 From: s-alves10 Date: Wed, 24 Jan 2024 23:03:57 -0600 Subject: [PATCH 370/391] fix: create child report with parent's policy name --- src/libs/actions/Report.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 36ac445a78d4..020c22cefae0 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -754,7 +754,7 @@ function navigateToAndOpenChildReport(childReportID = '0', parentReportAction: P parentReport?.policyID ?? CONST.POLICY.OWNER_EMAIL_FAKE, CONST.POLICY.OWNER_ACCOUNT_ID_FAKE, false, - '', + parentReport?.policyName ?? '', undefined, undefined, ReportUtils.getChildReportNotificationPreference(parentReportAction), From 71fa6ac36742ef9c60baed78318c8af2aa7dc62b Mon Sep 17 00:00:00 2001 From: OSBotify Date: Thu, 25 Jan 2024 08:39:41 +0000 Subject: [PATCH 371/391] Update version to 1.4.32-0 --- android/app/build.gradle | 4 ++-- ios/NewExpensify/Info.plist | 4 ++-- ios/NewExpensifyTests/Info.plist | 4 ++-- ios/NotificationServiceExtension/Info.plist | 4 ++-- package-lock.json | 4 ++-- package.json | 2 +- 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 1872168bc2a0..e1c404a73d4b 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -98,8 +98,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1001043107 - versionName "1.4.31-7" + versionCode 1001043200 + versionName "1.4.32-0" } flavorDimensions "default" diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index a977be3acc70..7092d3239ae3 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -19,7 +19,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.4.31 + 1.4.32 CFBundleSignature ???? CFBundleURLTypes @@ -40,7 +40,7 @@ CFBundleVersion - 1.4.31.7 + 1.4.32.0 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 2fb6a26d633d..a402788ad579 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 1.4.31 + 1.4.32 CFBundleSignature ???? CFBundleVersion - 1.4.31.7 + 1.4.32.0 diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index d438940a2d1b..58a90b1796e9 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -3,9 +3,9 @@ CFBundleShortVersionString - 1.4.31 + 1.4.32 CFBundleVersion - 1.4.31.7 + 1.4.32.0 NSExtension NSExtensionPointIdentifier diff --git a/package-lock.json b/package-lock.json index 2bfec0ea4577..ad9754c4184b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "1.4.31-7", + "version": "1.4.32-0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.4.31-7", + "version": "1.4.32-0", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 7f09c49ad2bb..ad48cc7093b9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.4.31-7", + "version": "1.4.32-0", "author": "Expensify, Inc.", "homepage": "https://new.expensify.com", "description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.", From 500833d8b73328db74c49b3ba0818bf007571268 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Thu, 25 Jan 2024 09:43:06 +0100 Subject: [PATCH 372/391] fix: typecheck --- src/libs/OptionsListUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 861c7a6d62ba..2621e4d7f12b 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -106,7 +106,7 @@ type MemberForList = { alternateText: string | null; keyForList: string | null; isSelected: boolean; - isDisabled: boolean; + isDisabled: boolean | null; accountID?: number | null; login: string | null; rightElement: React.ReactNode | null; From 22f73e22300f70e9fbe2fbb67bd0d3d59898a4ee Mon Sep 17 00:00:00 2001 From: tienifr Date: Thu, 25 Jan 2024 15:43:44 +0700 Subject: [PATCH 373/391] change to default export --- src/components/ImageView/types.ts | 2 +- src/components/MultiGestureCanvas/types.ts | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/components/ImageView/types.ts b/src/components/ImageView/types.ts index 9ea51fd3c82c..bf83bc44d47b 100644 --- a/src/components/ImageView/types.ts +++ b/src/components/ImageView/types.ts @@ -1,5 +1,5 @@ import type {StyleProp, ViewStyle} from 'react-native'; -import type {ZoomRange} from '@components/MultiGestureCanvas/types'; +import type ZoomRange from '@components/MultiGestureCanvas/types'; type ImageViewProps = { /** Whether source url requires authentication */ diff --git a/src/components/MultiGestureCanvas/types.ts b/src/components/MultiGestureCanvas/types.ts index 3c8480257700..0242f045feef 100644 --- a/src/components/MultiGestureCanvas/types.ts +++ b/src/components/MultiGestureCanvas/types.ts @@ -3,5 +3,4 @@ type ZoomRange = { max: number; }; -// eslint-disable-next-line import/prefer-default-export -export type {ZoomRange}; +export default ZoomRange; From bb2791476f98d116404ac012d27b8b259a69481a Mon Sep 17 00:00:00 2001 From: tienifr Date: Thu, 25 Jan 2024 16:07:17 +0700 Subject: [PATCH 374/391] lint fix --- .../HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.js b/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.js index bae4258461a7..3646d9148b3a 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.js +++ b/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.js @@ -47,7 +47,7 @@ function MentionUserRenderer(props) { if (userAccountID && userLogin !== displayText) { return displayText; } - + // If the emails are not in the same private domain, we also return the displayText if (!LoginUtils.areEmailsFromSamePrivateDomain(displayText, props.currentUserPersonalDetails.login)) { return displayText; From 691fef8073ccbfb2dfe07b323b0915bb5a7551f7 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Thu, 25 Jan 2024 10:15:34 +0100 Subject: [PATCH 375/391] fix: typecheck --- src/components/LHNOptionsList/LHNOptionsList.tsx | 3 +-- src/components/LHNOptionsList/types.ts | 8 +++++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/components/LHNOptionsList/LHNOptionsList.tsx b/src/components/LHNOptionsList/LHNOptionsList.tsx index ecf320807b48..28b8e253800d 100644 --- a/src/components/LHNOptionsList/LHNOptionsList.tsx +++ b/src/components/LHNOptionsList/LHNOptionsList.tsx @@ -48,7 +48,7 @@ function LHNOptionsList({ 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 participants = [...ReportUtils.getParticipantsIDs(itemFullReport), itemFullReport?.ownerAccountID, itemParentReportAction?.actorAccountID]; + const participants = [...ReportUtils.getParticipantsIDs(itemFullReport), itemFullReport?.ownerAccountID, itemParentReportAction?.actorAccountID].filter(Boolean) as number[]; const participantsPersonalDetails = OptionsListUtils.getPersonalDetailsForAccountIDs(participants, personalDetails); return ( @@ -58,7 +58,6 @@ function LHNOptionsList({ reportActions={itemReportActions} parentReportAction={itemParentReportAction} policy={itemPolicy} - // @ts-expect-error TODO: Remove this once OptionsListUtils (https://github.com/Expensify/App/issues/24921) is migrated to TypeScript. personalDetails={participantsPersonalDetails} transaction={itemTransaction} receiptTransactions={transactions} diff --git a/src/components/LHNOptionsList/types.ts b/src/components/LHNOptionsList/types.ts index 24cebb8e3da2..67033e02f51b 100644 --- a/src/components/LHNOptionsList/types.ts +++ b/src/components/LHNOptionsList/types.ts @@ -47,7 +47,7 @@ type CustomLHNOptionsListProps = { data: string[]; /** Callback to fire when a row is selected */ - onSelectRow: (reportID: string) => void; + onSelectRow?: (optionItem: OptionData, popoverAnchor: RefObject) => void; /** Toggle between compact and default view of the option */ optionMode: OptionMode; @@ -97,6 +97,12 @@ type OptionRowLHNDataProps = { /** Whether the user can use violations */ canUseViolations: boolean | undefined; + + /** Toggle between compact and default view */ + viewMode?: OptionMode; + + /** A function that is called when an option is selected. Selected option is passed as a param */ + onSelectRow?: (optionItem: OptionData, popoverAnchor: RefObject) => void; }; type OptionRowLHNProps = { From cccb89a12b2f6a901a6326fe9f17c9646f54e4cf Mon Sep 17 00:00:00 2001 From: OSBotify Date: Thu, 25 Jan 2024 09:16:22 +0000 Subject: [PATCH 376/391] Update version to 1.4.32-1 --- android/app/build.gradle | 4 ++-- ios/NewExpensify/Info.plist | 2 +- ios/NewExpensifyTests/Info.plist | 2 +- ios/NotificationServiceExtension/Info.plist | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index e1c404a73d4b..6d3168382073 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -98,8 +98,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1001043200 - versionName "1.4.32-0" + versionCode 1001043201 + versionName "1.4.32-1" } flavorDimensions "default" diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index 7092d3239ae3..6e62db6dea30 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -40,7 +40,7 @@ CFBundleVersion - 1.4.32.0 + 1.4.32.1 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index a402788ad579..45e0b42db439 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -19,6 +19,6 @@ CFBundleSignature ???? CFBundleVersion - 1.4.32.0 + 1.4.32.1 diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index 58a90b1796e9..2f0ce291dfc7 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -5,7 +5,7 @@ CFBundleShortVersionString 1.4.32 CFBundleVersion - 1.4.32.0 + 1.4.32.1 NSExtension NSExtensionPointIdentifier diff --git a/package-lock.json b/package-lock.json index ad9754c4184b..5a774298babd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "1.4.32-0", + "version": "1.4.32-1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.4.32-0", + "version": "1.4.32-1", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index ad48cc7093b9..055958b9a375 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.4.32-0", + "version": "1.4.32-1", "author": "Expensify, Inc.", "homepage": "https://new.expensify.com", "description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.", From 2f91117eb6498de1687188047a7f406e7bdf9adf Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Thu, 25 Jan 2024 11:48:11 +0100 Subject: [PATCH 377/391] Remove ts-expect-error after merging main --- src/ONYXKEYS.ts | 4 ++-- src/pages/PrivateNotes/PrivateNotesEditPage.tsx | 8 +++----- src/types/onyx/Form.ts | 6 +++++- src/types/onyx/index.ts | 3 ++- 4 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 2a535bab038c..7abf6db1769d 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -524,8 +524,8 @@ type OnyxValues = { [ONYXKEYS.FORMS.SETTINGS_STATUS_CLEAR_DATE_FORM_DRAFT]: OnyxTypes.Form; [ONYXKEYS.FORMS.SETTINGS_STATUS_SET_CLEAR_AFTER_FORM]: OnyxTypes.Form; [ONYXKEYS.FORMS.SETTINGS_STATUS_SET_CLEAR_AFTER_FORM_DRAFT]: OnyxTypes.Form; - [ONYXKEYS.FORMS.PRIVATE_NOTES_FORM]: OnyxTypes.Form; - [ONYXKEYS.FORMS.PRIVATE_NOTES_FORM_DRAFT]: OnyxTypes.Form; + [ONYXKEYS.FORMS.PRIVATE_NOTES_FORM]: OnyxTypes.PrivateNotesForm; + [ONYXKEYS.FORMS.PRIVATE_NOTES_FORM_DRAFT]: OnyxTypes.PrivateNotesForm; [ONYXKEYS.FORMS.I_KNOW_A_TEACHER_FORM]: OnyxTypes.IKnowATeacherForm; [ONYXKEYS.FORMS.I_KNOW_A_TEACHER_FORM_DRAFT]: OnyxTypes.IKnowATeacherForm; [ONYXKEYS.FORMS.INTRO_SCHOOL_PRINCIPAL_FORM]: OnyxTypes.IntroSchoolPrincipalForm; diff --git a/src/pages/PrivateNotes/PrivateNotesEditPage.tsx b/src/pages/PrivateNotes/PrivateNotesEditPage.tsx index 6b96ee657d65..805f2d8fcb90 100644 --- a/src/pages/PrivateNotes/PrivateNotesEditPage.tsx +++ b/src/pages/PrivateNotes/PrivateNotesEditPage.tsx @@ -5,13 +5,13 @@ import Str from 'expensify-common/lib/str'; import lodashDebounce from 'lodash/debounce'; import React, {useCallback, useMemo, useRef, useState} from 'react'; import {Keyboard} from 'react-native'; -import type {TextInput as TextInputRN} from 'react-native'; import type {OnyxCollection} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import FormProvider from '@components/Form/FormProvider'; import InputWrapper from '@components/Form/InputWrapper'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; +import type {AnimatedTextInputRef} from '@components/RNTextInput'; import ScreenWrapper from '@components/ScreenWrapper'; import Text from '@components/Text'; import TextInput from '@components/TextInput'; @@ -64,7 +64,7 @@ function PrivateNotesEditPage({route, personalDetailsList, report}: PrivateNotes ); // To focus on the input field when the page loads - const privateNotesInput = useRef(null); + const privateNotesInput = useRef(null); const focusTimeoutRef = useRef(null); useFocusEffect( @@ -114,7 +114,6 @@ function PrivateNotesEditPage({route, personalDetailsList, report}: PrivateNotes shouldShowBackButton onCloseButtonPress={() => Navigation.dismissModal()} /> - {/* @ts-expect-error TODO: Remove this once FormProvider (https://github.com/Expensify/App/issues/31972) is migrated to TypeScript. */} { + ref={(el: AnimatedTextInputRef) => { if (!el) { return; } diff --git a/src/types/onyx/Form.ts b/src/types/onyx/Form.ts index f341cef8b7b8..c3bcec2a2d3b 100644 --- a/src/types/onyx/Form.ts +++ b/src/types/onyx/Form.ts @@ -50,6 +50,10 @@ type IntroSchoolPrincipalForm = Form<{ partnerUserID: string; }>; +type PrivateNotesForm = Form<{ + privateNotes: string; +}>; + export default Form; -export type {AddDebitCardForm, DateOfBirthForm, DisplayNameForm, FormValueType, NewRoomForm, BaseForm, IKnowATeacherForm, IntroSchoolPrincipalForm}; +export type {AddDebitCardForm, DateOfBirthForm, PrivateNotesForm, DisplayNameForm, FormValueType, NewRoomForm, BaseForm, IKnowATeacherForm, IntroSchoolPrincipalForm}; diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts index bceba6496851..5b04cae58671 100644 --- a/src/types/onyx/index.ts +++ b/src/types/onyx/index.ts @@ -9,7 +9,7 @@ import type Credentials from './Credentials'; import type Currency from './Currency'; import type CustomStatusDraft from './CustomStatusDraft'; import type Download from './Download'; -import type {AddDebitCardForm, DateOfBirthForm, DisplayNameForm, IKnowATeacherForm, IntroSchoolPrincipalForm, NewRoomForm} from './Form'; +import type {AddDebitCardForm, DateOfBirthForm, DisplayNameForm, IKnowATeacherForm, IntroSchoolPrincipalForm, NewRoomForm, PrivateNotesForm} from './Form'; import type Form from './Form'; import type FrequentlyUsedEmoji from './FrequentlyUsedEmoji'; import type {FundList} from './Fund'; @@ -150,4 +150,5 @@ export type { NewRoomForm, IKnowATeacherForm, IntroSchoolPrincipalForm, + PrivateNotesForm, }; From 6e2c58de92b81c754390a2dd9fc45e6a77557974 Mon Sep 17 00:00:00 2001 From: OSBotify Date: Thu, 25 Jan 2024 11:01:58 +0000 Subject: [PATCH 378/391] Update version to 1.4.32-2 --- android/app/build.gradle | 4 ++-- ios/NewExpensify/Info.plist | 2 +- ios/NewExpensifyTests/Info.plist | 2 +- ios/NotificationServiceExtension/Info.plist | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 6d3168382073..e135d44eb834 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -98,8 +98,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1001043201 - versionName "1.4.32-1" + versionCode 1001043202 + versionName "1.4.32-2" } flavorDimensions "default" diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index 6e62db6dea30..c636ced8e7f9 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -40,7 +40,7 @@ CFBundleVersion - 1.4.32.1 + 1.4.32.2 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 45e0b42db439..ef1ef0d998d5 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -19,6 +19,6 @@ CFBundleSignature ???? CFBundleVersion - 1.4.32.1 + 1.4.32.2 diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index 2f0ce291dfc7..16439b1d24d9 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -5,7 +5,7 @@ CFBundleShortVersionString 1.4.32 CFBundleVersion - 1.4.32.1 + 1.4.32.2 NSExtension NSExtensionPointIdentifier diff --git a/package-lock.json b/package-lock.json index 5a774298babd..543a1366f8d6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "1.4.32-1", + "version": "1.4.32-2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.4.32-1", + "version": "1.4.32-2", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 055958b9a375..8ceac3912660 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.4.32-1", + "version": "1.4.32-2", "author": "Expensify, Inc.", "homepage": "https://new.expensify.com", "description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.", From 666ff99ad31bcbb2d9f0e2060f0fa0015a40e58d Mon Sep 17 00:00:00 2001 From: Nicolay Arefyeu Date: Thu, 25 Jan 2024 13:18:10 +0200 Subject: [PATCH 379/391] Fix hidden name is search --- src/libs/ReportUtils.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index e9c3b1710cc0..a61a9b671db6 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -1617,7 +1617,7 @@ function getDisplayNameForParticipant(accountID?: number, shouldUseShortForm = f // This is to check if account is an invite/optimistically created one // and prevent from falling back to 'Hidden', so a correct value is shown // when searching for a new user - if (personalDetails.isOptimisticPersonalDetail === true && formattedLogin) { + if (personalDetails.isOptimisticPersonalDetail === true) { return formattedLogin; } @@ -2341,7 +2341,13 @@ function getAdminRoomInvitedParticipants(parentReportAction: ReportAction | Reco const originalMessage = isChangeLogObject(parentReportAction.originalMessage); const participantAccountIDs = originalMessage?.targetAccountIDs ?? []; - const participants = participantAccountIDs.map((id) => getDisplayNameForParticipant(id)); + const participants = participantAccountIDs.map((id) => { + const name = getDisplayNameForParticipant(id); + if (name && name?.length > 0) { + return name; + } + return Localize.translateLocal('common.hidden'); + }); const users = participants.length > 1 ? participants.join(` ${Localize.translateLocal('common.and')} `) : participants[0]; if (!users) { return parentReportActionMessage; From a5caca2d9e411566e7a9510d416c8dbd8930248b Mon Sep 17 00:00:00 2001 From: brunovjk Date: Thu, 25 Jan 2024 10:15:52 -0300 Subject: [PATCH 380/391] fix lint --- .../MoneyTemporaryForRefactorRequestParticipantsSelector.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js index 40f535ebbaf7..34c06fad8618 100644 --- a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js +++ b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js @@ -80,7 +80,6 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({ safeAreaPaddingBottomStyle, iouType, iouRequestType, - isSearchingForReports, didScreenTransitionEnd, }) { const {translate} = useLocalize(); From c53276d7bb3e30edc066d9c01ef58d701459e521 Mon Sep 17 00:00:00 2001 From: brunovjk Date: Thu, 25 Jan 2024 10:55:35 -0300 Subject: [PATCH 381/391] fix lint: Removed unused variables and props --- ...oneyTemporaryForRefactorRequestParticipantsSelector.js | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js index 34c06fad8618..15f98205839e 100644 --- a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js +++ b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js @@ -55,9 +55,6 @@ const propTypes = { /** The request type, ie. manual, scan, distance */ iouRequestType: PropTypes.oneOf(_.values(CONST.IOU.REQUEST_TYPE)).isRequired, - /** Whether we are searching for reports in the server */ - isSearchingForReports: PropTypes.bool, - /** Whether the parent screen transition has ended */ didScreenTransitionEnd: PropTypes.bool, }; @@ -67,7 +64,6 @@ const defaultProps = { safeAreaPaddingBottomStyle: {}, reports: {}, betas: [], - isSearchingForReports: false, didScreenTransitionEnd: false, }; @@ -369,8 +365,4 @@ export default withOnyx({ betas: { key: ONYXKEYS.BETAS, }, - isSearchingForReports: { - key: ONYXKEYS.IS_SEARCHING_FOR_REPORTS, - initWithStoredValues: false, - }, })(MoneyTemporaryForRefactorRequestParticipantsSelector); From 0fad915b3b7328ea04d092f6bba412d9729b8d54 Mon Sep 17 00:00:00 2001 From: mkhutornyi Date: Thu, 25 Jan 2024 15:00:57 +0100 Subject: [PATCH 382/391] fix scan capture button overlap with iOS navigation bar --- src/pages/iou/request/step/IOURequestStepScan/index.native.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pages/iou/request/step/IOURequestStepScan/index.native.js b/src/pages/iou/request/step/IOURequestStepScan/index.native.js index 23e30ce25711..b6200f48c507 100644 --- a/src/pages/iou/request/step/IOURequestStepScan/index.native.js +++ b/src/pages/iou/request/step/IOURequestStepScan/index.native.js @@ -217,6 +217,7 @@ function IOURequestStepScan({ return ( Date: Thu, 25 Jan 2024 15:41:05 +0100 Subject: [PATCH 383/391] Perf test workflow improvement --- .github/workflows/reassurePerformanceTests.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/reassurePerformanceTests.yml b/.github/workflows/reassurePerformanceTests.yml index 64b4536d9241..ebe31f41f3d8 100644 --- a/.github/workflows/reassurePerformanceTests.yml +++ b/.github/workflows/reassurePerformanceTests.yml @@ -33,6 +33,7 @@ jobs: npx reassure --baseline git switch --force --detach - git merge --no-commit --allow-unrelated-histories "$BASELINE_BRANCH" -X ours + git checkout --ours . npm install --force npx reassure --branch From 17374b2aeefd9a570430cdf6ad1e703ed5f03275 Mon Sep 17 00:00:00 2001 From: anshuagarwal24 Date: Thu, 25 Jan 2024 23:29:54 +0700 Subject: [PATCH 384/391] Updated Out of Pocket Text Updated (English and Spanish) --- src/languages/en.ts | 2 +- src/languages/es.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index 8a959b5da550..fc426002809a 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -2000,7 +2000,7 @@ export default { }, cardTransactions: { notActivated: 'Not activated', - outOfPocket: 'Out of pocket', + outOfPocket: 'Out-of-pocket spend', companySpend: 'Company spend', }, distance: { diff --git a/src/languages/es.ts b/src/languages/es.ts index 271e564c9b1f..5fb65ab42d50 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -2487,7 +2487,7 @@ export default { }, cardTransactions: { notActivated: 'No activado', - outOfPocket: 'Por cuenta propia', + outOfPocket: 'Gastos por cuenta propia', companySpend: 'Gastos de empresa', }, distance: { From 2ee6e1a52775c06eb287a49d2fa10ae0222a8d30 Mon Sep 17 00:00:00 2001 From: Ionatan Wiznia Date: Thu, 25 Jan 2024 13:59:44 -0300 Subject: [PATCH 385/391] Log if we received push event in old format --- src/libs/actions/User.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/libs/actions/User.ts b/src/libs/actions/User.ts index df5709ac68e2..aaba02ecec1e 100644 --- a/src/libs/actions/User.ts +++ b/src/libs/actions/User.ts @@ -5,6 +5,7 @@ import type {OnyxEntry} from 'react-native-onyx/lib/types'; import type {ValueOf} from 'type-fest'; import * as API from '@libs/API'; import * as ErrorUtils from '@libs/ErrorUtils'; +import Log from '@libs/Log'; import Navigation from '@libs/Navigation/Navigation'; import * as SequentialQueue from '@libs/Network/SequentialQueue'; import * as Pusher from '@libs/Pusher/pusher'; @@ -490,6 +491,7 @@ function subscribeToUserEvents() { // - The data is an object, containing updateIDs from the server and an array of onyx updates (this array is the same format as the original format above) // Example: {lastUpdateID: 1, previousUpdateID: 0, updates: [{onyxMethod: 'whatever', key: 'foo', value: 'bar'}]} if (Array.isArray(pushJSON)) { + Log.warn('Received pusher event with array format'); pushJSON.forEach((multipleEvent) => { PusherUtils.triggerMultiEventHandler(multipleEvent.eventType, multipleEvent.data); }); From 919676f2639ffe433811b324c8fa2c392d33df37 Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Thu, 25 Jan 2024 12:47:21 -0700 Subject: [PATCH 386/391] update policy types --- src/types/onyx/Policy.ts | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/types/onyx/Policy.ts b/src/types/onyx/Policy.ts index 2d8bbb7924bd..c01b09274555 100644 --- a/src/types/onyx/Policy.ts +++ b/src/types/onyx/Policy.ts @@ -121,6 +121,27 @@ type Policy = { /** The approval mode set up on this policy */ approvalMode?: ValueOf; + + /** Whether transactions should be billable by default */ + defaultBillable?: boolean; + + /** The workspace description */ + description?: string; + + /** List of field names that are disabled */ + disabledFields?: Record; + + /** Whether new transactions need to be tagged */ + requiresTag?: boolean; + + /** Whether new transactions need to be categorized */ + requiresCategory?: boolean; + + /** Whether the workspace has multiple levels of tags enabled */ + hasMultipleTagLists?: boolean; + + /** When tax tracking is enabled */ + isTaxTrackingEnabled?: boolean; }; export default Policy; From 7fa54c3826c8bdf5f366d6d787d4e8c89608b1a0 Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Thu, 25 Jan 2024 12:56:12 -0700 Subject: [PATCH 387/391] update disabled fields --- src/types/onyx/Policy.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/types/onyx/Policy.ts b/src/types/onyx/Policy.ts index c01b09274555..0daf0b1275b8 100644 --- a/src/types/onyx/Policy.ts +++ b/src/types/onyx/Policy.ts @@ -26,6 +26,11 @@ type CustomUnit = { errors?: OnyxCommon.Errors; }; +type DisabledFields = { + defaultBillable?: boolean; + reimbursable?: boolean; +} + type AutoReportingOffset = number | ValueOf; type Policy = { @@ -129,7 +134,7 @@ type Policy = { description?: string; /** List of field names that are disabled */ - disabledFields?: Record; + disabledFields?: DisabledFields; /** Whether new transactions need to be tagged */ requiresTag?: boolean; From a58f90f8b05841dfc87ce16836a6460b0da622f9 Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Thu, 25 Jan 2024 13:02:42 -0700 Subject: [PATCH 388/391] fix style --- src/types/onyx/Policy.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types/onyx/Policy.ts b/src/types/onyx/Policy.ts index 0daf0b1275b8..eca7e9d1ee06 100644 --- a/src/types/onyx/Policy.ts +++ b/src/types/onyx/Policy.ts @@ -29,7 +29,7 @@ type CustomUnit = { type DisabledFields = { defaultBillable?: boolean; reimbursable?: boolean; -} +}; type AutoReportingOffset = number | ValueOf; From 4915f12a92708d2decccf28b5fb16ac827b63d38 Mon Sep 17 00:00:00 2001 From: rayane-djouah <77965000+rayane-djouah@users.noreply.github.com> Date: Thu, 25 Jan 2024 22:21:45 +0100 Subject: [PATCH 389/391] add shouldUseNarrowLayout --- src/hooks/useResponsiveLayout.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/hooks/useResponsiveLayout.ts b/src/hooks/useResponsiveLayout.ts index a825acd1039c..f00890116d47 100644 --- a/src/hooks/useResponsiveLayout.ts +++ b/src/hooks/useResponsiveLayout.ts @@ -3,6 +3,7 @@ import NAVIGATORS from '@src/NAVIGATORS'; import useWindowDimensions from './useWindowDimensions'; type ResponsiveLayoutResult = { + shouldUseNarrowLayout: boolean; isSmallScreenWidth: boolean; isInModal: boolean; }; @@ -15,5 +16,6 @@ export default function useResponsiveLayout(): ResponsiveLayoutResult { const lastRoute = state?.routes?.at(-1); const lastRouteName = lastRoute?.name; const isInModal = lastRouteName === NAVIGATORS.LEFT_MODAL_NAVIGATOR || lastRouteName === NAVIGATORS.RIGHT_MODAL_NAVIGATOR; - return {isSmallScreenWidth, isInModal}; + const shouldUseNarrowLayout = isSmallScreenWidth || isInModal; + return {shouldUseNarrowLayout, isSmallScreenWidth, isInModal}; } From 03f0da1a366c9e2b1a92adc9b2d07a579b72e71e Mon Sep 17 00:00:00 2001 From: Ishpaul Singh Date: Fri, 26 Jan 2024 03:56:41 +0530 Subject: [PATCH 390/391] fixes typescript mistake with type assertion --- src/pages/home/report/ReportActionItemMessage.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/home/report/ReportActionItemMessage.tsx b/src/pages/home/report/ReportActionItemMessage.tsx index d68fbf889f29..5f0e3ea4b72c 100644 --- a/src/pages/home/report/ReportActionItemMessage.tsx +++ b/src/pages/home/report/ReportActionItemMessage.tsx @@ -9,7 +9,7 @@ import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import * as ReportUtils from '@libs/ReportUtils'; import CONST from '@src/CONST'; import type {ReportAction} from '@src/types/onyx'; -import type {OriginalMessageSource} from '@src/types/onyx/OriginalMessage'; +import type {OriginalMessageAddComment} from '@src/types/onyx/OriginalMessage'; import TextCommentFragment from './comment/TextCommentFragment'; import ReportActionItemFragment from './ReportActionItemFragment'; @@ -78,7 +78,7 @@ function ReportActionItemMessage({action, displayAsGroup, reportID, style, isHid iouMessage={iouMessage} isThreadParentMessage={ReportActionsUtils.isThreadParentMessage(action, reportID)} pendingAction={action.pendingAction} - source={action.originalMessage as OriginalMessageSource} + source={(action.originalMessage as OriginalMessageAddComment['originalMessage'])?.source} accountID={action.actorAccountID ?? 0} style={style} displayAsGroup={displayAsGroup} From 4daa50ce35368eebcc18a3421433860fe4a3335c Mon Sep 17 00:00:00 2001 From: Jack Nam Date: Fri, 26 Jan 2024 10:37:55 +0900 Subject: [PATCH 391/391] Add onLayout --- src/components/LHNOptionsList/types.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/components/LHNOptionsList/types.ts b/src/components/LHNOptionsList/types.ts index ab7d27fdbd6a..1f2c98301f9a 100644 --- a/src/components/LHNOptionsList/types.ts +++ b/src/components/LHNOptionsList/types.ts @@ -106,6 +106,9 @@ type OptionRowLHNDataProps = { /** A function that is called when an option is selected. Selected option is passed as a param */ onSelectRow?: (optionItem: OptionData, popoverAnchor: RefObject) => void; + + /** Callback to execute when the OptionList lays out */ + onLayout?: (event: LayoutChangeEvent) => void; }; type OptionRowLHNProps = {