From 1c2690a36e376d9b5dbae91e43a538013dd4764d Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Mon, 2 Oct 2023 16:28:03 +0200 Subject: [PATCH 001/580] 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/580] fix: removed loadshGet usage --- src/libs/OptionsListUtils.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index fb6bf665afd7..868a0128c219 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -409,7 +409,7 @@ function getLastMessageTextForReport(report: Report) { ); let lastMessageTextFromReport = ''; - const lastActionName = lodashGet(lastReportAction, 'actionName', ''); + const lastActionName = lastReportAction?.actionName ?? ''; if (ReportUtils.isReportMessageAttachment({text: report.lastMessageText ?? '', html: report.lastMessageHtml ?? '', translationKey: report.lastMessageTranslationKey ?? ''})) { lastMessageTextFromReport = `[${Localize.translateLocal(report.lastMessageTranslationKey ?? 'common.attachment')}]`; @@ -739,7 +739,7 @@ function getCategoryListSections( .filter((category) => !selectedOptionNames.includes(category)) .map((category) => ({ name: category, - enabled: lodashGet(categories, `${category}.enabled`, false), + enabled: categories[`${category}`]?.enabled ?? false, })); const filteredCategories = categoriesValues.filter((category) => !selectedOptionNames.includes(category.name)); @@ -1391,19 +1391,19 @@ function formatMemberForList(member, config = {}) { return undefined; } - const accountID = lodashGet(member, 'accountID', ''); + const accountID = member.accountID; return { - text: lodashGet(member, 'text', '') || lodashGet(member, 'displayName', ''), - alternateText: lodashGet(member, 'alternateText', '') || lodashGet(member, 'login', ''), - keyForList: lodashGet(member, 'keyForList', '') || String(accountID), + text: member.text ?? member.displayName ?? '', + alternateText: member.alternateText ?? member.login ?? '', + keyForList: member.keyForList ?? String(accountID), isSelected: false, isDisabled: false, accountID, login: member.login ?? '', rightElement: null, - icons: lodashGet(member, 'icons'), - pendingAction: lodashGet(member, 'pendingAction'), + icons: member.icons, + pendingAction: member.pendingAction, ...config, }; } From 639136ad63796d54e7c18af31b6cd769fcd4f9e4 Mon Sep 17 00:00:00 2001 From: Aswin S Date: Tue, 7 Nov 2023 10:38:53 +0530 Subject: [PATCH 003/580] fix: do not append whitespace to emoji if whitespace is already present --- .../home/report/ReportActionCompose/ComposerWithSuggestions.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions.js b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions.js index b306676d476a..d99e93e8f628 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions.js +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions.js @@ -259,7 +259,7 @@ function ComposerWithSuggestions({ (commentValue, shouldDebounceSaveComment) => { raiseIsScrollLikelyLayoutTriggered(); const {startIndex, endIndex, diff} = findNewlyAddedChars(lastTextRef.current, commentValue); - const isEmojiInserted = diff.length && endIndex > startIndex && EmojiUtils.containsOnlyEmojis(diff); + const isEmojiInserted = diff.length && endIndex > startIndex && diff.trim() === diff && EmojiUtils.containsOnlyEmojis(diff); const {text: newComment, emojis} = EmojiUtils.replaceAndExtractEmojis( isEmojiInserted ? insertWhiteSpace(commentValue, endIndex) : commentValue, preferredSkinTone, From 6062753ff9e20bae3f6f1ca1623c55c4f63ff1c3 Mon Sep 17 00:00:00 2001 From: Aswin S Date: Wed, 8 Nov 2023 11:06:19 +0530 Subject: [PATCH 004/580] fix: whitespace after emoji with skin tone --- .../ComposerWithSuggestions/ComposerWithSuggestions.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js index cf1fe0e2ec3c..32ea949c4593 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js @@ -224,11 +224,13 @@ function ComposerWithSuggestions({ startIndex = currentIndex; // if text is getting pasted over find length of common suffix and subtract it from new text length + const commonSuffixLength = ComposerUtils.getCommonSuffixLength(prevText, newText); if (selection.end - selection.start > 0) { - const commonSuffixLength = ComposerUtils.getCommonSuffixLength(prevText, newText); endIndex = newText.length - commonSuffixLength; + } else if (commonSuffixLength > 0) { + endIndex = currentIndex + newText.length - prevText.length; } else { - endIndex = currentIndex + (newText.length - prevText.length); + endIndex = currentIndex + newText.length; } } From 4d6412eb31349659cf973616c4e0e266e2914f0d Mon Sep 17 00:00:00 2001 From: Aswin S Date: Wed, 8 Nov 2023 11:29:28 +0530 Subject: [PATCH 005/580] fix: whitespace after short codes right before a word --- .../ComposerWithSuggestions/ComposerWithSuggestions.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js index 32ea949c4593..4e906bd81f9f 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js @@ -225,10 +225,8 @@ function ComposerWithSuggestions({ // if text is getting pasted over find length of common suffix and subtract it from new text length const commonSuffixLength = ComposerUtils.getCommonSuffixLength(prevText, newText); - if (selection.end - selection.start > 0) { + if (commonSuffixLength > 0 || selection.end - selection.start > 0) { endIndex = newText.length - commonSuffixLength; - } else if (commonSuffixLength > 0) { - endIndex = currentIndex + newText.length - prevText.length; } else { endIndex = currentIndex + newText.length; } From 276cae20b7f0c44d11fc3118216f0c03b71064ac Mon Sep 17 00:00:00 2001 From: Aswin S Date: Wed, 8 Nov 2023 11:48:00 +0530 Subject: [PATCH 006/580] fix: insert whitespace after emoji --- src/libs/ComposerUtils/index.ts | 6 +- .../ComposerWithSuggestions.js | 83 +++++++++++++++---- 2 files changed, 74 insertions(+), 15 deletions(-) diff --git a/src/libs/ComposerUtils/index.ts b/src/libs/ComposerUtils/index.ts index 5a7da7ca08cf..58e1efa7aa65 100644 --- a/src/libs/ComposerUtils/index.ts +++ b/src/libs/ComposerUtils/index.ts @@ -32,7 +32,11 @@ function canSkipTriggerHotkeys(isSmallScreenWidth: boolean, isKeyboardShown: boo */ function getCommonSuffixLength(str1: string, str2: string): number { let i = 0; - while (str1[str1.length - 1 - i] === str2[str2.length - 1 - i]) { + if (str1.length === 0 || str2.length === 0) { + return 0; + } + const minLen = Math.min(str1.length, str2.length); + while (i < minLen && str1[str1.length - 1 - i] === str2[str2.length - 1 - i]) { i++; } return i; diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js index b69e65e854d7..8791c1e7cef9 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js @@ -119,6 +119,7 @@ function ComposerWithSuggestions({ return draft; }); const commentRef = useRef(value); + const lastTextRef = useRef(value); const {isSmallScreenWidth} = useWindowDimensions(); const maxComposerLines = isSmallScreenWidth ? CONST.COMPOSER.MAX_LINES_SMALL_SCREEN : CONST.COMPOSER.MAX_LINES; @@ -206,6 +207,50 @@ function ComposerWithSuggestions({ [], ); + /** + * Find the newly added characters between the previous text and the new text based on the selection. + * + * @param {string} prevText - The previous text. + * @param {string} newText - The new text. + * @returns {object} An object containing information about the newly added characters. + * @property {number} startIndex - The start index of the newly added characters in the new text. + * @property {number} endIndex - The end index of the newly added characters in the new text. + * @property {string} diff - The newly added characters. + */ + const findNewlyAddedChars = useCallback( + (prevText, newText) => { + let startIndex = -1; + let endIndex = -1; + let currentIndex = 0; + + // Find the first character mismatch with newText + while (currentIndex < newText.length && prevText.charAt(currentIndex) === newText.charAt(currentIndex) && selection.start > currentIndex) { + currentIndex++; + } + + if (currentIndex < newText.length) { + startIndex = currentIndex; + + const commonSuffixLength = ComposerUtils.getCommonSuffixLength(prevText, newText); + // if text is getting pasted over find length of common suffix and subtract it from new text length + if (commonSuffixLength > 0 || selection.end - selection.start > 0) { + endIndex = newText.length - commonSuffixLength; + } else { + endIndex = currentIndex + newText.length + } + } + + return { + startIndex, + endIndex, + diff: newText.substring(startIndex, endIndex), + }; + }, + [selection.end, selection.start], + ); + + const insertWhiteSpace = (text, index) => `${text.slice(0, index)} ${text.slice(index)}`; + /** * Update the value of the comment in Onyx * @@ -215,7 +260,13 @@ function ComposerWithSuggestions({ const updateComment = useCallback( (commentValue, shouldDebounceSaveComment) => { raiseIsScrollLikelyLayoutTriggered(); - const {text: newComment, emojis} = EmojiUtils.replaceAndExtractEmojis(commentValue, preferredSkinTone, preferredLocale); + const {startIndex, endIndex, diff} = findNewlyAddedChars(lastTextRef.current, commentValue); + const isEmojiInserted = diff.length && endIndex > startIndex && diff.trim() === diff && EmojiUtils.containsOnlyEmojis(diff); + const {text: newComment, emojis} = EmojiUtils.replaceAndExtractEmojis( + isEmojiInserted ? insertWhiteSpace(commentValue, endIndex) : commentValue, + preferredSkinTone, + preferredLocale, + ); if (!_.isEmpty(emojis)) { const newEmojis = EmojiUtils.getAddedEmojis(emojis, emojisPresentBefore.current); if (!_.isEmpty(newEmojis)) { @@ -260,13 +311,14 @@ function ComposerWithSuggestions({ } }, [ - debouncedUpdateFrequentlyUsedEmojis, - preferredLocale, + raiseIsScrollLikelyLayoutTriggered, + findNewlyAddedChars, preferredSkinTone, - reportID, + preferredLocale, setIsCommentEmpty, suggestionsRef, - raiseIsScrollLikelyLayoutTriggered, + debouncedUpdateFrequentlyUsedEmojis, + reportID, debouncedSaveReportComment, ], ); @@ -317,14 +369,8 @@ function ComposerWithSuggestions({ * @param {Boolean} shouldAddTrailSpace */ const replaceSelectionWithText = useCallback( - (text, shouldAddTrailSpace = true) => { - const updatedText = shouldAddTrailSpace ? `${text} ` : text; - const selectionSpaceLength = shouldAddTrailSpace ? CONST.SPACE_LENGTH : 0; - updateComment(ComposerUtils.insertText(commentRef.current, selection, updatedText)); - setSelection((prevSelection) => ({ - start: prevSelection.start + text.length + selectionSpaceLength, - end: prevSelection.start + text.length + selectionSpaceLength, - })); + (text) => { + updateComment(ComposerUtils.insertText(commentRef.current, selection, text)); }, [selection, updateComment], ); @@ -448,7 +494,12 @@ function ComposerWithSuggestions({ } focus(); - replaceSelectionWithText(e.key, false); + // Reset cursor to last known location + setSelection((prevSelection) => ({ + start: prevSelection.start + 1, + end: prevSelection.end + 1, + })); + replaceSelectionWithText(e.key); }, [checkComposerVisibility, focus, replaceSelectionWithText], ); @@ -524,6 +575,10 @@ function ComposerWithSuggestions({ [blur, focus, prepareCommentAndResetComposer, replaceSelectionWithText], ); + useEffect(() => { + lastTextRef.current = value; + }, [value]); + return ( <> From d7269601b2d90d10ae230144787d376ef5a33f07 Mon Sep 17 00:00:00 2001 From: Aswin S Date: Wed, 8 Nov 2023 11:55:53 +0530 Subject: [PATCH 007/580] fix: prettier issue --- .../ComposerWithSuggestions/ComposerWithSuggestions.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js index 8791c1e7cef9..518339828e2a 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js @@ -236,7 +236,7 @@ function ComposerWithSuggestions({ if (commonSuffixLength > 0 || selection.end - selection.start > 0) { endIndex = newText.length - commonSuffixLength; } else { - endIndex = currentIndex + newText.length + endIndex = currentIndex + newText.length; } } From 80535723c3f0301bcf508100116ea59b837bbfb4 Mon Sep 17 00:00:00 2001 From: Aswin S Date: Wed, 8 Nov 2023 12:04:01 +0530 Subject: [PATCH 008/580] fix: prevent duplicate character insertion on refocus --- .../ComposerWithSuggestions/ComposerWithSuggestions.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js index 518339828e2a..6c906246121b 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js @@ -494,14 +494,14 @@ function ComposerWithSuggestions({ } focus(); + // Reset cursor to last known location setSelection((prevSelection) => ({ start: prevSelection.start + 1, end: prevSelection.end + 1, })); - replaceSelectionWithText(e.key); }, - [checkComposerVisibility, focus, replaceSelectionWithText], + [checkComposerVisibility, focus], ); const blur = useCallback(() => { From 658038c9865fee0d2a630c13242b118935168433 Mon Sep 17 00:00:00 2001 From: Aswin S Date: Fri, 24 Nov 2023 23:10:18 +0530 Subject: [PATCH 009/580] fix: refactor utility methods --- src/libs/ComposerUtils/index.ts | 33 ++++++++++++++++++- .../ComposerWithSuggestions.js | 20 ++++------- 2 files changed, 38 insertions(+), 15 deletions(-) diff --git a/src/libs/ComposerUtils/index.ts b/src/libs/ComposerUtils/index.ts index 32ebca9afee8..54af287a67b7 100644 --- a/src/libs/ComposerUtils/index.ts +++ b/src/libs/ComposerUtils/index.ts @@ -14,6 +14,17 @@ function insertText(text: string, selection: Selection, textToInsert: string): s return text.slice(0, selection.start) + textToInsert + text.slice(selection.end, text.length); } +/** + * Insert a white space at given index of text + * @param text - text that needs whitespace to be appended to + * @param index - index at which whitespace should be inserted + * @returns + */ + +function insertWhiteSpaceAtIndex(text: string, index: number) { + return `${text.slice(0, index)} ${text.slice(index)}`; +} + /** * Check whether we can skip trigger hotkeys on some specific devices. */ @@ -23,4 +34,24 @@ function canSkipTriggerHotkeys(isSmallScreenWidth: boolean, isKeyboardShown: boo return (isSmallScreenWidth && DeviceCapabilities.canUseTouchScreen()) || isKeyboardShown; } -export {getNumberOfLines, updateNumberOfLines, insertText, canSkipTriggerHotkeys}; +/** + * Finds the length of common suffix between two texts + * @param str1 - first string to compare + * @param str2 - next string to compare + * @returns number - Length of the common suffix + */ +function findCommonSuffixLength(str1: string, str2: string) { + let commonSuffixLength = 0; + const minLength = Math.min(str1.length, str2.length); + for (let i = 1; i <= minLength; i++) { + if (str1.charAt(str1.length - i) === str2.charAt(str2.length - i)) { + commonSuffixLength++; + } else { + break; + } + } + + return commonSuffixLength; +} + +export {getNumberOfLines, updateNumberOfLines, insertText, canSkipTriggerHotkeys, insertWhiteSpaceAtIndex, findCommonSuffixLength}; diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js index 1425b872f7f1..8793f617b306 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js @@ -209,7 +209,6 @@ function ComposerWithSuggestions({ [], ); - /** * Find the newly added characters between the previous text and the new text based on the selection. * @@ -226,14 +225,6 @@ function ComposerWithSuggestions({ let endIndex = -1; let currentIndex = 0; - const getCommonSuffixLength=(str1, str2) =>{ - let i = 0; - while (str1[str1.length - 1 - i] === str2[str2.length - 1 - i]) { - i++; - } - return i; - } - // Find the first character mismatch with newText while (currentIndex < newText.length && prevText.charAt(currentIndex) === newText.charAt(currentIndex) && selection.start > currentIndex) { currentIndex++; @@ -241,8 +232,7 @@ function ComposerWithSuggestions({ if (currentIndex < newText.length) { startIndex = currentIndex; - - const commonSuffixLength = getCommonSuffixLength(prevText, newText); + const commonSuffixLength = ComposerUtils.findCommonSuffixLength(prevText, newText); // if text is getting pasted over find length of common suffix and subtract it from new text length if (commonSuffixLength > 0 || selection.end - selection.start > 0) { endIndex = newText.length - commonSuffixLength; @@ -260,8 +250,6 @@ function ComposerWithSuggestions({ [selection.end, selection.start], ); - const insertWhiteSpace = (text, index) => `${text.slice(0, index)} ${text.slice(index)}`; - /** * Update the value of the comment in Onyx * @@ -273,7 +261,11 @@ function ComposerWithSuggestions({ raiseIsScrollLikelyLayoutTriggered(); const {startIndex, endIndex, diff} = findNewlyAddedChars(lastTextRef.current, commentValue); const isEmojiInserted = diff.length && endIndex > startIndex && diff.trim() === diff && EmojiUtils.containsOnlyEmojis(diff); - const {text: newComment, emojis, cursorPosition} = EmojiUtils.replaceAndExtractEmojis(isEmojiInserted ? insertWhiteSpace(commentValue, endIndex) : commentValue, preferredSkinTone, preferredLocale); + const { + text: newComment, + emojis, + cursorPosition, + } = EmojiUtils.replaceAndExtractEmojis(isEmojiInserted ? ComposerUtils.insertWhiteSpaceAtIndex(commentValue, endIndex) : commentValue, preferredSkinTone, preferredLocale); if (!_.isEmpty(emojis)) { const newEmojis = EmojiUtils.getAddedEmojis(emojis, emojisPresentBefore.current); if (!_.isEmpty(newEmojis)) { From 2a09811e8833f478b8f4aed976b27156dff6ff5a Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Tue, 28 Nov 2023 13:33:47 +0100 Subject: [PATCH 010/580] 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 011/580] 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 012/580] 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 013/580] 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 014/580] 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 015/580] 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 016/580] 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 017/580] 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 018/580] 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 019/580] 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 020/580] 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 021/580] 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 022/580] 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 023/580] 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 024/580] 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 025/580] 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 026/580] 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 027/580] 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 028/580] 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 029/580] 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 030/580] 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 031/580] 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 032/580] 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 033/580] 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 034/580] 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 035/580] 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 036/580] 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 037/580] 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 a790c0e15a138457f8a182781121484c248d7389 Mon Sep 17 00:00:00 2001 From: Someshwar Tripathi Date: Wed, 6 Dec 2023 03:59:23 +0530 Subject: [PATCH 038/580] Add confirmation modal when user cancels a task --- src/pages/home/HeaderView.js | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/pages/home/HeaderView.js b/src/pages/home/HeaderView.js index 5b57419c8530..8cb504a0c4b0 100644 --- a/src/pages/home/HeaderView.js +++ b/src/pages/home/HeaderView.js @@ -19,6 +19,7 @@ import TaskHeaderActionButton from '@components/TaskHeaderActionButton'; import Text from '@components/Text'; import ThreeDotsMenu from '@components/ThreeDotsMenu'; import Tooltip from '@components/Tooltip'; +import ConfirmModal from '@components/ConfirmModal'; import useLocalize from '@hooks/useLocalize'; import useWindowDimensions from '@hooks/useWindowDimensions'; import {getGroupChatName} from '@libs/GroupChatUtils'; @@ -80,6 +81,7 @@ const defaultProps = { }; function HeaderView(props) { + const [isCancelTaskConfirmModalVisible, setIsCancelTaskConfirmModalVisible] = React.useState(false); const {isSmallScreenWidth, windowWidth} = useWindowDimensions(); const {translate} = useLocalize(); const theme = useTheme(); @@ -128,7 +130,7 @@ function HeaderView(props) { threeDotMenuItems.push({ icon: Expensicons.Trashcan, text: translate('common.cancel'), - onSelected: Session.checkIfActionIsAllowed(() => Task.cancelTask(props.report.reportID, props.report.reportName, props.report.stateNum, props.report.statusNum)), + onSelected: () => setIsCancelTaskConfirmModalVisible(true), }); } } @@ -283,6 +285,19 @@ function HeaderView(props) { )} + { + setIsCancelTaskConfirmModalVisible(false); + Session.checkIfActionIsAllowed(Task.cancelTask(props.report.reportID, props.report.reportName, props.report.stateNum, props.report.statusNum)); + }} + onCancel={() => setIsCancelTaskConfirmModalVisible(false)} + title={translate('task.cancelTask')} + prompt={translate('task.cancelConfirmation')} + confirmText={translate('common.delete')} + cancelText={translate('common.cancel')} + danger + /> )} From 5a4037cdc37ef1982040258faf53537b4ee96538 Mon Sep 17 00:00:00 2001 From: Someshwar Tripathi Date: Wed, 6 Dec 2023 03:59:54 +0530 Subject: [PATCH 039/580] Add text strings to language files --- src/languages/en.ts | 2 ++ src/languages/es.ts | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/languages/en.ts b/src/languages/en.ts index 817f06f6b344..30f87c625044 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -1679,6 +1679,8 @@ export default { markAsIncomplete: 'Mark as incomplete', assigneeError: 'There was an error assigning this task, please try another assignee.', genericCreateTaskFailureMessage: 'Unexpected error create task, please try again later.', + cancelTask: 'Cancel task', + cancelConfirmation: 'Are you sure that you want to cancel this task?', }, statementPage: { title: (year, monthName) => `${monthName} ${year} statement`, diff --git a/src/languages/es.ts b/src/languages/es.ts index b219021daa0f..3f7e24602124 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -1703,6 +1703,8 @@ export default { markAsIncomplete: 'Marcar como incompleta', assigneeError: 'Hubo un error al asignar esta tarea, inténtalo con otro usuario.', genericCreateTaskFailureMessage: 'Error inesperado al crear el tarea, por favor, inténtalo más tarde.', + cancelTask: 'Cancelar tarea', + cancelConfirmation: '¿Estás seguro de que quieres cancelar esta tarea?', }, statementPage: { title: (year, monthName) => `Estado de cuenta de ${monthName} ${year}`, From 8e85d34d0b7525509132d4a730deca89bc46561d Mon Sep 17 00:00:00 2001 From: Someshwar Tripathi Date: Wed, 6 Dec 2023 04:04:16 +0530 Subject: [PATCH 040/580] Prettier changes --- src/pages/home/HeaderView.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/home/HeaderView.js b/src/pages/home/HeaderView.js index 8cb504a0c4b0..f0c6a44fca8e 100644 --- a/src/pages/home/HeaderView.js +++ b/src/pages/home/HeaderView.js @@ -6,6 +6,7 @@ import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; import GoogleMeetIcon from '@assets/images/google-meet.svg'; import ZoomIcon from '@assets/images/zoom-icon.svg'; +import ConfirmModal from '@components/ConfirmModal'; import DisplayNames from '@components/DisplayNames'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; @@ -19,7 +20,6 @@ import TaskHeaderActionButton from '@components/TaskHeaderActionButton'; import Text from '@components/Text'; import ThreeDotsMenu from '@components/ThreeDotsMenu'; import Tooltip from '@components/Tooltip'; -import ConfirmModal from '@components/ConfirmModal'; import useLocalize from '@hooks/useLocalize'; import useWindowDimensions from '@hooks/useWindowDimensions'; import {getGroupChatName} from '@libs/GroupChatUtils'; From b25572798fd71ce4b0f961c0ea51dadfc4751044 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Thu, 7 Dec 2023 07:57:40 +0100 Subject: [PATCH 041/580] 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 042/580] 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 043/580] 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 044/580] 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 045/580] 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 046/580] 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 047/580] 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 048/580] 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 049/580] 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 f623dbbdb4bccfd48b4df1d46d3ddb130909a4cd Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Wed, 13 Dec 2023 14:59:09 +0700 Subject: [PATCH 050/580] use set method when creating policy for payment --- src/libs/actions/Policy.js | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src/libs/actions/Policy.js b/src/libs/actions/Policy.js index f33e6637e2de..430caaf6d6b4 100644 --- a/src/libs/actions/Policy.js +++ b/src/libs/actions/Policy.js @@ -1568,12 +1568,12 @@ function createWorkspaceFromIOUPayment(iouReport) { const optimisticData = [ { - onyxMethod: Onyx.METHOD.MERGE, + onyxMethod: Onyx.METHOD.SET, key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, value: newWorkspace, }, { - onyxMethod: Onyx.METHOD.MERGE, + onyxMethod: Onyx.METHOD.SET, key: `${ONYXKEYS.COLLECTION.POLICY_MEMBERS}${policyID}`, value: { [sessionAccountID]: { @@ -1587,7 +1587,7 @@ function createWorkspaceFromIOUPayment(iouReport) { }, }, { - onyxMethod: Onyx.METHOD.MERGE, + onyxMethod: Onyx.METHOD.SET, key: `${ONYXKEYS.COLLECTION.REPORT}${announceChatReportID}`, value: { pendingFields: { @@ -1597,12 +1597,12 @@ function createWorkspaceFromIOUPayment(iouReport) { }, }, { - onyxMethod: Onyx.METHOD.MERGE, + onyxMethod: Onyx.METHOD.SET, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${announceChatReportID}`, value: announceReportActionData, }, { - onyxMethod: Onyx.METHOD.MERGE, + onyxMethod: Onyx.METHOD.SET, key: `${ONYXKEYS.COLLECTION.REPORT}${adminsChatReportID}`, value: { pendingFields: { @@ -1612,12 +1612,12 @@ function createWorkspaceFromIOUPayment(iouReport) { }, }, { - onyxMethod: Onyx.METHOD.MERGE, + onyxMethod: Onyx.METHOD.SET, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${adminsChatReportID}`, value: adminsReportActionData, }, { - onyxMethod: Onyx.METHOD.MERGE, + onyxMethod: Onyx.METHOD.SET, key: `${ONYXKEYS.COLLECTION.REPORT}${workspaceChatReportID}`, value: { pendingFields: { @@ -1627,17 +1627,17 @@ function createWorkspaceFromIOUPayment(iouReport) { }, }, { - onyxMethod: Onyx.METHOD.MERGE, + onyxMethod: Onyx.METHOD.SET, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${workspaceChatReportID}`, value: workspaceChatReportActionData, }, { - onyxMethod: Onyx.METHOD.MERGE, + onyxMethod: Onyx.METHOD.SET, key: `${ONYXKEYS.COLLECTION.POLICY_DRAFTS}${policyID}`, value: null, }, { - onyxMethod: Onyx.METHOD.MERGE, + onyxMethod: Onyx.METHOD.SET, key: `${ONYXKEYS.COLLECTION.POLICY_MEMBERS_DRAFTS}${policyID}`, value: null, }, @@ -1712,37 +1712,37 @@ function createWorkspaceFromIOUPayment(iouReport) { const failureData = [ { - onyxMethod: Onyx.METHOD.MERGE, + onyxMethod: Onyx.METHOD.SET, key: `${ONYXKEYS.COLLECTION.POLICY_MEMBERS}${policyID}`, value: null, }, { - onyxMethod: Onyx.METHOD.MERGE, + onyxMethod: Onyx.METHOD.SET, key: `${ONYXKEYS.COLLECTION.REPORT}${announceChatReportID}`, value: null, }, { - onyxMethod: Onyx.METHOD.MERGE, + onyxMethod: Onyx.METHOD.SET, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${announceChatReportID}`, value: null, }, { - onyxMethod: Onyx.METHOD.MERGE, + onyxMethod: Onyx.METHOD.SET, key: `${ONYXKEYS.COLLECTION.REPORT}${adminsChatReportID}`, value: null, }, { - onyxMethod: Onyx.METHOD.MERGE, + onyxMethod: Onyx.METHOD.SET, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${adminsChatReportID}`, value: null, }, { - onyxMethod: Onyx.METHOD.MERGE, + onyxMethod: Onyx.METHOD.SET, key: `${ONYXKEYS.COLLECTION.REPORT}${workspaceChatReportID}`, value: null, }, { - onyxMethod: Onyx.METHOD.MERGE, + onyxMethod: Onyx.METHOD.SET, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${workspaceChatReportID}`, value: null, }, From f34bc7a0f17d1d983653600199b2fa1df77cc0ce Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Wed, 13 Dec 2023 10:32:26 +0100 Subject: [PATCH 051/580] 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 052/580] 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 053/580] 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 054/580] 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 40df8fda377ed115332d05cfd5bf7ff99fcb3ed7 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Wed, 13 Dec 2023 17:21:29 +0100 Subject: [PATCH 055/580] 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 056/580] 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 057/580] 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 15b35b34b614ed683effb4db754aa80a6d15ed2d Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Thu, 14 Dec 2023 17:16:36 +0100 Subject: [PATCH 058/580] start migration --- .../Pager/AttachmentCarouselPagerContext.js | 5 - .../AttachmentCarousel/Pager/index.js | 172 ----- src/components/Lightbox.js | 2 +- .../MultiGestureCanvas/getCanvasFitScale.ts | 22 - src/components/MultiGestureCanvas/index.js | 602 ------------------ 5 files changed, 1 insertion(+), 802 deletions(-) delete mode 100644 src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.js delete mode 100644 src/components/Attachments/AttachmentCarousel/Pager/index.js delete mode 100644 src/components/MultiGestureCanvas/getCanvasFitScale.ts delete mode 100644 src/components/MultiGestureCanvas/index.js diff --git a/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.js b/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.js deleted file mode 100644 index abaf06900853..000000000000 --- a/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.js +++ /dev/null @@ -1,5 +0,0 @@ -import {createContext} from 'react'; - -const AttachmentCarouselPagerContext = createContext(null); - -export default AttachmentCarouselPagerContext; diff --git a/src/components/Attachments/AttachmentCarousel/Pager/index.js b/src/components/Attachments/AttachmentCarousel/Pager/index.js deleted file mode 100644 index 553e963a3461..000000000000 --- a/src/components/Attachments/AttachmentCarousel/Pager/index.js +++ /dev/null @@ -1,172 +0,0 @@ -import PropTypes from 'prop-types'; -import React, {useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react'; -import {View} from 'react-native'; -import {createNativeWrapper} from 'react-native-gesture-handler'; -import PagerView from 'react-native-pager-view'; -import Animated, {runOnJS, useAnimatedProps, useAnimatedReaction, useEvent, useHandler, useSharedValue} from 'react-native-reanimated'; -import _ from 'underscore'; -import refPropTypes from '@components/refPropTypes'; -import useThemeStyles from '@hooks/useThemeStyles'; -import AttachmentCarouselPagerContext from './AttachmentCarouselPagerContext'; - -const AnimatedPagerView = Animated.createAnimatedComponent(createNativeWrapper(PagerView)); - -function usePageScrollHandler(handlers, dependencies) { - const {context, doDependenciesDiffer} = useHandler(handlers, dependencies); - const subscribeForEvents = ['onPageScroll']; - - return useEvent( - (event) => { - 'worklet'; - - const {onPageScroll} = handlers; - if (onPageScroll && event.eventName.endsWith('onPageScroll')) { - onPageScroll(event, context); - } - }, - subscribeForEvents, - doDependenciesDiffer, - ); -} - -const noopWorklet = () => { - 'worklet'; - - // noop -}; - -const pagerPropTypes = { - items: PropTypes.arrayOf( - PropTypes.shape({ - key: PropTypes.string, - url: PropTypes.string, - }), - ).isRequired, - renderItem: PropTypes.func.isRequired, - initialIndex: PropTypes.number, - onPageSelected: PropTypes.func, - onTap: PropTypes.func, - onSwipe: PropTypes.func, - onSwipeSuccess: PropTypes.func, - onSwipeDown: PropTypes.func, - onPinchGestureChange: PropTypes.func, - forwardedRef: refPropTypes, -}; - -const pagerDefaultProps = { - initialIndex: 0, - onPageSelected: () => {}, - onTap: () => {}, - onSwipe: noopWorklet, - onSwipeSuccess: () => {}, - onSwipeDown: () => {}, - onPinchGestureChange: () => {}, - forwardedRef: null, -}; - -function AttachmentCarouselPager({items, renderItem, initialIndex, onPageSelected, onTap, onSwipe = noopWorklet, onSwipeSuccess, onSwipeDown, onPinchGestureChange, forwardedRef}) { - const styles = useThemeStyles(); - const shouldPagerScroll = useSharedValue(true); - const pagerRef = useRef(null); - - const isScrolling = useSharedValue(false); - const activeIndex = useSharedValue(initialIndex); - - const pageScrollHandler = usePageScrollHandler( - { - onPageScroll: (e) => { - 'worklet'; - - activeIndex.value = e.position; - isScrolling.value = e.offset !== 0; - }, - }, - [], - ); - - const [activePage, setActivePage] = useState(initialIndex); - - useEffect(() => { - setActivePage(initialIndex); - activeIndex.value = initialIndex; - }, [activeIndex, initialIndex]); - - // we use reanimated for this since onPageSelected is called - // in the middle of the pager animation - useAnimatedReaction( - () => isScrolling.value, - (stillScrolling) => { - if (stillScrolling) { - return; - } - - runOnJS(setActivePage)(activeIndex.value); - }, - ); - - useImperativeHandle( - forwardedRef, - () => ({ - setPage: (...props) => pagerRef.current.setPage(...props), - }), - [], - ); - - const animatedProps = useAnimatedProps(() => ({ - scrollEnabled: shouldPagerScroll.value, - })); - - const contextValue = useMemo( - () => ({ - isScrolling, - pagerRef, - shouldPagerScroll, - onPinchGestureChange, - onTap, - onSwipe, - onSwipeSuccess, - onSwipeDown, - }), - [isScrolling, pagerRef, shouldPagerScroll, onPinchGestureChange, onTap, onSwipe, onSwipeSuccess, onSwipeDown], - ); - - return ( - - - {_.map(items, (item, index) => ( - - {renderItem({item, index, isActive: index === activePage})} - - ))} - - - ); -} - -AttachmentCarouselPager.propTypes = pagerPropTypes; -AttachmentCarouselPager.defaultProps = pagerDefaultProps; -AttachmentCarouselPager.displayName = 'AttachmentCarouselPager'; - -const AttachmentCarouselPagerWithRef = React.forwardRef((props, ref) => ( - -)); - -AttachmentCarouselPagerWithRef.displayName = 'AttachmentCarouselPagerWithRef'; - -export default AttachmentCarouselPagerWithRef; diff --git a/src/components/Lightbox.js b/src/components/Lightbox.js index d0d5a1653242..0f570d6d0d01 100644 --- a/src/components/Lightbox.js +++ b/src/components/Lightbox.js @@ -6,8 +6,8 @@ import useStyleUtils from '@styles/useStyleUtils'; import * as AttachmentsPropTypes from './Attachments/propTypes'; import Image from './Image'; import MultiGestureCanvas from './MultiGestureCanvas'; -import getCanvasFitScale from './MultiGestureCanvas/getCanvasFitScale'; import {zoomRangeDefaultProps, zoomRangePropTypes} from './MultiGestureCanvas/propTypes'; +import getCanvasFitScale from './MultiGestureCanvas/utils'; // Increase/decrease this number to change the number of concurrent lightboxes // The more concurrent lighboxes, the worse performance gets (especially on low-end devices) diff --git a/src/components/MultiGestureCanvas/getCanvasFitScale.ts b/src/components/MultiGestureCanvas/getCanvasFitScale.ts deleted file mode 100644 index e3e402fb066b..000000000000 --- a/src/components/MultiGestureCanvas/getCanvasFitScale.ts +++ /dev/null @@ -1,22 +0,0 @@ -type GetCanvasFitScale = (props: { - canvasSize: { - width: number; - height: number; - }; - contentSize: { - width: number; - height: number; - }; -}) => {scaleX: number; scaleY: number; minScale: number; maxScale: number}; - -const getCanvasFitScale: GetCanvasFitScale = ({canvasSize, contentSize}) => { - const scaleX = canvasSize.width / contentSize.width; - const scaleY = canvasSize.height / contentSize.height; - - const minScale = Math.min(scaleX, scaleY); - const maxScale = Math.max(scaleX, scaleY); - - return {scaleX, scaleY, minScale, maxScale}; -}; - -export default getCanvasFitScale; diff --git a/src/components/MultiGestureCanvas/index.js b/src/components/MultiGestureCanvas/index.js deleted file mode 100644 index c5fd2632c22d..000000000000 --- a/src/components/MultiGestureCanvas/index.js +++ /dev/null @@ -1,602 +0,0 @@ -import React, {useContext, useEffect, useMemo, useRef, useState} from 'react'; -import {View} from 'react-native'; -import {Gesture, GestureDetector} from 'react-native-gesture-handler'; -import Animated, { - cancelAnimation, - runOnJS, - runOnUI, - useAnimatedReaction, - useAnimatedStyle, - useDerivedValue, - useSharedValue, - useWorkletCallback, - withDecay, - withSpring, -} from 'react-native-reanimated'; -import AttachmentCarouselPagerContext from '@components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext'; -import useStyleUtils from '@hooks/useStyleUtils'; -import useThemeStyles from '@hooks/useThemeStyles'; -import getCanvasFitScale from './getCanvasFitScale'; -import {defaultZoomRange, multiGestureCanvasDefaultProps, multiGestureCanvasPropTypes} from './propTypes'; - -const DOUBLE_TAP_SCALE = 3; - -const zoomScaleBounceFactors = { - min: 0.7, - max: 1.5, -}; - -const SPRING_CONFIG = { - mass: 1, - stiffness: 1000, - damping: 500, -}; - -function clamp(value, lowerBound, upperBound) { - 'worklet'; - - return Math.min(Math.max(lowerBound, value), upperBound); -} - -function getDeepDefaultProps({contentSize: contentSizeProp = {}, zoomRange: zoomRangeProp = {}}) { - const contentSize = { - width: contentSizeProp.width == null ? 1 : contentSizeProp.width, - height: contentSizeProp.height == null ? 1 : contentSizeProp.height, - }; - - const zoomRange = { - min: zoomRangeProp.min == null ? defaultZoomRange.min : zoomRangeProp.min, - max: zoomRangeProp.max == null ? defaultZoomRange.max : zoomRangeProp.max, - }; - - return {contentSize, zoomRange}; -} - -function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, children, ...props}) { - const styles = useThemeStyles(); - const StyleUtils = useStyleUtils(); - const {contentSize, zoomRange} = getDeepDefaultProps(props); - - const attachmentCarouselPagerContext = useContext(AttachmentCarouselPagerContext); - - const pagerRefFallback = useRef(null); - const {onTap, onSwipe, onSwipeSuccess, pagerRef, shouldPagerScroll, isScrolling, onPinchGestureChange} = attachmentCarouselPagerContext || { - onTap: () => undefined, - onSwipe: () => undefined, - onSwipeSuccess: () => undefined, - onPinchGestureChange: () => undefined, - pagerRef: pagerRefFallback, - shouldPagerScroll: false, - isScrolling: false, - ...props, - }; - - const {minScale: minContentScale, maxScale: maxContentScale} = useMemo(() => getCanvasFitScale({canvasSize, contentSize}), [canvasSize, contentSize]); - const scaledWidth = useMemo(() => contentSize.width * minContentScale, [contentSize.width, minContentScale]); - const scaledHeight = useMemo(() => contentSize.height * minContentScale, [contentSize.height, minContentScale]); - - // On double tap zoom to fill, but at least 3x zoom - const doubleTapScale = useMemo(() => Math.max(DOUBLE_TAP_SCALE, maxContentScale / minContentScale), [maxContentScale, minContentScale]); - - const zoomScale = useSharedValue(1); - // Adding together the pinch zoom scale and the initial scale to fit the content into the canvas - // Using the smaller content scale, so that the immage is not bigger than the canvas - // and not smaller than needed to fit - const totalScale = useDerivedValue(() => zoomScale.value * minContentScale, [minContentScale]); - - const zoomScaledContentWidth = useDerivedValue(() => contentSize.width * totalScale.value, [contentSize.width]); - const zoomScaledContentHeight = useDerivedValue(() => contentSize.height * totalScale.value, [contentSize.height]); - - // used for pan gesture - const translateY = useSharedValue(0); - const translateX = useSharedValue(0); - const offsetX = useSharedValue(0); - const offsetY = useSharedValue(0); - const isSwiping = useSharedValue(false); - - // used for moving fingers when pinching - const pinchTranslateX = useSharedValue(0); - const pinchTranslateY = useSharedValue(0); - const pinchBounceTranslateX = useSharedValue(0); - const pinchBounceTranslateY = useSharedValue(0); - - // storage for the the origin of the gesture - const origin = { - x: useSharedValue(0), - y: useSharedValue(0), - }; - - // storage for the pan velocity to calculate the decay - const panVelocityX = useSharedValue(0); - const panVelocityY = useSharedValue(0); - - // store scale in between gestures - const pinchScaleOffset = useSharedValue(1); - - // disable pan vertically when content is smaller than screen - const canPanVertically = useDerivedValue(() => canvasSize.height < zoomScaledContentHeight.value, [canvasSize.height]); - - // calculates bounds of the scaled content - // can we pan left/right/up/down - // can be used to limit gesture or implementing tension effect - const getBounds = useWorkletCallback(() => { - let rightBoundary = 0; - let topBoundary = 0; - - if (canvasSize.width < zoomScaledContentWidth.value) { - rightBoundary = Math.abs(canvasSize.width - zoomScaledContentWidth.value) / 2; - } - - if (canvasSize.height < zoomScaledContentHeight.value) { - topBoundary = Math.abs(zoomScaledContentHeight.value - canvasSize.height) / 2; - } - - const maxVector = {x: rightBoundary, y: topBoundary}; - const minVector = {x: -rightBoundary, y: -topBoundary}; - - const target = { - x: clamp(offsetX.value, minVector.x, maxVector.x), - y: clamp(offsetY.value, minVector.y, maxVector.y), - }; - - const isInBoundaryX = target.x === offsetX.value; - const isInBoundaryY = target.y === offsetY.value; - - return { - target, - isInBoundaryX, - isInBoundaryY, - minVector, - maxVector, - canPanLeft: target.x < maxVector.x, - canPanRight: target.x > minVector.x, - }; - }, [canvasSize.width, canvasSize.height]); - - const afterPanGesture = useWorkletCallback(() => { - const {target, isInBoundaryX, isInBoundaryY, minVector, maxVector} = getBounds(); - - if (!canPanVertically.value) { - offsetY.value = withSpring(target.y, SPRING_CONFIG); - } - - if (zoomScale.value === 1 && offsetX.value === 0 && offsetY.value === 0 && translateX.value === 0 && translateY.value === 0) { - // we don't need to run any animations - return; - } - - if (zoomScale.value <= 1) { - // just center it - offsetX.value = withSpring(0, SPRING_CONFIG); - offsetY.value = withSpring(0, SPRING_CONFIG); - return; - } - - const deceleration = 0.9915; - - if (isInBoundaryX) { - if (Math.abs(panVelocityX.value) > 0 && zoomScale.value <= zoomRange.max) { - offsetX.value = withDecay({ - velocity: panVelocityX.value, - clamp: [minVector.x, maxVector.x], - deceleration, - rubberBandEffect: false, - }); - } - } else { - offsetX.value = withSpring(target.x, SPRING_CONFIG); - } - - if (isInBoundaryY) { - if ( - Math.abs(panVelocityY.value) > 0 && - zoomScale.value <= zoomRange.max && - // limit vertical pan only when content is smaller than screen - offsetY.value !== minVector.y && - offsetY.value !== maxVector.y - ) { - offsetY.value = withDecay({ - velocity: panVelocityY.value, - clamp: [minVector.y, maxVector.y], - deceleration, - }); - } - } else { - offsetY.value = withSpring(target.y, SPRING_CONFIG, () => { - isSwiping.value = false; - }); - } - }); - - const stopAnimation = useWorkletCallback(() => { - cancelAnimation(offsetX); - cancelAnimation(offsetY); - }); - - const zoomToCoordinates = useWorkletCallback( - (canvasFocalX, canvasFocalY) => { - 'worklet'; - - stopAnimation(); - - const canvasOffsetX = Math.max(0, (canvasSize.width - scaledWidth) / 2); - const canvasOffsetY = Math.max(0, (canvasSize.height - scaledHeight) / 2); - - const contentFocal = { - x: clamp(canvasFocalX - canvasOffsetX, 0, scaledWidth), - y: clamp(canvasFocalY - canvasOffsetY, 0, scaledHeight), - }; - - const canvasCenter = { - x: canvasSize.width / 2, - y: canvasSize.height / 2, - }; - - const originContentCenter = { - x: scaledWidth / 2, - y: scaledHeight / 2, - }; - - const targetContentSize = { - width: scaledWidth * doubleTapScale, - height: scaledHeight * doubleTapScale, - }; - - const targetContentCenter = { - x: targetContentSize.width / 2, - y: targetContentSize.height / 2, - }; - - const currentOrigin = { - x: (targetContentCenter.x - canvasCenter.x) * -1, - y: (targetContentCenter.y - canvasCenter.y) * -1, - }; - - const koef = { - x: (1 / originContentCenter.x) * contentFocal.x - 1, - y: (1 / originContentCenter.y) * contentFocal.y - 1, - }; - - const target = { - x: currentOrigin.x * koef.x, - y: currentOrigin.y * koef.y, - }; - - if (targetContentSize.height < canvasSize.height) { - target.y = 0; - } - - offsetX.value = withSpring(target.x, SPRING_CONFIG); - offsetY.value = withSpring(target.y, SPRING_CONFIG); - zoomScale.value = withSpring(doubleTapScale, SPRING_CONFIG); - pinchScaleOffset.value = doubleTapScale; - }, - [scaledWidth, scaledHeight, canvasSize, doubleTapScale], - ); - - const reset = useWorkletCallback((animated) => { - pinchScaleOffset.value = 1; - - stopAnimation(); - - if (animated) { - offsetX.value = withSpring(0, SPRING_CONFIG); - offsetY.value = withSpring(0, SPRING_CONFIG); - zoomScale.value = withSpring(1, SPRING_CONFIG); - } else { - zoomScale.value = 1; - translateX.value = 0; - translateY.value = 0; - offsetX.value = 0; - offsetY.value = 0; - pinchTranslateX.value = 0; - pinchTranslateY.value = 0; - } - }); - - const doubleTap = Gesture.Tap() - .numberOfTaps(2) - .maxDelay(150) - .maxDistance(20) - .onEnd((evt) => { - if (zoomScale.value > 1) { - reset(true); - } else { - zoomToCoordinates(evt.x, evt.y); - } - - if (onScaleChanged != null) { - runOnJS(onScaleChanged)(zoomScale.value); - } - }); - - const panGestureRef = useRef(Gesture.Pan()); - - const singleTap = Gesture.Tap() - .numberOfTaps(1) - .maxDuration(50) - .requireExternalGestureToFail(doubleTap, panGestureRef) - .onBegin(() => { - stopAnimation(); - }) - .onFinalize((evt, success) => { - if (!success || !onTap) { - return; - } - - runOnJS(onTap)(); - }); - - const previousTouch = useSharedValue(null); - - const panGesture = Gesture.Pan() - .manualActivation(true) - .averageTouches(true) - .onTouchesMove((evt, state) => { - if (zoomScale.value > 1) { - state.activate(); - } - - // TODO: Swipe down to close carousel gesture - // this needs fine tuning to work properly - // if (!isScrolling.value && scale.value === 1 && previousTouch.value != null) { - // const velocityX = Math.abs(evt.allTouches[0].x - previousTouch.value.x); - // const velocityY = evt.allTouches[0].y - previousTouch.value.y; - - // // TODO: this needs tuning - // if (Math.abs(velocityY) > velocityX && velocityY > 20) { - // state.activate(); - - // isSwiping.value = true; - // previousTouch.value = null; - - // runOnJS(onSwipeDown)(); - // return; - // } - // } - - if (previousTouch.value == null) { - previousTouch.value = { - x: evt.allTouches[0].x, - y: evt.allTouches[0].y, - }; - } - }) - .simultaneousWithExternalGesture(pagerRef, doubleTap, singleTap) - .onBegin(() => { - stopAnimation(); - }) - .onChange((evt) => { - // since we running both pinch and pan gesture handlers simultaneously - // we need to make sure that we don't pan when we pinch and move fingers - // since we track it as pinch focal gesture - if (evt.numberOfPointers > 1 || isScrolling.value) { - return; - } - - panVelocityX.value = evt.velocityX; - - panVelocityY.value = evt.velocityY; - - if (!isSwiping.value) { - translateX.value += evt.changeX; - } - - if (canPanVertically.value || isSwiping.value) { - translateY.value += evt.changeY; - } - }) - .onEnd((evt) => { - previousTouch.value = null; - - if (isScrolling.value) { - return; - } - - offsetX.value += translateX.value; - offsetY.value += translateY.value; - translateX.value = 0; - translateY.value = 0; - - if (isSwiping.value) { - const enoughVelocity = Math.abs(evt.velocityY) > 300 && Math.abs(evt.velocityX) < Math.abs(evt.velocityY); - const rightDirection = (evt.translationY > 0 && evt.velocityY > 0) || (evt.translationY < 0 && evt.velocityY < 0); - - if (enoughVelocity && rightDirection) { - const maybeInvert = (v) => { - const invert = evt.velocityY < 0; - return invert ? -v : v; - }; - - offsetY.value = withSpring( - maybeInvert(contentSize.height * 2), - { - stiffness: 50, - damping: 30, - mass: 1, - overshootClamping: true, - restDisplacementThreshold: 300, - restSpeedThreshold: 300, - velocity: Math.abs(evt.velocityY) < 1200 ? maybeInvert(1200) : evt.velocityY, - }, - () => { - runOnJS(onSwipeSuccess)(); - }, - ); - return; - } - } - - afterPanGesture(); - - panVelocityX.value = 0; - panVelocityY.value = 0; - }) - .withRef(panGestureRef); - - const getAdjustedFocal = useWorkletCallback( - (focalX, focalY) => ({ - x: focalX - (canvasSize.width / 2 + offsetX.value), - y: focalY - (canvasSize.height / 2 + offsetY.value), - }), - [canvasSize.width, canvasSize.height], - ); - - // used to store event scale value when we limit scale - const pinchGestureScale = useSharedValue(1); - const pinchGestureRunning = useSharedValue(false); - const pinchGesture = Gesture.Pinch() - .onTouchesDown((evt, state) => { - // we don't want to activate pinch gesture when we are scrolling pager - if (!isScrolling.value) { - return; - } - - state.fail(); - }) - .simultaneousWithExternalGesture(panGesture, doubleTap) - .onStart((evt) => { - pinchGestureRunning.value = true; - - stopAnimation(); - - const adjustFocal = getAdjustedFocal(evt.focalX, evt.focalY); - - origin.x.value = adjustFocal.x; - origin.y.value = adjustFocal.y; - }) - .onChange((evt) => { - const newZoomScale = pinchScaleOffset.value * evt.scale; - - if (zoomScale.value >= zoomRange.min * zoomScaleBounceFactors.min && zoomScale.value <= zoomRange.max * zoomScaleBounceFactors.max) { - zoomScale.value = newZoomScale; - pinchGestureScale.value = evt.scale; - } - - const adjustedFocal = getAdjustedFocal(evt.focalX, evt.focalY); - const newPinchTranslateX = adjustedFocal.x + pinchGestureScale.value * origin.x.value * -1; - const newPinchTranslateY = adjustedFocal.y + pinchGestureScale.value * origin.y.value * -1; - - if (zoomScale.value >= zoomRange.min && zoomScale.value <= zoomRange.max) { - pinchTranslateX.value = newPinchTranslateX; - pinchTranslateY.value = newPinchTranslateY; - } else { - pinchBounceTranslateX.value = newPinchTranslateX - pinchTranslateX.value; - pinchBounceTranslateY.value = newPinchTranslateY - pinchTranslateY.value; - } - }) - .onEnd(() => { - offsetX.value += pinchTranslateX.value; - offsetY.value += pinchTranslateY.value; - pinchTranslateX.value = 0; - pinchTranslateY.value = 0; - pinchScaleOffset.value = zoomScale.value; - pinchGestureScale.value = 1; - - if (pinchScaleOffset.value < zoomRange.min) { - pinchScaleOffset.value = zoomRange.min; - zoomScale.value = withSpring(zoomRange.min, SPRING_CONFIG); - } else if (pinchScaleOffset.value > zoomRange.max) { - pinchScaleOffset.value = zoomRange.max; - zoomScale.value = withSpring(zoomRange.max, SPRING_CONFIG); - } - - if (pinchBounceTranslateX.value !== 0 || pinchBounceTranslateY.value !== 0) { - pinchBounceTranslateX.value = withSpring(0, SPRING_CONFIG); - pinchBounceTranslateY.value = withSpring(0, SPRING_CONFIG); - } - - pinchGestureRunning.value = false; - - if (onScaleChanged != null) { - runOnJS(onScaleChanged)(zoomScale.value); - } - }); - - const [isPinchGestureInUse, setIsPinchGestureInUse] = useState(false); - useAnimatedReaction( - () => [zoomScale.value, pinchGestureRunning.value], - ([zoom, running]) => { - const newIsPinchGestureInUse = zoom !== 1 || running; - if (isPinchGestureInUse !== newIsPinchGestureInUse) { - runOnJS(setIsPinchGestureInUse)(newIsPinchGestureInUse); - } - }, - ); - // eslint-disable-next-line react-hooks/exhaustive-deps - useEffect(() => onPinchGestureChange(isPinchGestureInUse), [isPinchGestureInUse]); - - const animatedStyles = useAnimatedStyle(() => { - const x = pinchTranslateX.value + pinchBounceTranslateX.value + translateX.value + offsetX.value; - const y = pinchTranslateY.value + pinchBounceTranslateY.value + translateY.value + offsetY.value; - - if (isSwiping.value) { - onSwipe(y); - } - - return { - transform: [ - { - translateX: x, - }, - { - translateY: y, - }, - {scale: totalScale.value}, - ], - }; - }); - - // reacts to scale change and enables/disables pager scroll - useAnimatedReaction( - () => zoomScale.value, - () => { - shouldPagerScroll.value = zoomScale.value === 1; - }, - ); - - const mounted = useRef(false); - useEffect(() => { - if (!mounted.current) { - mounted.current = true; - return; - } - - if (!isActive) { - runOnUI(reset)(false); - } - }, [isActive, mounted, reset]); - - return ( - - - - - {children} - - - - - ); -} -MultiGestureCanvas.propTypes = multiGestureCanvasPropTypes; -MultiGestureCanvas.defaultProps = multiGestureCanvasDefaultProps; -MultiGestureCanvas.displayName = 'MultiGestureCanvas'; - -export default MultiGestureCanvas; -export {defaultZoomRange, zoomScaleBounceFactors}; From 83df84983451c7d95a5b9f7a0c93541cda08a98b Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Thu, 14 Dec 2023 17:16:42 +0100 Subject: [PATCH 059/580] continue --- .../Pager/AttachmentCarouselPagerContext.ts | 18 + .../AttachmentCarousel/Pager/index.tsx | 174 +++++ src/components/MultiGestureCanvas/index.tsx | 623 ++++++++++++++++++ src/components/MultiGestureCanvas/utils.ts | 42 ++ 4 files changed, 857 insertions(+) create mode 100644 src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts create mode 100644 src/components/Attachments/AttachmentCarousel/Pager/index.tsx create mode 100644 src/components/MultiGestureCanvas/index.tsx create mode 100644 src/components/MultiGestureCanvas/utils.ts diff --git a/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts b/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts new file mode 100644 index 000000000000..6c19d1ccdafe --- /dev/null +++ b/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts @@ -0,0 +1,18 @@ +import {createContext} from 'react'; +import PagerView from 'react-native-pager-view'; +import {SharedValue} from 'react-native-reanimated'; + +type AttachmentCarouselPagerContextType = { + onTap: () => void; + onSwipe: (y: number) => void; + onSwipeSuccess: () => void; + onPinchGestureChange: (isPinchGestureInUse: boolean) => void; + pagerRef: React.Ref; + shouldPagerScroll: SharedValue; + isScrolling: SharedValue; +}; + +const AttachmentCarouselPagerContext = createContext(null); + +export default AttachmentCarouselPagerContext; +export type {AttachmentCarouselPagerContextType}; diff --git a/src/components/Attachments/AttachmentCarousel/Pager/index.tsx b/src/components/Attachments/AttachmentCarousel/Pager/index.tsx new file mode 100644 index 000000000000..7043579edd3c --- /dev/null +++ b/src/components/Attachments/AttachmentCarousel/Pager/index.tsx @@ -0,0 +1,174 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, {useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react'; +import {View} from 'react-native'; +import {createNativeWrapper} from 'react-native-gesture-handler'; +import PagerView, {PagerViewProps} from 'react-native-pager-view'; +import Animated, {runOnJS, useAnimatedProps, useAnimatedReaction, useEvent, useHandler, useSharedValue} from 'react-native-reanimated'; +import refPropTypes from '@components/refPropTypes'; +import useThemeStyles from '@hooks/useThemeStyles'; +import AttachmentCarouselPagerContext from './AttachmentCarouselPagerContext'; + +const AnimatedPagerView = Animated.createAnimatedComponent(createNativeWrapper(PagerView)); + +type PageScrollHandler = NonNullable; +type PageScrollHandlerParams = Parameters; +const usePageScrollHandler = (handlers: PageScrollHandlerParams[0], dependencies: PageScrollHandlerParams[1]): PageScrollHandler => { + const {context, doDependenciesDiffer} = useHandler(handlers, dependencies); + const subscribeForEvents = ['onPageScroll']; + + return useEvent( + (event) => { + 'worklet'; + + const {onPageScroll} = handlers; + if (onPageScroll && event.eventName.endsWith('onPageScroll')) { + onPageScroll(event, context); + } + }, + subscribeForEvents, + doDependenciesDiffer, + ); +}; + +const noopWorklet = () => { + 'worklet'; + + // noop +}; + +const pagerPropTypes = { + items: PropTypes.arrayOf( + PropTypes.shape({ + key: PropTypes.string, + url: PropTypes.string, + }), + ).isRequired, + renderItem: PropTypes.func.isRequired, + initialIndex: PropTypes.number, + onPageSelected: PropTypes.func, + onTap: PropTypes.func, + onSwipe: PropTypes.func, + onSwipeSuccess: PropTypes.func, + onSwipeDown: PropTypes.func, + onPinchGestureChange: PropTypes.func, + forwardedRef: refPropTypes, +}; + +const pagerDefaultProps = { + initialIndex: 0, + onPageSelected: () => {}, + onTap: () => {}, + onSwipe: noopWorklet, + onSwipeSuccess: () => {}, + onSwipeDown: () => {}, + onPinchGestureChange: () => {}, + forwardedRef: null, +}; + +function AttachmentCarouselPager({items, renderItem, initialIndex, onPageSelected, onTap, onSwipe = noopWorklet, onSwipeSuccess, onSwipeDown, onPinchGestureChange, forwardedRef}) { + const styles = useThemeStyles(); + const shouldPagerScroll = useSharedValue(true); + const pagerRef = useRef(null); + + const isScrolling = useSharedValue(false); + const activeIndex = useSharedValue(initialIndex); + + const pageScrollHandler = usePageScrollHandler( + { + onPageScroll: (e) => { + 'worklet'; + + activeIndex.value = e.position; + isScrolling.value = e.offset !== 0; + }, + }, + [], + ); + + const [activePage, setActivePage] = useState(initialIndex); + + useEffect(() => { + setActivePage(initialIndex); + activeIndex.value = initialIndex; + }, [activeIndex, initialIndex]); + + // we use reanimated for this since onPageSelected is called + // in the middle of the pager animation + useAnimatedReaction( + () => isScrolling.value, + (stillScrolling) => { + if (stillScrolling) { + return; + } + + runOnJS(setActivePage)(activeIndex.value); + }, + ); + + useImperativeHandle( + forwardedRef, + () => ({ + setPage: (...props) => pagerRef.current.setPage(...props), + }), + [], + ); + + const animatedProps = useAnimatedProps(() => ({ + scrollEnabled: shouldPagerScroll.value, + })); + + const contextValue = useMemo( + () => ({ + isScrolling, + pagerRef, + shouldPagerScroll, + onPinchGestureChange, + onTap, + onSwipe, + onSwipeSuccess, + onSwipeDown, + }), + [isScrolling, pagerRef, shouldPagerScroll, onPinchGestureChange, onTap, onSwipe, onSwipeSuccess, onSwipeDown], + ); + + return ( + + + {_.map(items, (item, index) => ( + + {renderItem({item, index, isActive: index === activePage})} + + ))} + + + ); +} + +AttachmentCarouselPager.propTypes = pagerPropTypes; +AttachmentCarouselPager.defaultProps = pagerDefaultProps; +AttachmentCarouselPager.displayName = 'AttachmentCarouselPager'; + +const AttachmentCarouselPagerWithRef = React.forwardRef((props, ref) => ( + +)); + +AttachmentCarouselPagerWithRef.displayName = 'AttachmentCarouselPagerWithRef'; + +export default AttachmentCarouselPagerWithRef; diff --git a/src/components/MultiGestureCanvas/index.tsx b/src/components/MultiGestureCanvas/index.tsx new file mode 100644 index 000000000000..224c0c41c23a --- /dev/null +++ b/src/components/MultiGestureCanvas/index.tsx @@ -0,0 +1,623 @@ +import React, {useContext, useEffect, useMemo, useRef, useState} from 'react'; +import {View} from 'react-native'; +import {Gesture, GestureDetector} from 'react-native-gesture-handler'; +import PagerView from 'react-native-pager-view'; +import Animated, { + cancelAnimation, + runOnJS, + runOnUI, + useAnimatedReaction, + useAnimatedStyle, + useDerivedValue, + useSharedValue, + useWorkletCallback, + withDecay, + withSpring, +} from 'react-native-reanimated'; +import AttachmentCarouselPagerContext, {AttachmentCarouselPagerContextType} from '@components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext'; +import useStyleUtils from '@hooks/useStyleUtils'; +import useThemeStyles from '@hooks/useThemeStyles'; +import {clamp, getCanvasFitScale, getDeepDefaultProps} from './utils'; + +const DOUBLE_TAP_SCALE = 3; + +const defaultZoomRange = { + min: 1, + max: 20, +}; + +const zoomScaleBounceFactors = { + min: 0.7, + max: 1.5, +}; + +const SPRING_CONFIG = { + mass: 1, + stiffness: 1000, + damping: 500, +}; + +type MultiGestureCanvasProps = React.PropsWithChildren<{ + /** + * Wheter the canvas is currently active (in the screen) or not. + * Disables certain gestures and functionality + */ + isActive: boolean; + + /** Handles scale changed event */ + onScaleChanged: (zoomScale: number) => void; + + /** The width and height of the canvas. + * This is needed in order to properly scale the content in the canvas + */ + canvasSize: { + width: number; + height: number; + }; + + /** The width and height of the content. + * This is needed in order to properly scale the content in the canvas + */ + contentSize: { + width: number; + height: number; + }; + + /** Range of zoom that can be applied to the content by pinching or double tapping. */ + zoomRange: { + min?: number; + max?: number; + }; +}>; + +function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, children, ...props}: MultiGestureCanvasProps) { + const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); + const {contentSize, zoomRange} = getDeepDefaultProps(props); + + const attachmentCarouselPagerContext = useContext(AttachmentCarouselPagerContext); + + const pagerRefFallback = useRef(null); + + const defaultContext: AttachmentCarouselPagerContextType = { + onTap: () => undefined, + onSwipe: () => undefined, + onSwipeSuccess: () => undefined, + onPinchGestureChange: () => undefined, + pagerRef: pagerRefFallback, + shouldPagerScroll: useSharedValue(false), + isScrolling: useSharedValue(false), + ...props, + }; + const {onTap, onSwipe, onSwipeSuccess, pagerRef, shouldPagerScroll, isScrolling, onPinchGestureChange} = attachmentCarouselPagerContext ?? defaultContext; + + const {minScale: minContentScale, maxScale: maxContentScale} = useMemo(() => getCanvasFitScale({canvasSize, contentSize}), [canvasSize, contentSize]); + const scaledWidth = useMemo(() => contentSize.width * minContentScale, [contentSize.width, minContentScale]); + const scaledHeight = useMemo(() => contentSize.height * minContentScale, [contentSize.height, minContentScale]); + + // On double tap zoom to fill, but at least 3x zoom + const doubleTapScale = useMemo(() => Math.max(DOUBLE_TAP_SCALE, maxContentScale / minContentScale), [maxContentScale, minContentScale]); + + const zoomScale = useSharedValue(1); + // Adding together the pinch zoom scale and the initial scale to fit the content into the canvas + // Using the smaller content scale, so that the immage is not bigger than the canvas + // and not smaller than needed to fit + const totalScale = useDerivedValue(() => zoomScale.value * minContentScale, [minContentScale]); + + const zoomScaledContentWidth = useDerivedValue(() => contentSize.width * totalScale.value, [contentSize.width]); + const zoomScaledContentHeight = useDerivedValue(() => contentSize.height * totalScale.value, [contentSize.height]); + + // used for pan gesture + const translateY = useSharedValue(0); + const translateX = useSharedValue(0); + const offsetX = useSharedValue(0); + const offsetY = useSharedValue(0); + const isSwiping = useSharedValue(false); + + // used for moving fingers when pinching + const pinchTranslateX = useSharedValue(0); + const pinchTranslateY = useSharedValue(0); + const pinchBounceTranslateX = useSharedValue(0); + const pinchBounceTranslateY = useSharedValue(0); + + // storage for the the origin of the gesture + const origin = { + x: useSharedValue(0), + y: useSharedValue(0), + }; + + // storage for the pan velocity to calculate the decay + const panVelocityX = useSharedValue(0); + const panVelocityY = useSharedValue(0); + + // store scale in between gestures + const pinchScaleOffset = useSharedValue(1); + + // disable pan vertically when content is smaller than screen + const canPanVertically = useDerivedValue(() => canvasSize.height < zoomScaledContentHeight.value, [canvasSize.height]); + + // calculates bounds of the scaled content + // can we pan left/right/up/down + // can be used to limit gesture or implementing tension effect + const getBounds = useWorkletCallback(() => { + let rightBoundary = 0; + let topBoundary = 0; + + if (canvasSize.width < zoomScaledContentWidth.value) { + rightBoundary = Math.abs(canvasSize.width - zoomScaledContentWidth.value) / 2; + } + + if (canvasSize.height < zoomScaledContentHeight.value) { + topBoundary = Math.abs(zoomScaledContentHeight.value - canvasSize.height) / 2; + } + + const maxVector = {x: rightBoundary, y: topBoundary}; + const minVector = {x: -rightBoundary, y: -topBoundary}; + + const target = { + x: clamp(offsetX.value, minVector.x, maxVector.x), + y: clamp(offsetY.value, minVector.y, maxVector.y), + }; + + const isInBoundaryX = target.x === offsetX.value; + const isInBoundaryY = target.y === offsetY.value; + + return { + target, + isInBoundaryX, + isInBoundaryY, + minVector, + maxVector, + canPanLeft: target.x < maxVector.x, + canPanRight: target.x > minVector.x, + }; + }, [canvasSize.width, canvasSize.height]); + + const afterPanGesture = useWorkletCallback(() => { + const {target, isInBoundaryX, isInBoundaryY, minVector, maxVector} = getBounds(); + + if (!canPanVertically.value) { + offsetY.value = withSpring(target.y, SPRING_CONFIG); + } + + if (zoomScale.value === 1 && offsetX.value === 0 && offsetY.value === 0 && translateX.value === 0 && translateY.value === 0) { + // we don't need to run any animations + return; + } + + if (zoomScale.value <= 1) { + // just center it + offsetX.value = withSpring(0, SPRING_CONFIG); + offsetY.value = withSpring(0, SPRING_CONFIG); + return; + } + + const deceleration = 0.9915; + + if (isInBoundaryX) { + if (Math.abs(panVelocityX.value) > 0 && zoomScale.value <= zoomRange.max) { + offsetX.value = withDecay({ + velocity: panVelocityX.value, + clamp: [minVector.x, maxVector.x], + deceleration, + rubberBandEffect: false, + }); + } + } else { + offsetX.value = withSpring(target.x, SPRING_CONFIG); + } + + if (isInBoundaryY) { + if ( + Math.abs(panVelocityY.value) > 0 && + zoomScale.value <= zoomRange.max && + // limit vertical pan only when content is smaller than screen + offsetY.value !== minVector.y && + offsetY.value !== maxVector.y + ) { + offsetY.value = withDecay({ + velocity: panVelocityY.value, + clamp: [minVector.y, maxVector.y], + deceleration, + }); + } + } else { + offsetY.value = withSpring(target.y, SPRING_CONFIG, () => { + isSwiping.value = false; + }); + } + }); + + const stopAnimation = useWorkletCallback(() => { + cancelAnimation(offsetX); + cancelAnimation(offsetY); + }); + + const zoomToCoordinates = useWorkletCallback( + (canvasFocalX: number, canvasFocalY: number) => { + 'worklet'; + + stopAnimation(); + + const canvasOffsetX = Math.max(0, (canvasSize.width - scaledWidth) / 2); + const canvasOffsetY = Math.max(0, (canvasSize.height - scaledHeight) / 2); + + const contentFocal = { + x: clamp(canvasFocalX - canvasOffsetX, 0, scaledWidth), + y: clamp(canvasFocalY - canvasOffsetY, 0, scaledHeight), + }; + + const canvasCenter = { + x: canvasSize.width / 2, + y: canvasSize.height / 2, + }; + + const originContentCenter = { + x: scaledWidth / 2, + y: scaledHeight / 2, + }; + + const targetContentSize = { + width: scaledWidth * doubleTapScale, + height: scaledHeight * doubleTapScale, + }; + + const targetContentCenter = { + x: targetContentSize.width / 2, + y: targetContentSize.height / 2, + }; + + const currentOrigin = { + x: (targetContentCenter.x - canvasCenter.x) * -1, + y: (targetContentCenter.y - canvasCenter.y) * -1, + }; + + const koef = { + x: (1 / originContentCenter.x) * contentFocal.x - 1, + y: (1 / originContentCenter.y) * contentFocal.y - 1, + }; + + const target = { + x: currentOrigin.x * koef.x, + y: currentOrigin.y * koef.y, + }; + + if (targetContentSize.height < canvasSize.height) { + target.y = 0; + } + + offsetX.value = withSpring(target.x, SPRING_CONFIG); + offsetY.value = withSpring(target.y, SPRING_CONFIG); + zoomScale.value = withSpring(doubleTapScale, SPRING_CONFIG); + pinchScaleOffset.value = doubleTapScale; + }, + [scaledWidth, scaledHeight, canvasSize, doubleTapScale], + ); + + const reset = useWorkletCallback((animated) => { + pinchScaleOffset.value = 1; + + stopAnimation(); + + if (animated) { + offsetX.value = withSpring(0, SPRING_CONFIG); + offsetY.value = withSpring(0, SPRING_CONFIG); + zoomScale.value = withSpring(1, SPRING_CONFIG); + } else { + zoomScale.value = 1; + translateX.value = 0; + translateY.value = 0; + offsetX.value = 0; + offsetY.value = 0; + pinchTranslateX.value = 0; + pinchTranslateY.value = 0; + } + }); + + const doubleTap = Gesture.Tap() + .numberOfTaps(2) + .maxDelay(150) + .maxDistance(20) + .onEnd((evt) => { + if (zoomScale.value > 1) { + reset(true); + } else { + zoomToCoordinates(evt.x, evt.y); + } + + if (onScaleChanged != null) { + runOnJS(onScaleChanged)(zoomScale.value); + } + }); + + const panGestureRef = useRef(Gesture.Pan()); + + const singleTap = Gesture.Tap() + .numberOfTaps(1) + .maxDuration(50) + .requireExternalGestureToFail(doubleTap, panGestureRef) + .onBegin(() => { + stopAnimation(); + }) + .onFinalize((evt, success) => { + if (!success || !onTap) { + return; + } + + runOnJS(onTap)(); + }); + + const previousTouch = useSharedValue<{ + x: number; + y: number; + } | null>(null); + + const panGesture = Gesture.Pan() + .manualActivation(true) + .averageTouches(true) + .onTouchesMove((evt, state) => { + if (zoomScale.value > 1) { + state.activate(); + } + + // TODO: Swipe down to close carousel gesture + // this needs fine tuning to work properly + // if (!isScrolling.value && scale.value === 1 && previousTouch.value != null) { + // const velocityX = Math.abs(evt.allTouches[0].x - previousTouch.value.x); + // const velocityY = evt.allTouches[0].y - previousTouch.value.y; + + // // TODO: this needs tuning + // if (Math.abs(velocityY) > velocityX && velocityY > 20) { + // state.activate(); + + // isSwiping.value = true; + // previousTouch.value = null; + + // runOnJS(onSwipeDown)(); + // return; + // } + // } + + if (previousTouch.value == null) { + previousTouch.value = { + x: evt.allTouches[0].x, + y: evt.allTouches[0].y, + }; + } + }) + .simultaneousWithExternalGesture(pagerRef, doubleTap, singleTap) + .onBegin(() => { + stopAnimation(); + }) + .onChange((evt) => { + // since we running both pinch and pan gesture handlers simultaneously + // we need to make sure that we don't pan when we pinch and move fingers + // since we track it as pinch focal gesture + if (evt.numberOfPointers > 1 || isScrolling.value) { + return; + } + + panVelocityX.value = evt.velocityX; + + panVelocityY.value = evt.velocityY; + + if (!isSwiping.value) { + translateX.value += evt.changeX; + } + + if (canPanVertically.value || isSwiping.value) { + translateY.value += evt.changeY; + } + }) + .onEnd((evt) => { + previousTouch.value = null; + + if (isScrolling.value) { + return; + } + + offsetX.value += translateX.value; + offsetY.value += translateY.value; + translateX.value = 0; + translateY.value = 0; + + if (isSwiping.value) { + const enoughVelocity = Math.abs(evt.velocityY) > 300 && Math.abs(evt.velocityX) < Math.abs(evt.velocityY); + const rightDirection = (evt.translationY > 0 && evt.velocityY > 0) || (evt.translationY < 0 && evt.velocityY < 0); + + if (enoughVelocity && rightDirection) { + const maybeInvert = (v: number) => { + const invert = evt.velocityY < 0; + return invert ? -v : v; + }; + + offsetY.value = withSpring( + maybeInvert(contentSize.height * 2), + { + stiffness: 50, + damping: 30, + mass: 1, + overshootClamping: true, + restDisplacementThreshold: 300, + restSpeedThreshold: 300, + velocity: Math.abs(evt.velocityY) < 1200 ? maybeInvert(1200) : evt.velocityY, + }, + () => { + runOnJS(onSwipeSuccess)(); + }, + ); + return; + } + } + + afterPanGesture(); + + panVelocityX.value = 0; + panVelocityY.value = 0; + }) + .withRef(panGestureRef); + + const getAdjustedFocal = useWorkletCallback( + (focalX: number, focalY: number) => ({ + x: focalX - (canvasSize.width / 2 + offsetX.value), + y: focalY - (canvasSize.height / 2 + offsetY.value), + }), + [canvasSize.width, canvasSize.height], + ); + + // used to store event scale value when we limit scale + const pinchGestureScale = useSharedValue(1); + const pinchGestureRunning = useSharedValue(false); + const pinchGesture = Gesture.Pinch() + .onTouchesDown((evt, state) => { + // we don't want to activate pinch gesture when we are scrolling pager + if (!isScrolling.value) { + return; + } + + state.fail(); + }) + .simultaneousWithExternalGesture(panGesture, doubleTap) + .onStart((evt) => { + pinchGestureRunning.value = true; + + stopAnimation(); + + const adjustFocal = getAdjustedFocal(evt.focalX, evt.focalY); + + origin.x.value = adjustFocal.x; + origin.y.value = adjustFocal.y; + }) + .onChange((evt) => { + const newZoomScale = pinchScaleOffset.value * evt.scale; + + if (zoomScale.value >= zoomRange.min * zoomScaleBounceFactors.min && zoomScale.value <= zoomRange.max * zoomScaleBounceFactors.max) { + zoomScale.value = newZoomScale; + pinchGestureScale.value = evt.scale; + } + + const adjustedFocal = getAdjustedFocal(evt.focalX, evt.focalY); + const newPinchTranslateX = adjustedFocal.x + pinchGestureScale.value * origin.x.value * -1; + const newPinchTranslateY = adjustedFocal.y + pinchGestureScale.value * origin.y.value * -1; + + if (zoomScale.value >= zoomRange.min && zoomScale.value <= zoomRange.max) { + pinchTranslateX.value = newPinchTranslateX; + pinchTranslateY.value = newPinchTranslateY; + } else { + pinchBounceTranslateX.value = newPinchTranslateX - pinchTranslateX.value; + pinchBounceTranslateY.value = newPinchTranslateY - pinchTranslateY.value; + } + }) + .onEnd(() => { + offsetX.value += pinchTranslateX.value; + offsetY.value += pinchTranslateY.value; + pinchTranslateX.value = 0; + pinchTranslateY.value = 0; + pinchScaleOffset.value = zoomScale.value; + pinchGestureScale.value = 1; + + if (pinchScaleOffset.value < zoomRange.min) { + pinchScaleOffset.value = zoomRange.min; + zoomScale.value = withSpring(zoomRange.min, SPRING_CONFIG); + } else if (pinchScaleOffset.value > zoomRange.max) { + pinchScaleOffset.value = zoomRange.max; + zoomScale.value = withSpring(zoomRange.max, SPRING_CONFIG); + } + + if (pinchBounceTranslateX.value !== 0 || pinchBounceTranslateY.value !== 0) { + pinchBounceTranslateX.value = withSpring(0, SPRING_CONFIG); + pinchBounceTranslateY.value = withSpring(0, SPRING_CONFIG); + } + + pinchGestureRunning.value = false; + + if (onScaleChanged != null) { + runOnJS(onScaleChanged)(zoomScale.value); + } + }); + + const [isPinchGestureInUse, setIsPinchGestureInUse] = useState(false); + useAnimatedReaction( + () => ({scale: zoomScale.value, isRunning: pinchGestureRunning.value}), + ({scale, isRunning}) => { + const newIsPinchGestureInUse = scale !== 1 || isRunning; + if (isPinchGestureInUse !== newIsPinchGestureInUse) { + runOnJS(setIsPinchGestureInUse)(newIsPinchGestureInUse); + } + }, + ); + // eslint-disable-next-line react-hooks/exhaustive-deps + useEffect(() => onPinchGestureChange(isPinchGestureInUse), [isPinchGestureInUse]); + + const animatedStyles = useAnimatedStyle(() => { + const x = pinchTranslateX.value + pinchBounceTranslateX.value + translateX.value + offsetX.value; + const y = pinchTranslateY.value + pinchBounceTranslateY.value + translateY.value + offsetY.value; + + if (isSwiping.value) { + onSwipe(y); + } + + return { + transform: [ + { + translateX: x, + }, + { + translateY: y, + }, + {scale: totalScale.value}, + ], + }; + }); + + // reacts to scale change and enables/disables pager scroll + useAnimatedReaction( + () => zoomScale.value, + () => { + shouldPagerScroll.value = zoomScale.value === 1; + }, + ); + + const mounted = useRef(false); + useEffect(() => { + if (!mounted.current) { + mounted.current = true; + return; + } + + if (!isActive) { + runOnUI(reset)(false); + } + }, [isActive, mounted, reset]); + + return ( + + + + + {children} + + + + + ); +} +MultiGestureCanvas.displayName = 'MultiGestureCanvas'; + +export default MultiGestureCanvas; +export {defaultZoomRange, zoomScaleBounceFactors}; diff --git a/src/components/MultiGestureCanvas/utils.ts b/src/components/MultiGestureCanvas/utils.ts new file mode 100644 index 000000000000..697ca5e65ba5 --- /dev/null +++ b/src/components/MultiGestureCanvas/utils.ts @@ -0,0 +1,42 @@ +type GetCanvasFitScale = (props: { + canvasSize: { + width: number; + height: number; + }; + contentSize: { + width: number; + height: number; + }; +}) => {scaleX: number; scaleY: number; minScale: number; maxScale: number}; + +const getCanvasFitScale: GetCanvasFitScale = ({canvasSize, contentSize}) => { + const scaleX = canvasSize.width / contentSize.width; + const scaleY = canvasSize.height / contentSize.height; + + const minScale = Math.min(scaleX, scaleY); + const maxScale = Math.max(scaleX, scaleY); + + return {scaleX, scaleY, minScale, maxScale}; +}; + +function clamp(value, lowerBound, upperBound) { + 'worklet'; + + return Math.min(Math.max(lowerBound, value), upperBound); +} + +function getDeepDefaultProps({contentSize: contentSizeProp = {}, zoomRange: zoomRangeProp = {}}) { + const contentSize = { + width: contentSizeProp.width == null ? 1 : contentSizeProp.width, + height: contentSizeProp.height == null ? 1 : contentSizeProp.height, + }; + + const zoomRange = { + min: zoomRangeProp.min == null ? defaultZoomRange.min : zoomRangeProp.min, + max: zoomRangeProp.max == null ? defaultZoomRange.max : zoomRangeProp.max, + }; + + return {contentSize, zoomRange}; +} + +export {getCanvasFitScale, clamp, getDeepDefaultProps}; From 3a8a606720b787cff978e1d8a554fb348d654a0d Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Thu, 14 Dec 2023 17:39:57 +0100 Subject: [PATCH 060/580] further type stuff --- .../AttachmentCarousel/Pager/index.tsx | 58 ++++++++------- .../Pager/usePageScrollHandler.ts | 24 ++++++ .../MultiGestureCanvas/constants.ts | 11 +++ src/components/MultiGestureCanvas/index.tsx | 26 ++----- .../MultiGestureCanvas/propTypes.js | 73 ------------------- src/components/MultiGestureCanvas/types.ts | 11 +++ src/components/MultiGestureCanvas/utils.ts | 25 +++++-- 7 files changed, 102 insertions(+), 126 deletions(-) create mode 100644 src/components/Attachments/AttachmentCarousel/Pager/usePageScrollHandler.ts create mode 100644 src/components/MultiGestureCanvas/constants.ts delete mode 100644 src/components/MultiGestureCanvas/propTypes.js create mode 100644 src/components/MultiGestureCanvas/types.ts diff --git a/src/components/Attachments/AttachmentCarousel/Pager/index.tsx b/src/components/Attachments/AttachmentCarousel/Pager/index.tsx index 7043579edd3c..d483b59a6edb 100644 --- a/src/components/Attachments/AttachmentCarousel/Pager/index.tsx +++ b/src/components/Attachments/AttachmentCarousel/Pager/index.tsx @@ -3,34 +3,15 @@ import PropTypes from 'prop-types'; import React, {useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react'; import {View} from 'react-native'; import {createNativeWrapper} from 'react-native-gesture-handler'; -import PagerView, {PagerViewProps} from 'react-native-pager-view'; -import Animated, {runOnJS, useAnimatedProps, useAnimatedReaction, useEvent, useHandler, useSharedValue} from 'react-native-reanimated'; +import PagerView from 'react-native-pager-view'; +import Animated, {runOnJS, useAnimatedProps, useAnimatedReaction, useSharedValue} from 'react-native-reanimated'; import refPropTypes from '@components/refPropTypes'; import useThemeStyles from '@hooks/useThemeStyles'; import AttachmentCarouselPagerContext from './AttachmentCarouselPagerContext'; +import usePageScrollHandler from './usePageScrollHandler'; const AnimatedPagerView = Animated.createAnimatedComponent(createNativeWrapper(PagerView)); -type PageScrollHandler = NonNullable; -type PageScrollHandlerParams = Parameters; -const usePageScrollHandler = (handlers: PageScrollHandlerParams[0], dependencies: PageScrollHandlerParams[1]): PageScrollHandler => { - const {context, doDependenciesDiffer} = useHandler(handlers, dependencies); - const subscribeForEvents = ['onPageScroll']; - - return useEvent( - (event) => { - 'worklet'; - - const {onPageScroll} = handlers; - if (onPageScroll && event.eventName.endsWith('onPageScroll')) { - onPageScroll(event, context); - } - }, - subscribeForEvents, - doDependenciesDiffer, - ); -}; - const noopWorklet = () => { 'worklet'; @@ -66,7 +47,34 @@ const pagerDefaultProps = { forwardedRef: null, }; -function AttachmentCarouselPager({items, renderItem, initialIndex, onPageSelected, onTap, onSwipe = noopWorklet, onSwipeSuccess, onSwipeDown, onPinchGestureChange, forwardedRef}) { +type AttachmentCarouselPagerProps = React.PropsWithChildren<{ + items: Array<{ + key: string; + url: string; + }>; + renderItem: () => React.ReactNode; + initialIndex: number; + onPageSelected: () => void; + onTap: () => void; + onSwipe: () => void; + onSwipeSuccess: () => void; + onSwipeDown: () => void; + onPinchGestureChange: () => void; + forwardedRef: React.Ref; +}>; + +function AttachmentCarouselPager({ + items, + renderItem, + initialIndex, + onPageSelected, + onTap, + onSwipe = noopWorklet, + onSwipeSuccess, + onSwipeDown, + onPinchGestureChange, + forwardedRef, +}: AttachmentCarouselPagerProps) { const styles = useThemeStyles(); const shouldPagerScroll = useSharedValue(true); const pagerRef = useRef(null); @@ -144,7 +152,7 @@ function AttachmentCarouselPager({items, renderItem, initialIndex, onPageSelecte style={styles.flex1} initialPage={initialIndex} > - {_.map(items, (item, index) => ( + {items.map((item, index) => ( ( diff --git a/src/components/Attachments/AttachmentCarousel/Pager/usePageScrollHandler.ts b/src/components/Attachments/AttachmentCarousel/Pager/usePageScrollHandler.ts new file mode 100644 index 000000000000..e65f1ff3cd00 --- /dev/null +++ b/src/components/Attachments/AttachmentCarousel/Pager/usePageScrollHandler.ts @@ -0,0 +1,24 @@ +import {PagerViewProps} from 'react-native-pager-view'; +import {useEvent, useHandler} from 'react-native-reanimated'; + +type PageScrollHandler = NonNullable; +type PageScrollHandlerParams = Parameters; +const usePageScrollHandler = (handlers: PageScrollHandlerParams[0], dependencies: PageScrollHandlerParams[1]): PageScrollHandler => { + const {context, doDependenciesDiffer} = useHandler(handlers, dependencies); + const subscribeForEvents = ['onPageScroll']; + + return useEvent( + (event) => { + 'worklet'; + + const {onPageScroll} = handlers; + if (onPageScroll && event.eventName.endsWith('onPageScroll')) { + onPageScroll(event, context); + } + }, + subscribeForEvents, + doDependenciesDiffer, + ); +}; + +export default usePageScrollHandler; diff --git a/src/components/MultiGestureCanvas/constants.ts b/src/components/MultiGestureCanvas/constants.ts new file mode 100644 index 000000000000..0103d07c55c2 --- /dev/null +++ b/src/components/MultiGestureCanvas/constants.ts @@ -0,0 +1,11 @@ +const defaultZoomRange = { + min: 1, + max: 20, +}; + +const zoomScaleBounceFactors = { + min: 0.7, + max: 1.5, +}; + +export {defaultZoomRange, zoomScaleBounceFactors}; diff --git a/src/components/MultiGestureCanvas/index.tsx b/src/components/MultiGestureCanvas/index.tsx index 224c0c41c23a..17e2eeb417e0 100644 --- a/src/components/MultiGestureCanvas/index.tsx +++ b/src/components/MultiGestureCanvas/index.tsx @@ -17,20 +17,12 @@ import Animated, { import AttachmentCarouselPagerContext, {AttachmentCarouselPagerContextType} from '@components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; +import {zoomScaleBounceFactors} from './constants'; +import {ContentSizeProp, ZoomRangeProp} from './types'; import {clamp, getCanvasFitScale, getDeepDefaultProps} from './utils'; const DOUBLE_TAP_SCALE = 3; -const defaultZoomRange = { - min: 1, - max: 20, -}; - -const zoomScaleBounceFactors = { - min: 0.7, - max: 1.5, -}; - const SPRING_CONFIG = { mass: 1, stiffness: 1000, @@ -58,22 +50,16 @@ type MultiGestureCanvasProps = React.PropsWithChildren<{ /** The width and height of the content. * This is needed in order to properly scale the content in the canvas */ - contentSize: { - width: number; - height: number; - }; + contentSize: ContentSizeProp; /** Range of zoom that can be applied to the content by pinching or double tapping. */ - zoomRange: { - min?: number; - max?: number; - }; + zoomRange?: ZoomRangeProp; }>; function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, children, ...props}: MultiGestureCanvasProps) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); - const {contentSize, zoomRange} = getDeepDefaultProps(props); + const {contentSize, zoomRange} = getDeepDefaultProps({contentSize: props.contentSize, zoomRange: props.zoomRange}); const attachmentCarouselPagerContext = useContext(AttachmentCarouselPagerContext); @@ -620,4 +606,4 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr MultiGestureCanvas.displayName = 'MultiGestureCanvas'; export default MultiGestureCanvas; -export {defaultZoomRange, zoomScaleBounceFactors}; +export type {MultiGestureCanvasProps}; diff --git a/src/components/MultiGestureCanvas/propTypes.js b/src/components/MultiGestureCanvas/propTypes.js deleted file mode 100644 index f1961ec0e156..000000000000 --- a/src/components/MultiGestureCanvas/propTypes.js +++ /dev/null @@ -1,73 +0,0 @@ -import PropTypes from 'prop-types'; - -const defaultZoomRange = { - min: 1, - max: 20, -}; - -const zoomRangePropTypes = { - /** Range of zoom that can be applied to the content by pinching or double tapping. */ - zoomRange: PropTypes.shape({ - min: PropTypes.number, - max: PropTypes.number, - }), -}; - -const zoomRangeDefaultProps = { - zoomRange: { - min: defaultZoomRange.min, - max: defaultZoomRange.max, - }, -}; - -const multiGestureCanvasPropTypes = { - ...zoomRangePropTypes, - - /** - * Wheter the canvas is currently active (in the screen) or not. - * Disables certain gestures and functionality - */ - isActive: PropTypes.bool, - - /** Handles scale changed event */ - onScaleChanged: PropTypes.func, - - /** The width and height of the canvas. - * This is needed in order to properly scale the content in the canvas - */ - canvasSize: PropTypes.shape({ - width: PropTypes.number.isRequired, - height: PropTypes.number.isRequired, - }).isRequired, - - /** The width and height of the content. - * This is needed in order to properly scale the content in the canvas - */ - contentSize: PropTypes.shape({ - width: PropTypes.number, - height: PropTypes.number, - }), - - /** The scale factors (scaleX, scaleY) that are used to scale the content (width/height) to the canvas size. - * `scaledWidth` and `scaledHeight` reflect the actual size of the content after scaling. - */ - contentScaling: PropTypes.shape({ - scaleX: PropTypes.number, - scaleY: PropTypes.number, - scaledWidth: PropTypes.number, - scaledHeight: PropTypes.number, - }), - - /** Content that should be transformed inside the canvas (images, pdf, ...) */ - children: PropTypes.node.isRequired, -}; - -const multiGestureCanvasDefaultProps = { - isActive: true, - onScaleChanged: () => undefined, - contentSize: undefined, - contentScaling: undefined, - zoomRange: undefined, -}; - -export {defaultZoomRange, zoomRangePropTypes, zoomRangeDefaultProps, multiGestureCanvasPropTypes, multiGestureCanvasDefaultProps}; diff --git a/src/components/MultiGestureCanvas/types.ts b/src/components/MultiGestureCanvas/types.ts new file mode 100644 index 000000000000..11dfc767aacf --- /dev/null +++ b/src/components/MultiGestureCanvas/types.ts @@ -0,0 +1,11 @@ +type ContentSizeProp = { + width: number; + height: number; +}; + +type ZoomRangeProp = { + min?: number; + max?: number; +}; + +export type {ContentSizeProp, ZoomRangeProp}; diff --git a/src/components/MultiGestureCanvas/utils.ts b/src/components/MultiGestureCanvas/utils.ts index 697ca5e65ba5..85cc59887fd5 100644 --- a/src/components/MultiGestureCanvas/utils.ts +++ b/src/components/MultiGestureCanvas/utils.ts @@ -1,3 +1,6 @@ +import {defaultZoomRange} from './constants'; +import {ContentSizeProp, ZoomRangeProp} from './types'; + type GetCanvasFitScale = (props: { canvasSize: { width: number; @@ -19,24 +22,32 @@ const getCanvasFitScale: GetCanvasFitScale = ({canvasSize, contentSize}) => { return {scaleX, scaleY, minScale, maxScale}; }; -function clamp(value, lowerBound, upperBound) { +function clamp(value: number, lowerBound: number, upperBound: number) { 'worklet'; return Math.min(Math.max(lowerBound, value), upperBound); } -function getDeepDefaultProps({contentSize: contentSizeProp = {}, zoomRange: zoomRangeProp = {}}) { +type Props = { + contentSize?: ContentSizeProp; + zoomRange?: ZoomRangeProp; +}; +type PropsWithDefault = { + contentSize: ContentSizeProp; + zoomRange: Required; +}; +const getDeepDefaultProps = ({contentSize: contentSizeProp, zoomRange: zoomRangeProp}: Props): PropsWithDefault => { const contentSize = { - width: contentSizeProp.width == null ? 1 : contentSizeProp.width, - height: contentSizeProp.height == null ? 1 : contentSizeProp.height, + width: contentSizeProp?.width ?? 1, + height: contentSizeProp?.height ?? 1, }; const zoomRange = { - min: zoomRangeProp.min == null ? defaultZoomRange.min : zoomRangeProp.min, - max: zoomRangeProp.max == null ? defaultZoomRange.max : zoomRangeProp.max, + min: zoomRangeProp?.min ?? defaultZoomRange.min, + max: zoomRangeProp?.max ?? defaultZoomRange.max, }; return {contentSize, zoomRange}; -} +}; export {getCanvasFitScale, clamp, getDeepDefaultProps}; From ef06c1f4d00c4685b5cd8c11c7e95c91676a1461 Mon Sep 17 00:00:00 2001 From: brunovjk Date: Thu, 14 Dec 2023 15:36:28 -0300 Subject: [PATCH 061/580] 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 062/580] 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 063/580] 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 064/580] 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 065/580] 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 066/580] 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 067/580] 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 068/580] 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 069/580] 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 070/580] 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 071/580] 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 072/580] 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 073/580] 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 074/580] 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 075/580] 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 076/580] 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 077/580] 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 078/580] 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 079/580] 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 9d22a45679e58d5e1638beb5a0e367dce05f6909 Mon Sep 17 00:00:00 2001 From: tienifr Date: Wed, 20 Dec 2023 14:27:29 +0700 Subject: [PATCH 080/580] fix: 33318 --- src/components/ThemeProvider.tsx | 7 +++- src/libs/DomUtils/index.native.ts | 6 ++++ src/libs/DomUtils/index.ts | 57 +++++++++++++++++++++++++++++++ web/index.html | 26 -------------- 4 files changed, 69 insertions(+), 27 deletions(-) diff --git a/src/components/ThemeProvider.tsx b/src/components/ThemeProvider.tsx index 34bc32be9c99..41423991f051 100644 --- a/src/components/ThemeProvider.tsx +++ b/src/components/ThemeProvider.tsx @@ -1,10 +1,11 @@ /* eslint-disable react/jsx-props-no-spreading */ import PropTypes from 'prop-types'; -import React, {useMemo} from 'react'; +import React, {useEffect, useMemo} from 'react'; import useThemePreferenceWithStaticOverride from '@hooks/useThemePreferenceWithStaticOverride'; import themes from '@styles/theme'; import ThemeContext from '@styles/theme/context/ThemeContext'; import {ThemePreferenceWithoutSystem} from '@styles/theme/types'; +import DomUtils from '@libs/DomUtils'; const propTypes = { /** Rendered child component */ @@ -20,6 +21,10 @@ function ThemeProvider({children, theme: staticThemePreference}: ThemeProviderPr const theme = useMemo(() => themes[themePreference], [themePreference]); + useEffect(() => { + DomUtils.addCSS(DomUtils.getAutofilledInputStyle(theme.text), 'autofill-input') + }, [theme.text]); + return {children}; } diff --git a/src/libs/DomUtils/index.native.ts b/src/libs/DomUtils/index.native.ts index 0864f1a16ac0..f161e0eeeeb2 100644 --- a/src/libs/DomUtils/index.native.ts +++ b/src/libs/DomUtils/index.native.ts @@ -2,6 +2,10 @@ import GetActiveElement from './types'; const getActiveElement: GetActiveElement = () => null; +const addCSS = () => null; + +const getAutofilledInputStyle = () => null; + const requestAnimationFrame = (callback: () => void) => { if (!callback) { return; @@ -11,6 +15,8 @@ const requestAnimationFrame = (callback: () => void) => { }; export default { + addCSS, + getAutofilledInputStyle, getActiveElement, requestAnimationFrame, }; diff --git a/src/libs/DomUtils/index.ts b/src/libs/DomUtils/index.ts index 6a2eed57fbe6..001d57745f53 100644 --- a/src/libs/DomUtils/index.ts +++ b/src/libs/DomUtils/index.ts @@ -2,7 +2,64 @@ import GetActiveElement from './types'; const getActiveElement: GetActiveElement = () => document.activeElement; +const addCSS = (css: string, styleId: string) => { + var head = document.getElementsByTagName('head')[0]; + var existingStyle = document.getElementById(styleId); + + if (existingStyle) { + // If style tag with the specified ID exists, update its content + if (existingStyle.styleSheet) { // IE + existingStyle.styleSheet.cssText = css; + } else { // the world + existingStyle.innerHTML = css; + } + } else { + // If style tag doesn't exist, create a new one + var s = document.createElement('style'); + s.setAttribute("id", styleId); + s.setAttribute('type', 'text/css'); + + if (s.styleSheet) { // IE + s.styleSheet.cssText = css; + } else { // the world + s.appendChild(document.createTextNode(css)); + } + + head.appendChild(s); + } +} + +/* Customizes the background and text colors for autofill inputs in Chrome */ +/* Chrome on iOS does not support the autofill pseudo class because it is a non-standard webkit feature. +We should rely on the chrome-autofilled property being added to the input when users use auto-fill */ +const getAutofilledInputStyle = (inputTextColor: string) => ` + input[chrome-autofilled], + input[chrome-autofilled]:hover, + input[chrome-autofilled]:focus, + textarea[chrome-autofilled], + textarea[chrome-autofilled]:hover, + textarea[chrome-autofilled]:focus, + select[chrome-autofilled], + select[chrome-autofilled]:hover, + select[chrome-autofilled]:focus, + input:-webkit-autofill, + input:-webkit-autofill:hover, + input:-webkit-autofill:focus, + textarea:-webkit-autofill, + textarea:-webkit-autofill:hover, + textarea:-webkit-autofill:focus, + select:-webkit-autofill, + select:-webkit-autofill:hover, + select:-webkit-autofill:focus { + -webkit-background-clip: text; + -webkit-text-fill-color: ${inputTextColor}; + caret-color: ${inputTextColor}; + } +`; + export default { + addCSS, + getAutofilledInputStyle, getActiveElement, requestAnimationFrame: window.requestAnimationFrame.bind(window), }; diff --git a/web/index.html b/web/index.html index 967873fe586c..e83f4527a1a3 100644 --- a/web/index.html +++ b/web/index.html @@ -70,32 +70,6 @@ display: none; } - /* Customizes the background and text colors for autofill inputs in Chrome */ - /* Chrome on iOS does not support the autofill pseudo class because it is a non-standard webkit feature. - We should rely on the chrome-autofilled property being added to the input when users use auto-fill */ - input[chrome-autofilled], - input[chrome-autofilled]:hover, - input[chrome-autofilled]:focus, - textarea[chrome-autofilled], - textarea[chrome-autofilled]:hover, - textarea[chrome-autofilled]:focus, - select[chrome-autofilled], - select[chrome-autofilled]:hover, - select[chrome-autofilled]:focus, - input:-webkit-autofill, - input:-webkit-autofill:hover, - input:-webkit-autofill:focus, - textarea:-webkit-autofill, - textarea:-webkit-autofill:hover, - textarea:-webkit-autofill:focus, - select:-webkit-autofill, - select:-webkit-autofill:hover, - select:-webkit-autofill:focus { - -webkit-background-clip: text; - -webkit-text-fill-color: #ffffff; - caret-color: #ffffff; - } - /* Prevent autofill from overlapping with the input label in Chrome */ div:has(input:-webkit-autofill, input[chrome-autofilled]) > label { transform: translateY(var(--active-label-translate-y)) scale(var(--active-label-scale)) !important; From ddd452f23225c7f3409c39a9f7c2fd14145f7f89 Mon Sep 17 00:00:00 2001 From: tienifr Date: Thu, 21 Dec 2023 15:38:48 +0700 Subject: [PATCH 081/580] fix: Green dot does not appear in LHN when the request is created in workspace chat --- src/libs/actions/IOU.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index 802f0f00fffd..4653ca586039 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -302,7 +302,10 @@ function buildOnyxDataForMoneyRequest( optimisticPolicyRecentlyUsedTags, isNewChatReport, isNewIOUReport, + policyID, ) { + const isPolicyAdmin = ReportUtils.getPolicy(policyID).role === CONST.POLICY.ROLE.ADMIN; + const optimisticData = [ { // Use SET for new reports because it doesn't exist yet, is faster and we need the data to be available when we navigate to the chat page @@ -313,6 +316,7 @@ function buildOnyxDataForMoneyRequest( lastReadTime: DateUtils.getDBTime(), lastMessageTranslationKey: '', iouReportID: iouReport.reportID, + ...(isPolicyAdmin ? {hasOutstandingChildRequest: true} : {}), ...(isNewChatReport ? {pendingFields: {createChat: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD}} : {}), }, }, @@ -459,6 +463,7 @@ function buildOnyxDataForMoneyRequest( iouReportID: chatReport.iouReportID, lastReadTime: chatReport.lastReadTime, pendingFields: null, + ...(isPolicyAdmin ? {hasOutstandingChildRequest: chatReport.hasOutstandingChildRequest} : {}), ...(isNewChatReport ? { errorFields: { @@ -752,6 +757,7 @@ function getMoneyRequestInformation( optimisticPolicyRecentlyUsedTags, isNewChatReport, isNewIOUReport, + participant.policyID, ); return { @@ -1430,6 +1436,7 @@ function createSplitsAndOnyxData(participants, currentUserLogin, currentUserAcco optimisticPolicyRecentlyUsedTags, isNewOneOnOneChatReport, shouldCreateNewOneOnOneIOUReport, + participant.policyID, ); const individualSplit = { @@ -1955,6 +1962,7 @@ function completeSplitBill(chatReportID, reportAction, updatedTransaction, sessi {}, isNewOneOnOneChatReport, shouldCreateNewOneOnOneIOUReport, + participant.policyID, ); splits.push({ From b676aa93c9337ee179980d8d9ce0e0ac31872603 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Thu, 21 Dec 2023 09:49:44 +0100 Subject: [PATCH 082/580] 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 083/580] 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 084/580] 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 085/580] 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 81142e7cf9191e19c461e8704c27f2fcbe54117a Mon Sep 17 00:00:00 2001 From: tienifr Date: Fri, 22 Dec 2023 15:54:36 +0700 Subject: [PATCH 086/580] fix ts error --- src/components/ThemeProvider.tsx | 4 ++-- src/libs/DomUtils/index.ts | 40 +++++++++++++++++--------------- 2 files changed, 23 insertions(+), 21 deletions(-) diff --git a/src/components/ThemeProvider.tsx b/src/components/ThemeProvider.tsx index 41423991f051..5fe9bfec1e4a 100644 --- a/src/components/ThemeProvider.tsx +++ b/src/components/ThemeProvider.tsx @@ -2,10 +2,10 @@ import PropTypes from 'prop-types'; import React, {useEffect, useMemo} from 'react'; import useThemePreferenceWithStaticOverride from '@hooks/useThemePreferenceWithStaticOverride'; +import DomUtils from '@libs/DomUtils'; import themes from '@styles/theme'; import ThemeContext from '@styles/theme/context/ThemeContext'; import {ThemePreferenceWithoutSystem} from '@styles/theme/types'; -import DomUtils from '@libs/DomUtils'; const propTypes = { /** Rendered child component */ @@ -22,7 +22,7 @@ function ThemeProvider({children, theme: staticThemePreference}: ThemeProviderPr const theme = useMemo(() => themes[themePreference], [themePreference]); useEffect(() => { - DomUtils.addCSS(DomUtils.getAutofilledInputStyle(theme.text), 'autofill-input') + DomUtils.addCSS(DomUtils.getAutofilledInputStyle(theme.text), 'autofill-input'); }, [theme.text]); return {children}; diff --git a/src/libs/DomUtils/index.ts b/src/libs/DomUtils/index.ts index 001d57745f53..17dce79cf503 100644 --- a/src/libs/DomUtils/index.ts +++ b/src/libs/DomUtils/index.ts @@ -3,35 +3,37 @@ import GetActiveElement from './types'; const getActiveElement: GetActiveElement = () => document.activeElement; const addCSS = (css: string, styleId: string) => { - var head = document.getElementsByTagName('head')[0]; - var existingStyle = document.getElementById(styleId); + const existingStyle = document.getElementById(styleId); if (existingStyle) { - // If style tag with the specified ID exists, update its content - if (existingStyle.styleSheet) { // IE - existingStyle.styleSheet.cssText = css; - } else { // the world + if ('styleSheet' in existingStyle) { + // Supports IE8 and below + (existingStyle.styleSheet as any).cssText = css; + } else { existingStyle.innerHTML = css; } } else { - // If style tag doesn't exist, create a new one - var s = document.createElement('style'); - s.setAttribute("id", styleId); - s.setAttribute('type', 'text/css'); + const styleElement = document.createElement('style'); + styleElement.setAttribute('id', styleId); + styleElement.setAttribute('type', 'text/css'); - if (s.styleSheet) { // IE - s.styleSheet.cssText = css; - } else { // the world - s.appendChild(document.createTextNode(css)); + if ('styleSheet' in styleElement) { + // Supports IE8 and below + (styleElement.styleSheet as any).cssText = css; + } else { + styleElement.appendChild(document.createTextNode(css)); } - head.appendChild(s); + const head = document.getElementsByTagName('head')[0]; + head.appendChild(styleElement); } -} +}; -/* Customizes the background and text colors for autofill inputs in Chrome */ -/* Chrome on iOS does not support the autofill pseudo class because it is a non-standard webkit feature. -We should rely on the chrome-autofilled property being added to the input when users use auto-fill */ +/** + * Customizes the background and text colors for autofill inputs in Chrome + * Chrome on iOS does not support the autofill pseudo class because it is a non-standard webkit feature. + * We should rely on the chrome-autofilled property being added to the input when users use auto-fill + */ const getAutofilledInputStyle = (inputTextColor: string) => ` input[chrome-autofilled], input[chrome-autofilled]:hover, From d5ba5b5f9d82cb2fa7027466de5ac7864dad41c9 Mon Sep 17 00:00:00 2001 From: tienifr Date: Fri, 22 Dec 2023 16:17:26 +0700 Subject: [PATCH 087/580] fix lint --- src/libs/DomUtils/index.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/libs/DomUtils/index.ts b/src/libs/DomUtils/index.ts index 17dce79cf503..25700ca015bb 100644 --- a/src/libs/DomUtils/index.ts +++ b/src/libs/DomUtils/index.ts @@ -8,6 +8,7 @@ const addCSS = (css: string, styleId: string) => { if (existingStyle) { if ('styleSheet' in existingStyle) { // Supports IE8 and below + // eslint-disable-next-line @typescript-eslint/no-explicit-any (existingStyle.styleSheet as any).cssText = css; } else { existingStyle.innerHTML = css; @@ -19,6 +20,7 @@ const addCSS = (css: string, styleId: string) => { if ('styleSheet' in styleElement) { // Supports IE8 and below + // eslint-disable-next-line @typescript-eslint/no-explicit-any (styleElement.styleSheet as any).cssText = css; } else { styleElement.appendChild(document.createTextNode(css)); From fefd084e6af6f8d9d6b40cf85a3f7557f3ac447c Mon Sep 17 00:00:00 2001 From: brunovjk Date: Sat, 23 Dec 2023 10:37:12 -0300 Subject: [PATCH 088/580] 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 089/580] 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 090/580] 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 1da2e9a5e97000034c8926287b8c1a149d407182 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Thu, 28 Dec 2023 14:04:43 +0100 Subject: [PATCH 091/580] fix: better gestures --- src/components/MultiGestureCanvas/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/MultiGestureCanvas/index.js b/src/components/MultiGestureCanvas/index.js index c5fd2632c22d..9c98f2780b05 100644 --- a/src/components/MultiGestureCanvas/index.js +++ b/src/components/MultiGestureCanvas/index.js @@ -578,7 +578,7 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr }, ]} > - + Date: Fri, 29 Dec 2023 13:10:20 +0100 Subject: [PATCH 092/580] further improve variable names --- src/components/MultiGestureCanvas/index.js | 66 +++++++++++----------- 1 file changed, 32 insertions(+), 34 deletions(-) diff --git a/src/components/MultiGestureCanvas/index.js b/src/components/MultiGestureCanvas/index.js index 9c98f2780b05..6ff38a15981a 100644 --- a/src/components/MultiGestureCanvas/index.js +++ b/src/components/MultiGestureCanvas/index.js @@ -87,35 +87,33 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr const zoomScaledContentWidth = useDerivedValue(() => contentSize.width * totalScale.value, [contentSize.width]); const zoomScaledContentHeight = useDerivedValue(() => contentSize.height * totalScale.value, [contentSize.height]); - // used for pan gesture - const translateY = useSharedValue(0); - const translateX = useSharedValue(0); + // pan and pinch gesture const offsetX = useSharedValue(0); const offsetY = useSharedValue(0); + + // pan gesture + const panTranslateX = useSharedValue(0); + const panTranslateY = useSharedValue(0); const isSwiping = useSharedValue(false); + // pan velocity to calculate the decay + const panVelocityX = useSharedValue(0); + const panVelocityY = useSharedValue(0); + // disable pan vertically when content is smaller than screen + const canPanVertically = useDerivedValue(() => canvasSize.height < zoomScaledContentHeight.value, [canvasSize.height]); - // used for moving fingers when pinching + // pinch gesture const pinchTranslateX = useSharedValue(0); const pinchTranslateY = useSharedValue(0); const pinchBounceTranslateX = useSharedValue(0); const pinchBounceTranslateY = useSharedValue(0); - - // storage for the the origin of the gesture - const origin = { + // scale in between gestures + const pinchScaleOffset = useSharedValue(1); + // origin of the pinch gesture + const pinchOrigin = { x: useSharedValue(0), y: useSharedValue(0), }; - // storage for the pan velocity to calculate the decay - const panVelocityX = useSharedValue(0); - const panVelocityY = useSharedValue(0); - - // store scale in between gestures - const pinchScaleOffset = useSharedValue(1); - - // disable pan vertically when content is smaller than screen - const canPanVertically = useDerivedValue(() => canvasSize.height < zoomScaledContentHeight.value, [canvasSize.height]); - // calculates bounds of the scaled content // can we pan left/right/up/down // can be used to limit gesture or implementing tension effect @@ -153,14 +151,14 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr }; }, [canvasSize.width, canvasSize.height]); - const afterPanGesture = useWorkletCallback(() => { + const returnToBoundaries = useWorkletCallback(() => { const {target, isInBoundaryX, isInBoundaryY, minVector, maxVector} = getBounds(); if (!canPanVertically.value) { offsetY.value = withSpring(target.y, SPRING_CONFIG); } - if (zoomScale.value === 1 && offsetX.value === 0 && offsetY.value === 0 && translateX.value === 0 && translateY.value === 0) { + if (zoomScale.value === 1 && offsetX.value === 0 && offsetY.value === 0 && panTranslateX.value === 0 && panTranslateY.value === 0) { // we don't need to run any animations return; } @@ -285,8 +283,8 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr zoomScale.value = withSpring(1, SPRING_CONFIG); } else { zoomScale.value = 1; - translateX.value = 0; - translateY.value = 0; + panTranslateX.value = 0; + panTranslateY.value = 0; offsetX.value = 0; offsetY.value = 0; pinchTranslateX.value = 0; @@ -379,11 +377,11 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr panVelocityY.value = evt.velocityY; if (!isSwiping.value) { - translateX.value += evt.changeX; + panTranslateX.value += evt.changeX; } if (canPanVertically.value || isSwiping.value) { - translateY.value += evt.changeY; + panTranslateY.value += evt.changeY; } }) .onEnd((evt) => { @@ -393,10 +391,10 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr return; } - offsetX.value += translateX.value; - offsetY.value += translateY.value; - translateX.value = 0; - translateY.value = 0; + offsetX.value += panTranslateX.value; + offsetY.value += panTranslateY.value; + panTranslateX.value = 0; + panTranslateY.value = 0; if (isSwiping.value) { const enoughVelocity = Math.abs(evt.velocityY) > 300 && Math.abs(evt.velocityX) < Math.abs(evt.velocityY); @@ -427,7 +425,7 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr } } - afterPanGesture(); + returnToBoundaries(); panVelocityX.value = 0; panVelocityY.value = 0; @@ -462,8 +460,8 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr const adjustFocal = getAdjustedFocal(evt.focalX, evt.focalY); - origin.x.value = adjustFocal.x; - origin.y.value = adjustFocal.y; + pinchOrigin.x.value = adjustFocal.x; + pinchOrigin.y.value = adjustFocal.y; }) .onChange((evt) => { const newZoomScale = pinchScaleOffset.value * evt.scale; @@ -474,8 +472,8 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr } const adjustedFocal = getAdjustedFocal(evt.focalX, evt.focalY); - const newPinchTranslateX = adjustedFocal.x + pinchGestureScale.value * origin.x.value * -1; - const newPinchTranslateY = adjustedFocal.y + pinchGestureScale.value * origin.y.value * -1; + const newPinchTranslateX = adjustedFocal.x + pinchGestureScale.value * pinchOrigin.x.value * -1; + const newPinchTranslateY = adjustedFocal.y + pinchGestureScale.value * pinchOrigin.y.value * -1; if (zoomScale.value >= zoomRange.min && zoomScale.value <= zoomRange.max) { pinchTranslateX.value = newPinchTranslateX; @@ -527,8 +525,8 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr useEffect(() => onPinchGestureChange(isPinchGestureInUse), [isPinchGestureInUse]); const animatedStyles = useAnimatedStyle(() => { - const x = pinchTranslateX.value + pinchBounceTranslateX.value + translateX.value + offsetX.value; - const y = pinchTranslateY.value + pinchBounceTranslateY.value + translateY.value + offsetY.value; + const x = pinchTranslateX.value + pinchBounceTranslateX.value + panTranslateX.value + offsetX.value; + const y = pinchTranslateY.value + pinchBounceTranslateY.value + panTranslateY.value + offsetY.value; if (isSwiping.value) { onSwipe(y); From 4c0d2c571df616d2ddcc31e36411cb7ef5b327e2 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Fri, 29 Dec 2023 13:59:43 +0100 Subject: [PATCH 093/580] extract gesture code to hooks --- src/components/MultiGestureCanvas/index.js | 519 +++--------------- .../MultiGestureCanvas/usePanGesture.js | 237 ++++++++ .../MultiGestureCanvas/usePinchGesture.js | 115 ++++ .../MultiGestureCanvas/useTapGestures.js | 127 +++++ src/components/MultiGestureCanvas/utils.ts | 19 + 5 files changed, 581 insertions(+), 436 deletions(-) create mode 100644 src/components/MultiGestureCanvas/usePanGesture.js create mode 100644 src/components/MultiGestureCanvas/usePinchGesture.js create mode 100644 src/components/MultiGestureCanvas/useTapGestures.js create mode 100644 src/components/MultiGestureCanvas/utils.ts diff --git a/src/components/MultiGestureCanvas/index.js b/src/components/MultiGestureCanvas/index.js index 6ff38a15981a..1e8f70c9665f 100644 --- a/src/components/MultiGestureCanvas/index.js +++ b/src/components/MultiGestureCanvas/index.js @@ -1,42 +1,19 @@ import React, {useContext, useEffect, useMemo, useRef, useState} from 'react'; import {View} from 'react-native'; import {Gesture, GestureDetector} from 'react-native-gesture-handler'; -import Animated, { - cancelAnimation, - runOnJS, - runOnUI, - useAnimatedReaction, - useAnimatedStyle, - useDerivedValue, - useSharedValue, - useWorkletCallback, - withDecay, - withSpring, -} from 'react-native-reanimated'; +import Animated, {cancelAnimation, runOnJS, runOnUI, useAnimatedReaction, useAnimatedStyle, useDerivedValue, useSharedValue, useWorkletCallback, withSpring} from 'react-native-reanimated'; import AttachmentCarouselPagerContext from '@components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; import getCanvasFitScale from './getCanvasFitScale'; import {defaultZoomRange, multiGestureCanvasDefaultProps, multiGestureCanvasPropTypes} from './propTypes'; +import usePanGesture from './usePanGesture'; +import usePinchGesture from './usePinchGesture'; +import useTapGestures from './useTapGestures'; +import * as MultiGestureCanvasUtils from './utils'; -const DOUBLE_TAP_SCALE = 3; - -const zoomScaleBounceFactors = { - min: 0.7, - max: 1.5, -}; - -const SPRING_CONFIG = { - mass: 1, - stiffness: 1000, - damping: 500, -}; - -function clamp(value, lowerBound, upperBound) { - 'worklet'; - - return Math.min(Math.max(lowerBound, value), upperBound); -} +const SPRING_CONFIG = MultiGestureCanvasUtils.SPRING_CONFIG; +const zoomScaleBounceFactors = MultiGestureCanvasUtils.zoomScaleBounceFactors; function getDeepDefaultProps({contentSize: contentSizeProp = {}, zoomRange: zoomRangeProp = {}}) { const contentSize = { @@ -72,11 +49,6 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr }; const {minScale: minContentScale, maxScale: maxContentScale} = useMemo(() => getCanvasFitScale({canvasSize, contentSize}), [canvasSize, contentSize]); - const scaledWidth = useMemo(() => contentSize.width * minContentScale, [contentSize.width, minContentScale]); - const scaledHeight = useMemo(() => contentSize.height * minContentScale, [contentSize.height, minContentScale]); - - // On double tap zoom to fill, but at least 3x zoom - const doubleTapScale = useMemo(() => Math.max(DOUBLE_TAP_SCALE, maxContentScale / minContentScale), [maxContentScale, minContentScale]); const zoomScale = useSharedValue(1); // Adding together the pinch zoom scale and the initial scale to fit the content into the canvas @@ -84,9 +56,6 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr // and not smaller than needed to fit const totalScale = useDerivedValue(() => zoomScale.value * minContentScale, [minContentScale]); - const zoomScaledContentWidth = useDerivedValue(() => contentSize.width * totalScale.value, [contentSize.width]); - const zoomScaledContentHeight = useDerivedValue(() => contentSize.height * totalScale.value, [contentSize.height]); - // pan and pinch gesture const offsetX = useSharedValue(0); const offsetY = useSharedValue(0); @@ -95,11 +64,6 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr const panTranslateX = useSharedValue(0); const panTranslateY = useSharedValue(0); const isSwiping = useSharedValue(false); - // pan velocity to calculate the decay - const panVelocityX = useSharedValue(0); - const panVelocityY = useSharedValue(0); - // disable pan vertically when content is smaller than screen - const canPanVertically = useDerivedValue(() => canvasSize.height < zoomScaledContentHeight.value, [canvasSize.height]); // pinch gesture const pinchTranslateX = useSharedValue(0); @@ -108,170 +72,12 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr const pinchBounceTranslateY = useSharedValue(0); // scale in between gestures const pinchScaleOffset = useSharedValue(1); - // origin of the pinch gesture - const pinchOrigin = { - x: useSharedValue(0), - y: useSharedValue(0), - }; - - // calculates bounds of the scaled content - // can we pan left/right/up/down - // can be used to limit gesture or implementing tension effect - const getBounds = useWorkletCallback(() => { - let rightBoundary = 0; - let topBoundary = 0; - - if (canvasSize.width < zoomScaledContentWidth.value) { - rightBoundary = Math.abs(canvasSize.width - zoomScaledContentWidth.value) / 2; - } - - if (canvasSize.height < zoomScaledContentHeight.value) { - topBoundary = Math.abs(zoomScaledContentHeight.value - canvasSize.height) / 2; - } - - const maxVector = {x: rightBoundary, y: topBoundary}; - const minVector = {x: -rightBoundary, y: -topBoundary}; - - const target = { - x: clamp(offsetX.value, minVector.x, maxVector.x), - y: clamp(offsetY.value, minVector.y, maxVector.y), - }; - - const isInBoundaryX = target.x === offsetX.value; - const isInBoundaryY = target.y === offsetY.value; - - return { - target, - isInBoundaryX, - isInBoundaryY, - minVector, - maxVector, - canPanLeft: target.x < maxVector.x, - canPanRight: target.x > minVector.x, - }; - }, [canvasSize.width, canvasSize.height]); - - const returnToBoundaries = useWorkletCallback(() => { - const {target, isInBoundaryX, isInBoundaryY, minVector, maxVector} = getBounds(); - - if (!canPanVertically.value) { - offsetY.value = withSpring(target.y, SPRING_CONFIG); - } - - if (zoomScale.value === 1 && offsetX.value === 0 && offsetY.value === 0 && panTranslateX.value === 0 && panTranslateY.value === 0) { - // we don't need to run any animations - return; - } - - if (zoomScale.value <= 1) { - // just center it - offsetX.value = withSpring(0, SPRING_CONFIG); - offsetY.value = withSpring(0, SPRING_CONFIG); - return; - } - - const deceleration = 0.9915; - - if (isInBoundaryX) { - if (Math.abs(panVelocityX.value) > 0 && zoomScale.value <= zoomRange.max) { - offsetX.value = withDecay({ - velocity: panVelocityX.value, - clamp: [minVector.x, maxVector.x], - deceleration, - rubberBandEffect: false, - }); - } - } else { - offsetX.value = withSpring(target.x, SPRING_CONFIG); - } - - if (isInBoundaryY) { - if ( - Math.abs(panVelocityY.value) > 0 && - zoomScale.value <= zoomRange.max && - // limit vertical pan only when content is smaller than screen - offsetY.value !== minVector.y && - offsetY.value !== maxVector.y - ) { - offsetY.value = withDecay({ - velocity: panVelocityY.value, - clamp: [minVector.y, maxVector.y], - deceleration, - }); - } - } else { - offsetY.value = withSpring(target.y, SPRING_CONFIG, () => { - isSwiping.value = false; - }); - } - }); const stopAnimation = useWorkletCallback(() => { cancelAnimation(offsetX); cancelAnimation(offsetY); }); - const zoomToCoordinates = useWorkletCallback( - (canvasFocalX, canvasFocalY) => { - 'worklet'; - - stopAnimation(); - - const canvasOffsetX = Math.max(0, (canvasSize.width - scaledWidth) / 2); - const canvasOffsetY = Math.max(0, (canvasSize.height - scaledHeight) / 2); - - const contentFocal = { - x: clamp(canvasFocalX - canvasOffsetX, 0, scaledWidth), - y: clamp(canvasFocalY - canvasOffsetY, 0, scaledHeight), - }; - - const canvasCenter = { - x: canvasSize.width / 2, - y: canvasSize.height / 2, - }; - - const originContentCenter = { - x: scaledWidth / 2, - y: scaledHeight / 2, - }; - - const targetContentSize = { - width: scaledWidth * doubleTapScale, - height: scaledHeight * doubleTapScale, - }; - - const targetContentCenter = { - x: targetContentSize.width / 2, - y: targetContentSize.height / 2, - }; - - const currentOrigin = { - x: (targetContentCenter.x - canvasCenter.x) * -1, - y: (targetContentCenter.y - canvasCenter.y) * -1, - }; - - const koef = { - x: (1 / originContentCenter.x) * contentFocal.x - 1, - y: (1 / originContentCenter.y) * contentFocal.y - 1, - }; - - const target = { - x: currentOrigin.x * koef.x, - y: currentOrigin.y * koef.y, - }; - - if (targetContentSize.height < canvasSize.height) { - target.y = 0; - } - - offsetX.value = withSpring(target.x, SPRING_CONFIG); - offsetY.value = withSpring(target.y, SPRING_CONFIG); - zoomScale.value = withSpring(doubleTapScale, SPRING_CONFIG); - pinchScaleOffset.value = doubleTapScale; - }, - [scaledWidth, scaledHeight, canvasSize, doubleTapScale], - ); - const reset = useWorkletCallback((animated) => { pinchScaleOffset.value = 1; @@ -282,235 +88,76 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr offsetY.value = withSpring(0, SPRING_CONFIG); zoomScale.value = withSpring(1, SPRING_CONFIG); } else { + offsetX.value = 0; + offsetY.value = 0; zoomScale.value = 1; panTranslateX.value = 0; panTranslateY.value = 0; - offsetX.value = 0; - offsetY.value = 0; pinchTranslateX.value = 0; pinchTranslateY.value = 0; } }); - const doubleTap = Gesture.Tap() - .numberOfTaps(2) - .maxDelay(150) - .maxDistance(20) - .onEnd((evt) => { - if (zoomScale.value > 1) { - reset(true); - } else { - zoomToCoordinates(evt.x, evt.y); - } - - if (onScaleChanged != null) { - runOnJS(onScaleChanged)(zoomScale.value); - } - }); - const panGestureRef = useRef(Gesture.Pan()); - const singleTap = Gesture.Tap() - .numberOfTaps(1) - .maxDuration(50) - .requireExternalGestureToFail(doubleTap, panGestureRef) - .onBegin(() => { - stopAnimation(); - }) - .onFinalize((evt, success) => { - if (!success || !onTap) { - return; - } - - runOnJS(onTap)(); - }); - - const previousTouch = useSharedValue(null); - - const panGesture = Gesture.Pan() - .manualActivation(true) - .averageTouches(true) - .onTouchesMove((evt, state) => { - if (zoomScale.value > 1) { - state.activate(); - } - - // TODO: Swipe down to close carousel gesture - // this needs fine tuning to work properly - // if (!isScrolling.value && scale.value === 1 && previousTouch.value != null) { - // const velocityX = Math.abs(evt.allTouches[0].x - previousTouch.value.x); - // const velocityY = evt.allTouches[0].y - previousTouch.value.y; - - // // TODO: this needs tuning - // if (Math.abs(velocityY) > velocityX && velocityY > 20) { - // state.activate(); - - // isSwiping.value = true; - // previousTouch.value = null; - - // runOnJS(onSwipeDown)(); - // return; - // } - // } - - if (previousTouch.value == null) { - previousTouch.value = { - x: evt.allTouches[0].x, - y: evt.allTouches[0].y, - }; - } - }) - .simultaneousWithExternalGesture(pagerRef, doubleTap, singleTap) - .onBegin(() => { - stopAnimation(); - }) - .onChange((evt) => { - // since we running both pinch and pan gesture handlers simultaneously - // we need to make sure that we don't pan when we pinch and move fingers - // since we track it as pinch focal gesture - if (evt.numberOfPointers > 1 || isScrolling.value) { - return; - } - - panVelocityX.value = evt.velocityX; - - panVelocityY.value = evt.velocityY; - - if (!isSwiping.value) { - panTranslateX.value += evt.changeX; - } - - if (canPanVertically.value || isSwiping.value) { - panTranslateY.value += evt.changeY; - } - }) - .onEnd((evt) => { - previousTouch.value = null; - - if (isScrolling.value) { - return; - } - - offsetX.value += panTranslateX.value; - offsetY.value += panTranslateY.value; - panTranslateX.value = 0; - panTranslateY.value = 0; - - if (isSwiping.value) { - const enoughVelocity = Math.abs(evt.velocityY) > 300 && Math.abs(evt.velocityX) < Math.abs(evt.velocityY); - const rightDirection = (evt.translationY > 0 && evt.velocityY > 0) || (evt.translationY < 0 && evt.velocityY < 0); - - if (enoughVelocity && rightDirection) { - const maybeInvert = (v) => { - const invert = evt.velocityY < 0; - return invert ? -v : v; - }; - - offsetY.value = withSpring( - maybeInvert(contentSize.height * 2), - { - stiffness: 50, - damping: 30, - mass: 1, - overshootClamping: true, - restDisplacementThreshold: 300, - restSpeedThreshold: 300, - velocity: Math.abs(evt.velocityY) < 1200 ? maybeInvert(1200) : evt.velocityY, - }, - () => { - runOnJS(onSwipeSuccess)(); - }, - ); - return; - } - } - - returnToBoundaries(); + const {singleTap, doubleTap} = useTapGestures({ + canvasSize, + contentSize, + minContentScale, + maxContentScale, + panGestureRef, + offsetX, + offsetY, + pinchScaleOffset, + zoomScale, + reset, + stopAnimation, + onScaleChanged, + onTap, + }); - panVelocityX.value = 0; - panVelocityY.value = 0; - }) - .withRef(panGestureRef); + const panGesture = usePanGesture({ + canvasSize, + contentSize, + panGestureRef, + pagerRef, + singleTap, + doubleTap, + zoomScale, + zoomRange, + totalScale, + offsetX, + offsetY, + panTranslateX, + panTranslateY, + isSwiping, + isScrolling, + onSwipeSuccess, + stopAnimation, + }); - const getAdjustedFocal = useWorkletCallback( - (focalX, focalY) => ({ - x: focalX - (canvasSize.width / 2 + offsetX.value), - y: focalY - (canvasSize.height / 2 + offsetY.value), - }), - [canvasSize.width, canvasSize.height], - ); + const pinchGesture = usePinchGesture({ + canvasSize, + contentSize, + panGestureRef, + pagerRef, + singleTap, + doubleTap, + zoomScale, + zoomRange, + totalScale, + offsetX, + offsetY, + panTranslateX, + panTranslateY, + isSwiping, + isScrolling, + onSwipeSuccess, + stopAnimation, + }); - // used to store event scale value when we limit scale - const pinchGestureScale = useSharedValue(1); + // Triggers "onPinchGestureChange" callback when pinch scale changes const pinchGestureRunning = useSharedValue(false); - const pinchGesture = Gesture.Pinch() - .onTouchesDown((evt, state) => { - // we don't want to activate pinch gesture when we are scrolling pager - if (!isScrolling.value) { - return; - } - - state.fail(); - }) - .simultaneousWithExternalGesture(panGesture, doubleTap) - .onStart((evt) => { - pinchGestureRunning.value = true; - - stopAnimation(); - - const adjustFocal = getAdjustedFocal(evt.focalX, evt.focalY); - - pinchOrigin.x.value = adjustFocal.x; - pinchOrigin.y.value = adjustFocal.y; - }) - .onChange((evt) => { - const newZoomScale = pinchScaleOffset.value * evt.scale; - - if (zoomScale.value >= zoomRange.min * zoomScaleBounceFactors.min && zoomScale.value <= zoomRange.max * zoomScaleBounceFactors.max) { - zoomScale.value = newZoomScale; - pinchGestureScale.value = evt.scale; - } - - const adjustedFocal = getAdjustedFocal(evt.focalX, evt.focalY); - const newPinchTranslateX = adjustedFocal.x + pinchGestureScale.value * pinchOrigin.x.value * -1; - const newPinchTranslateY = adjustedFocal.y + pinchGestureScale.value * pinchOrigin.y.value * -1; - - if (zoomScale.value >= zoomRange.min && zoomScale.value <= zoomRange.max) { - pinchTranslateX.value = newPinchTranslateX; - pinchTranslateY.value = newPinchTranslateY; - } else { - pinchBounceTranslateX.value = newPinchTranslateX - pinchTranslateX.value; - pinchBounceTranslateY.value = newPinchTranslateY - pinchTranslateY.value; - } - }) - .onEnd(() => { - offsetX.value += pinchTranslateX.value; - offsetY.value += pinchTranslateY.value; - pinchTranslateX.value = 0; - pinchTranslateY.value = 0; - pinchScaleOffset.value = zoomScale.value; - pinchGestureScale.value = 1; - - if (pinchScaleOffset.value < zoomRange.min) { - pinchScaleOffset.value = zoomRange.min; - zoomScale.value = withSpring(zoomRange.min, SPRING_CONFIG); - } else if (pinchScaleOffset.value > zoomRange.max) { - pinchScaleOffset.value = zoomRange.max; - zoomScale.value = withSpring(zoomRange.max, SPRING_CONFIG); - } - - if (pinchBounceTranslateX.value !== 0 || pinchBounceTranslateY.value !== 0) { - pinchBounceTranslateX.value = withSpring(0, SPRING_CONFIG); - pinchBounceTranslateY.value = withSpring(0, SPRING_CONFIG); - } - - pinchGestureRunning.value = false; - - if (onScaleChanged != null) { - runOnJS(onScaleChanged)(zoomScale.value); - } - }); - const [isPinchGestureInUse, setIsPinchGestureInUse] = useState(false); useAnimatedReaction( () => [zoomScale.value, pinchGestureRunning.value], @@ -524,6 +171,26 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr // eslint-disable-next-line react-hooks/exhaustive-deps useEffect(() => onPinchGestureChange(isPinchGestureInUse), [isPinchGestureInUse]); + // reacts to scale change and enables/disables pager scroll + useAnimatedReaction( + () => zoomScale.value, + () => { + shouldPagerScroll.value = zoomScale.value === 1; + }, + ); + + const mounted = useRef(false); + useEffect(() => { + if (!mounted.current) { + mounted.current = true; + return; + } + + if (!isActive) { + runOnUI(reset)(false); + } + }, [isActive, mounted, reset]); + const animatedStyles = useAnimatedStyle(() => { const x = pinchTranslateX.value + pinchBounceTranslateX.value + panTranslateX.value + offsetX.value; const y = pinchTranslateY.value + pinchBounceTranslateY.value + panTranslateY.value + offsetY.value; @@ -545,26 +212,6 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr }; }); - // reacts to scale change and enables/disables pager scroll - useAnimatedReaction( - () => zoomScale.value, - () => { - shouldPagerScroll.value = zoomScale.value === 1; - }, - ); - - const mounted = useRef(false); - useEffect(() => { - if (!mounted.current) { - mounted.current = true; - return; - } - - if (!isActive) { - runOnUI(reset)(false); - } - }, [isActive, mounted, reset]); - return ( { + const zoomScaledContentWidth = useDerivedValue(() => contentSize.width * totalScale.value, [contentSize.width]); + const zoomScaledContentHeight = useDerivedValue(() => contentSize.height * totalScale.value, [contentSize.height]); + + // pan velocity to calculate the decay + const panVelocityX = useSharedValue(0); + const panVelocityY = useSharedValue(0); + // disable pan vertically when content is smaller than screen + const canPanVertically = useDerivedValue(() => canvasSize.height < zoomScaledContentHeight.value, [canvasSize.height]); + + const previousTouch = useSharedValue(null); + + // calculates bounds of the scaled content + // can we pan left/right/up/down + // can be used to limit gesture or implementing tension effect + const getBounds = useWorkletCallback(() => { + let rightBoundary = 0; + let topBoundary = 0; + + if (canvasSize.width < zoomScaledContentWidth.value) { + rightBoundary = Math.abs(canvasSize.width - zoomScaledContentWidth.value) / 2; + } + + if (canvasSize.height < zoomScaledContentHeight.value) { + topBoundary = Math.abs(zoomScaledContentHeight.value - canvasSize.height) / 2; + } + + const maxVector = {x: rightBoundary, y: topBoundary}; + const minVector = {x: -rightBoundary, y: -topBoundary}; + + const target = { + x: clamp(offsetX.value, minVector.x, maxVector.x), + y: clamp(offsetY.value, minVector.y, maxVector.y), + }; + + const isInBoundaryX = target.x === offsetX.value; + const isInBoundaryY = target.y === offsetY.value; + + return { + target, + isInBoundaryX, + isInBoundaryY, + minVector, + maxVector, + canPanLeft: target.x < maxVector.x, + canPanRight: target.x > minVector.x, + }; + }, [canvasSize.width, canvasSize.height]); + + const returnToBoundaries = useWorkletCallback(() => { + const {target, isInBoundaryX, isInBoundaryY, minVector, maxVector} = getBounds(); + + if (zoomScale.value === zoomRange.min && offsetX.value === 0 && offsetY.value === 0 && panTranslateX.value === 0 && panTranslateY.value === 0) { + // we don't need to run any animations + return; + } + + if (zoomScale.value <= zoomRange.min) { + // just center it + offsetX.value = withSpring(0, SPRING_CONFIG); + offsetY.value = withSpring(0, SPRING_CONFIG); + return; + } + + if (isInBoundaryX) { + if (Math.abs(panVelocityX.value) > 0 && zoomScale.value <= zoomRange.max) { + offsetX.value = withDecay({ + velocity: panVelocityX.value, + clamp: [minVector.x, maxVector.x], + deceleration: PAN_DECAY_DECELARATION, + rubberBandEffect: false, + }); + } + } else { + offsetX.value = withSpring(target.x, SPRING_CONFIG); + } + + if (!canPanVertically.value) { + offsetY.value = withSpring(target.y, SPRING_CONFIG); + } else if (isInBoundaryY) { + if ( + Math.abs(panVelocityY.value) > 0 && + zoomScale.value <= zoomRange.max && + // limit vertical pan only when content is smaller than screen + offsetY.value !== minVector.y && + offsetY.value !== maxVector.y + ) { + offsetY.value = withDecay({ + velocity: panVelocityY.value, + clamp: [minVector.y, maxVector.y], + deceleration: PAN_DECAY_DECELARATION, + }); + } + } else { + offsetY.value = withSpring(target.y, SPRING_CONFIG, () => { + isSwiping.value = false; + }); + } + }); + + const panGesture = Gesture.Pan() + .manualActivation(true) + .averageTouches(true) + .onTouchesMove((evt, state) => { + if (zoomScale.value > 1) { + state.activate(); + } + + // TODO: Swipe down to close carousel gesture + // this needs fine tuning to work properly + // if (!isScrolling.value && scale.value === 1 && previousTouch.value != null) { + // const velocityX = Math.abs(evt.allTouches[0].x - previousTouch.value.x); + // const velocityY = evt.allTouches[0].y - previousTouch.value.y; + + // // TODO: this needs tuning + // if (Math.abs(velocityY) > velocityX && velocityY > 20) { + // state.activate(); + + // isSwiping.value = true; + // previousTouch.value = null; + + // runOnJS(onSwipeDown)(); + // return; + // } + // } + + if (previousTouch.value == null) { + previousTouch.value = { + x: evt.allTouches[0].x, + y: evt.allTouches[0].y, + }; + } + }) + .simultaneousWithExternalGesture(pagerRef, singleTap, doubleTap) + .onBegin(() => { + stopAnimation(); + }) + .onChange((evt) => { + // since we're running both pinch and pan gesture handlers simultaneously + // we need to make sure that we don't pan when we pinch and move fingers + // since we track it as pinch focal gesture + if (evt.numberOfPointers > 1 || isScrolling.value) { + return; + } + + panVelocityX.value = evt.velocityX; + + panVelocityY.value = evt.velocityY; + + if (!isSwiping.value) { + panTranslateX.value += evt.changeX; + } + + if (canPanVertically.value || isSwiping.value) { + panTranslateY.value += evt.changeY; + } + }) + .onEnd((evt) => { + previousTouch.value = null; + + if (isScrolling.value) { + return; + } + + offsetX.value += panTranslateX.value; + offsetY.value += panTranslateY.value; + panTranslateX.value = 0; + panTranslateY.value = 0; + + if (isSwiping.value) { + const enoughVelocity = Math.abs(evt.velocityY) > 300 && Math.abs(evt.velocityX) < Math.abs(evt.velocityY); + const rightDirection = (evt.translationY > 0 && evt.velocityY > 0) || (evt.translationY < 0 && evt.velocityY < 0); + + if (enoughVelocity && rightDirection) { + const maybeInvert = (v) => { + const invert = evt.velocityY < 0; + return invert ? -v : v; + }; + + offsetY.value = withSpring( + maybeInvert(contentSize.height * 2), + { + stiffness: 50, + damping: 30, + mass: 1, + overshootClamping: true, + restDisplacementThreshold: 300, + restSpeedThreshold: 300, + velocity: Math.abs(evt.velocityY) < 1200 ? maybeInvert(1200) : evt.velocityY, + }, + () => { + runOnJS(onSwipeSuccess)(); + }, + ); + return; + } + } + + returnToBoundaries(); + + panVelocityX.value = 0; + panVelocityY.value = 0; + }) + .withRef(panGestureRef); + + return panGesture; +}; + +export default usePanGesture; diff --git a/src/components/MultiGestureCanvas/usePinchGesture.js b/src/components/MultiGestureCanvas/usePinchGesture.js new file mode 100644 index 000000000000..9a7c8b9cf7b7 --- /dev/null +++ b/src/components/MultiGestureCanvas/usePinchGesture.js @@ -0,0 +1,115 @@ +/* eslint-disable no-param-reassign */ +import {Gesture} from 'react-native-gesture-handler'; +import {runOnJS, useSharedValue, useWorkletCallback, withSpring} from 'react-native-reanimated'; +import * as MultiGestureCanvasUtils from './utils'; + +const SPRING_CONFIG = MultiGestureCanvasUtils.SPRING_CONFIG; +const zoomScaleBounceFactors = MultiGestureCanvasUtils.zoomScaleBounceFactors; + +const usePinchGesture = ({ + canvasSize, + singleTap, + doubleTap, + panGesture, + zoomScale, + zoomRange, + offsetX, + offsetY, + pinchTranslateX, + pinchTranslateY, + pinchBounceTranslateX, + pinchBounceTranslateY, + pinchScaleOffset, + pinchGestureRunning, + isScrolling, + stopAnimation, + onScaleChanged, +}) => { + // used to store event scale value when we limit scale + const pinchGestureScale = useSharedValue(1); + + // origin of the pinch gesture + const pinchOrigin = { + x: useSharedValue(0), + y: useSharedValue(0), + }; + + const getAdjustedFocal = useWorkletCallback( + (focalX, focalY) => ({ + x: focalX - (canvasSize.width / 2 + offsetX.value), + y: focalY - (canvasSize.height / 2 + offsetY.value), + }), + [canvasSize.width, canvasSize.height], + ); + const pinchGesture = Gesture.Pinch() + .onTouchesDown((evt, state) => { + // we don't want to activate pinch gesture when we are scrolling pager + if (!isScrolling.value) { + return; + } + + state.fail(); + }) + .simultaneousWithExternalGesture(panGesture, singleTap, doubleTap) + .onStart((evt) => { + pinchGestureRunning.value = true; + + stopAnimation(); + + const adjustFocal = getAdjustedFocal(evt.focalX, evt.focalY); + + pinchOrigin.x.value = adjustFocal.x; + pinchOrigin.y.value = adjustFocal.y; + }) + .onChange((evt) => { + const newZoomScale = pinchScaleOffset.value * evt.scale; + + if (zoomScale.value >= zoomRange.min * zoomScaleBounceFactors.min && zoomScale.value <= zoomRange.max * zoomScaleBounceFactors.max) { + zoomScale.value = newZoomScale; + pinchGestureScale.value = evt.scale; + } + + const adjustedFocal = getAdjustedFocal(evt.focalX, evt.focalY); + const newPinchTranslateX = adjustedFocal.x + pinchGestureScale.value * pinchOrigin.x.value * -1; + const newPinchTranslateY = adjustedFocal.y + pinchGestureScale.value * pinchOrigin.y.value * -1; + + if (zoomScale.value >= zoomRange.min && zoomScale.value <= zoomRange.max) { + pinchTranslateX.value = newPinchTranslateX; + pinchTranslateY.value = newPinchTranslateY; + } else { + pinchBounceTranslateX.value = newPinchTranslateX - pinchTranslateX.value; + pinchBounceTranslateY.value = newPinchTranslateY - pinchTranslateY.value; + } + }) + .onEnd(() => { + offsetX.value += pinchTranslateX.value; + offsetY.value += pinchTranslateY.value; + pinchTranslateX.value = 0; + pinchTranslateY.value = 0; + pinchScaleOffset.value = zoomScale.value; + pinchGestureScale.value = 1; + + if (pinchScaleOffset.value < zoomRange.min) { + pinchScaleOffset.value = zoomRange.min; + zoomScale.value = withSpring(zoomRange.min, SPRING_CONFIG); + } else if (pinchScaleOffset.value > zoomRange.max) { + pinchScaleOffset.value = zoomRange.max; + zoomScale.value = withSpring(zoomRange.max, SPRING_CONFIG); + } + + if (pinchBounceTranslateX.value !== 0 || pinchBounceTranslateY.value !== 0) { + pinchBounceTranslateX.value = withSpring(0, SPRING_CONFIG); + pinchBounceTranslateY.value = withSpring(0, SPRING_CONFIG); + } + + pinchGestureRunning.value = false; + + if (onScaleChanged != null) { + runOnJS(onScaleChanged)(zoomScale.value); + } + }); + + return pinchGesture; +}; + +export default usePinchGesture; diff --git a/src/components/MultiGestureCanvas/useTapGestures.js b/src/components/MultiGestureCanvas/useTapGestures.js new file mode 100644 index 000000000000..0af5076618a4 --- /dev/null +++ b/src/components/MultiGestureCanvas/useTapGestures.js @@ -0,0 +1,127 @@ +/* eslint-disable no-param-reassign */ +import {useMemo} from 'react'; +import {Gesture} from 'react-native-gesture-handler'; +import {runOnJS, useWorkletCallback, withSpring} from 'react-native-reanimated'; +import * as MultiGestureCanvasUtils from './utils'; + +const clamp = MultiGestureCanvasUtils.clamp; +const DOUBLE_TAP_SCALE = MultiGestureCanvasUtils.DOUBLE_TAP_SCALE; +const SPRING_CONFIG = MultiGestureCanvasUtils.SPRING_CONFIG; + +const useTapGestures = ({ + canvasSize, + contentSize, + minContentScale, + maxContentScale, + panGestureRef, + offsetX, + offsetY, + pinchScaleOffset, + zoomScale, + reset, + stopAnimation, + onScaleChanged, + onTap, +}) => { + const scaledWidth = useMemo(() => contentSize.width * minContentScale, [contentSize.width, minContentScale]); + const scaledHeight = useMemo(() => contentSize.height * minContentScale, [contentSize.height, minContentScale]); + + // On double tap zoom to fill, but at least 3x zoom + const doubleTapScale = useMemo(() => Math.max(DOUBLE_TAP_SCALE, maxContentScale / minContentScale), [maxContentScale, minContentScale]); + + const zoomToCoordinates = useWorkletCallback( + (canvasFocalX, canvasFocalY) => { + 'worklet'; + + stopAnimation(); + + const canvasOffsetX = Math.max(0, (canvasSize.width - scaledWidth) / 2); + const canvasOffsetY = Math.max(0, (canvasSize.height - scaledHeight) / 2); + + const contentFocal = { + x: clamp(canvasFocalX - canvasOffsetX, 0, scaledWidth), + y: clamp(canvasFocalY - canvasOffsetY, 0, scaledHeight), + }; + + const canvasCenter = { + x: canvasSize.width / 2, + y: canvasSize.height / 2, + }; + + const originContentCenter = { + x: scaledWidth / 2, + y: scaledHeight / 2, + }; + + const targetContentSize = { + width: scaledWidth * doubleTapScale, + height: scaledHeight * doubleTapScale, + }; + + const targetContentCenter = { + x: targetContentSize.width / 2, + y: targetContentSize.height / 2, + }; + + const currentOrigin = { + x: (targetContentCenter.x - canvasCenter.x) * -1, + y: (targetContentCenter.y - canvasCenter.y) * -1, + }; + + const koef = { + x: (1 / originContentCenter.x) * contentFocal.x - 1, + y: (1 / originContentCenter.y) * contentFocal.y - 1, + }; + + const target = { + x: currentOrigin.x * koef.x, + y: currentOrigin.y * koef.y, + }; + + if (targetContentSize.height < canvasSize.height) { + target.y = 0; + } + + offsetX.value = withSpring(target.x, SPRING_CONFIG); + offsetY.value = withSpring(target.y, SPRING_CONFIG); + zoomScale.value = withSpring(doubleTapScale, SPRING_CONFIG); + pinchScaleOffset.value = doubleTapScale; + }, + [scaledWidth, scaledHeight, canvasSize, doubleTapScale], + ); + + const doubleTap = Gesture.Tap() + .numberOfTaps(2) + .maxDelay(150) + .maxDistance(20) + .onEnd((evt) => { + if (zoomScale.value > 1) { + reset(true); + } else { + zoomToCoordinates(evt.x, evt.y); + } + + if (onScaleChanged != null) { + runOnJS(onScaleChanged)(zoomScale.value); + } + }); + + const singleTap = Gesture.Tap() + .numberOfTaps(1) + .maxDuration(50) + .requireExternalGestureToFail(doubleTap, panGestureRef) + .onBegin(() => { + stopAnimation(); + }) + .onFinalize((_evt, success) => { + if (!success || !onTap) { + return; + } + + runOnJS(onTap)(); + }); + + return {singleTap, doubleTap}; +}; + +export default useTapGestures; diff --git a/src/components/MultiGestureCanvas/utils.ts b/src/components/MultiGestureCanvas/utils.ts new file mode 100644 index 000000000000..c28f02ed0f39 --- /dev/null +++ b/src/components/MultiGestureCanvas/utils.ts @@ -0,0 +1,19 @@ +const DOUBLE_TAP_SCALE = 3; + +const SPRING_CONFIG = { + mass: 1, + stiffness: 1000, + damping: 500, +}; + +const zoomScaleBounceFactors = { + min: 0.7, + max: 1.5, +}; +function clamp(value: number, lowerBound: number, upperBound: number) { + 'worklet'; + + return Math.min(Math.max(lowerBound, value), upperBound); +} + +export {clamp, DOUBLE_TAP_SCALE, SPRING_CONFIG, zoomScaleBounceFactors}; From 2632f75d6bcc0f336649054efa4052b453e099a1 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Fri, 29 Dec 2023 14:07:43 +0100 Subject: [PATCH 094/580] further simplify --- src/components/MultiGestureCanvas/index.js | 40 ++++++------------- .../MultiGestureCanvas/usePinchGesture.js | 21 ++++++++-- 2 files changed, 30 insertions(+), 31 deletions(-) diff --git a/src/components/MultiGestureCanvas/index.js b/src/components/MultiGestureCanvas/index.js index 1e8f70c9665f..70713bf3d80c 100644 --- a/src/components/MultiGestureCanvas/index.js +++ b/src/components/MultiGestureCanvas/index.js @@ -1,7 +1,7 @@ -import React, {useContext, useEffect, useMemo, useRef, useState} from 'react'; +import React, {useContext, useEffect, useMemo, useRef} from 'react'; import {View} from 'react-native'; import {Gesture, GestureDetector} from 'react-native-gesture-handler'; -import Animated, {cancelAnimation, runOnJS, runOnUI, useAnimatedReaction, useAnimatedStyle, useDerivedValue, useSharedValue, useWorkletCallback, withSpring} from 'react-native-reanimated'; +import Animated, {cancelAnimation, runOnUI, useAnimatedReaction, useAnimatedStyle, useDerivedValue, useSharedValue, useWorkletCallback, withSpring} from 'react-native-reanimated'; import AttachmentCarouselPagerContext from '@components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -56,7 +56,7 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr // and not smaller than needed to fit const totalScale = useDerivedValue(() => zoomScale.value * minContentScale, [minContentScale]); - // pan and pinch gesture + // stored offset of the canvas (used for panning and pinching) const offsetX = useSharedValue(0); const offsetY = useSharedValue(0); @@ -64,6 +64,7 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr const panTranslateX = useSharedValue(0); const panTranslateY = useSharedValue(0); const isSwiping = useSharedValue(false); + const panGestureRef = useRef(Gesture.Pan()); // pinch gesture const pinchTranslateX = useSharedValue(0); @@ -98,8 +99,6 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr } }); - const panGestureRef = useRef(Gesture.Pan()); - const {singleTap, doubleTap} = useTapGestures({ canvasSize, contentSize, @@ -138,39 +137,24 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr const pinchGesture = usePinchGesture({ canvasSize, - contentSize, - panGestureRef, - pagerRef, singleTap, doubleTap, + panGesture, zoomScale, zoomRange, - totalScale, offsetX, offsetY, - panTranslateX, - panTranslateY, - isSwiping, + pinchTranslateX, + pinchTranslateY, + pinchBounceTranslateX, + pinchBounceTranslateY, + pinchScaleOffset, isScrolling, - onSwipeSuccess, stopAnimation, + onScaleChanged, + onPinchGestureChange, }); - // Triggers "onPinchGestureChange" callback when pinch scale changes - const pinchGestureRunning = useSharedValue(false); - const [isPinchGestureInUse, setIsPinchGestureInUse] = useState(false); - useAnimatedReaction( - () => [zoomScale.value, pinchGestureRunning.value], - ([zoom, running]) => { - const newIsPinchGestureInUse = zoom !== 1 || running; - if (isPinchGestureInUse !== newIsPinchGestureInUse) { - runOnJS(setIsPinchGestureInUse)(newIsPinchGestureInUse); - } - }, - ); - // eslint-disable-next-line react-hooks/exhaustive-deps - useEffect(() => onPinchGestureChange(isPinchGestureInUse), [isPinchGestureInUse]); - // reacts to scale change and enables/disables pager scroll useAnimatedReaction( () => zoomScale.value, diff --git a/src/components/MultiGestureCanvas/usePinchGesture.js b/src/components/MultiGestureCanvas/usePinchGesture.js index 9a7c8b9cf7b7..544baa1d045e 100644 --- a/src/components/MultiGestureCanvas/usePinchGesture.js +++ b/src/components/MultiGestureCanvas/usePinchGesture.js @@ -1,6 +1,7 @@ /* eslint-disable no-param-reassign */ +import {useEffect, useState} from 'react'; import {Gesture} from 'react-native-gesture-handler'; -import {runOnJS, useSharedValue, useWorkletCallback, withSpring} from 'react-native-reanimated'; +import {runOnJS, useAnimatedReaction, useSharedValue, useWorkletCallback, withSpring} from 'react-native-reanimated'; import * as MultiGestureCanvasUtils from './utils'; const SPRING_CONFIG = MultiGestureCanvasUtils.SPRING_CONFIG; @@ -20,14 +21,14 @@ const usePinchGesture = ({ pinchBounceTranslateX, pinchBounceTranslateY, pinchScaleOffset, - pinchGestureRunning, isScrolling, stopAnimation, onScaleChanged, + onPinchGestureChange, }) => { // used to store event scale value when we limit scale const pinchGestureScale = useSharedValue(1); - + const pinchGestureRunning = useSharedValue(false); // origin of the pinch gesture const pinchOrigin = { x: useSharedValue(0), @@ -109,6 +110,20 @@ const usePinchGesture = ({ } }); + // Triggers "onPinchGestureChange" callback when pinch scale changes + const [isPinchGestureInUse, setIsPinchGestureInUse] = useState(false); + useAnimatedReaction( + () => [zoomScale.value, pinchGestureRunning.value], + ([zoom, running]) => { + const newIsPinchGestureInUse = zoom !== 1 || running; + if (isPinchGestureInUse !== newIsPinchGestureInUse) { + runOnJS(setIsPinchGestureInUse)(newIsPinchGestureInUse); + } + }, + ); + // eslint-disable-next-line react-hooks/exhaustive-deps + useEffect(() => onPinchGestureChange(isPinchGestureInUse), [isPinchGestureInUse]); + return pinchGesture; }; From 141387187c0c429577f3787d07705763d693ce41 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Fri, 29 Dec 2023 14:26:00 +0100 Subject: [PATCH 095/580] improve styles in Lightbox --- src/components/Lightbox.js | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/src/components/Lightbox.js b/src/components/Lightbox.js index 06f8ee4cfeb6..0b09ed1e745a 100644 --- a/src/components/Lightbox.js +++ b/src/components/Lightbox.js @@ -168,7 +168,7 @@ function Lightbox({isAuthTokenRequired, source, onScaleChanged, onPress, onError {isContainerLoaded && ( <> {isLightboxVisible && ( - + setImageLoaded(true)} onLoad={(e) => { - const width = (e.nativeEvent?.width || 0) / PixelRatio.get(); - const height = (e.nativeEvent?.height || 0) / PixelRatio.get(); + const width = (e.nativeEvent?.width || 0) * PixelRatio.get(); + const height = (e.nativeEvent?.height || 0) * PixelRatio.get(); setImageDimensions({...imageDimensions, lightboxSize: {width, height}}); }} /> @@ -194,10 +194,7 @@ function Lightbox({isAuthTokenRequired, source, onScaleChanged, onPress, onError {/* Keep rendering the image without gestures as fallback if the carousel item is not active and while the lightbox is loading the image */} {isFallbackVisible && ( - + setFallbackLoaded(true)} onLoad={(e) => { - const width = e.nativeEvent?.width || 0; - const height = e.nativeEvent?.height || 0; + const width = (e.nativeEvent?.width || 0) * PixelRatio.get(); + const height = (e.nativeEvent?.height || 0) * PixelRatio.get(); if (imageDimensions?.lightboxSize != null) { return; From dae44fca62a988ebafa34489349c19dc6bdba624 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Fri, 29 Dec 2023 18:27:25 +0100 Subject: [PATCH 096/580] rename "isScrolling" prop --- .../AttachmentCarousel/Pager/index.js | 10 ++++---- src/components/MultiGestureCanvas/index.js | 14 +++++------ .../MultiGestureCanvas/usePanGesture.js | 21 +++++++++-------- .../MultiGestureCanvas/usePinchGesture.js | 23 ++++++++++++------- 4 files changed, 39 insertions(+), 29 deletions(-) diff --git a/src/components/Attachments/AttachmentCarousel/Pager/index.js b/src/components/Attachments/AttachmentCarousel/Pager/index.js index 553e963a3461..699e2fc812cc 100644 --- a/src/components/Attachments/AttachmentCarousel/Pager/index.js +++ b/src/components/Attachments/AttachmentCarousel/Pager/index.js @@ -69,7 +69,7 @@ function AttachmentCarouselPager({items, renderItem, initialIndex, onPageSelecte const shouldPagerScroll = useSharedValue(true); const pagerRef = useRef(null); - const isScrolling = useSharedValue(false); + const isSwipingHorizontally = useSharedValue(false); const activeIndex = useSharedValue(initialIndex); const pageScrollHandler = usePageScrollHandler( @@ -78,7 +78,7 @@ function AttachmentCarouselPager({items, renderItem, initialIndex, onPageSelecte 'worklet'; activeIndex.value = e.position; - isScrolling.value = e.offset !== 0; + isSwipingHorizontally.value = e.offset !== 0; }, }, [], @@ -94,7 +94,7 @@ function AttachmentCarouselPager({items, renderItem, initialIndex, onPageSelecte // we use reanimated for this since onPageSelected is called // in the middle of the pager animation useAnimatedReaction( - () => isScrolling.value, + () => isSwipingHorizontally.value, (stillScrolling) => { if (stillScrolling) { return; @@ -118,7 +118,7 @@ function AttachmentCarouselPager({items, renderItem, initialIndex, onPageSelecte const contextValue = useMemo( () => ({ - isScrolling, + isSwipingHorizontally, pagerRef, shouldPagerScroll, onPinchGestureChange, @@ -127,7 +127,7 @@ function AttachmentCarouselPager({items, renderItem, initialIndex, onPageSelecte onSwipeSuccess, onSwipeDown, }), - [isScrolling, pagerRef, shouldPagerScroll, onPinchGestureChange, onTap, onSwipe, onSwipeSuccess, onSwipeDown], + [isSwipingHorizontally, pagerRef, shouldPagerScroll, onPinchGestureChange, onTap, onSwipe, onSwipeSuccess, onSwipeDown], ); return ( diff --git a/src/components/MultiGestureCanvas/index.js b/src/components/MultiGestureCanvas/index.js index 70713bf3d80c..671426ba66e7 100644 --- a/src/components/MultiGestureCanvas/index.js +++ b/src/components/MultiGestureCanvas/index.js @@ -37,14 +37,14 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr const attachmentCarouselPagerContext = useContext(AttachmentCarouselPagerContext); const pagerRefFallback = useRef(null); - const {onTap, onSwipe, onSwipeSuccess, pagerRef, shouldPagerScroll, isScrolling, onPinchGestureChange} = attachmentCarouselPagerContext || { + const {onTap, onSwipe, onSwipeSuccess, pagerRef, shouldPagerScroll, isSwipingHorizontally, onPinchGestureChange} = attachmentCarouselPagerContext || { onTap: () => undefined, onSwipe: () => undefined, onSwipeSuccess: () => undefined, onPinchGestureChange: () => undefined, pagerRef: pagerRefFallback, shouldPagerScroll: false, - isScrolling: false, + isSwipingHorizontally: false, ...props, }; @@ -63,7 +63,7 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr // pan gesture const panTranslateX = useSharedValue(0); const panTranslateY = useSharedValue(0); - const isSwiping = useSharedValue(false); + const isSwipingVertically = useSharedValue(false); const panGestureRef = useRef(Gesture.Pan()); // pinch gesture @@ -129,8 +129,8 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr offsetY, panTranslateX, panTranslateY, - isSwiping, - isScrolling, + isSwipingHorizontally, + isSwipingVertically, onSwipeSuccess, stopAnimation, }); @@ -149,7 +149,7 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr pinchBounceTranslateX, pinchBounceTranslateY, pinchScaleOffset, - isScrolling, + isSwipingHorizontally, stopAnimation, onScaleChanged, onPinchGestureChange, @@ -179,7 +179,7 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr const x = pinchTranslateX.value + pinchBounceTranslateX.value + panTranslateX.value + offsetX.value; const y = pinchTranslateY.value + pinchBounceTranslateY.value + panTranslateY.value + offsetY.value; - if (isSwiping.value) { + if (isSwipingVertically.value) { onSwipe(y); } diff --git a/src/components/MultiGestureCanvas/usePanGesture.js b/src/components/MultiGestureCanvas/usePanGesture.js index ddc887bcb3f4..c81af12ebb62 100644 --- a/src/components/MultiGestureCanvas/usePanGesture.js +++ b/src/components/MultiGestureCanvas/usePanGesture.js @@ -22,8 +22,8 @@ const usePanGesture = ({ offsetY, panTranslateX, panTranslateY, - isSwiping, - isScrolling, + isSwipingVertically, + isSwipingHorizontally, onSwipeSuccess, stopAnimation, }) => { @@ -121,7 +121,7 @@ const usePanGesture = ({ } } else { offsetY.value = withSpring(target.y, SPRING_CONFIG, () => { - isSwiping.value = false; + isSwipingVertically.value = false; }); } }); @@ -136,7 +136,7 @@ const usePanGesture = ({ // TODO: Swipe down to close carousel gesture // this needs fine tuning to work properly - // if (!isScrolling.value && scale.value === 1 && previousTouch.value != null) { + // if (!isSwipingHorizontally.value && scale.value === 1 && previousTouch.value != null) { // const velocityX = Math.abs(evt.allTouches[0].x - previousTouch.value.x); // const velocityY = evt.allTouches[0].y - previousTouch.value.y; @@ -167,7 +167,7 @@ const usePanGesture = ({ // since we're running both pinch and pan gesture handlers simultaneously // we need to make sure that we don't pan when we pinch and move fingers // since we track it as pinch focal gesture - if (evt.numberOfPointers > 1 || isScrolling.value) { + if (evt.numberOfPointers > 1 || isSwipingHorizontally.value) { return; } @@ -175,27 +175,30 @@ const usePanGesture = ({ panVelocityY.value = evt.velocityY; - if (!isSwiping.value) { + if (!isSwipingVertically.value) { panTranslateX.value += evt.changeX; } - if (canPanVertically.value || isSwiping.value) { + if (canPanVertically.value || isSwipingVertically.value) { panTranslateY.value += evt.changeY; } }) .onEnd((evt) => { previousTouch.value = null; - if (isScrolling.value) { + // If we are swiping, we don't want to return to boundaries + if (isSwipingHorizontally.value) { return; } + // add pan translation to total offset offsetX.value += panTranslateX.value; offsetY.value += panTranslateY.value; + // reset pan gesture variables panTranslateX.value = 0; panTranslateY.value = 0; - if (isSwiping.value) { + if (isSwipingVertically.value) { const enoughVelocity = Math.abs(evt.velocityY) > 300 && Math.abs(evt.velocityX) < Math.abs(evt.velocityY); const rightDirection = (evt.translationY > 0 && evt.velocityY > 0) || (evt.translationY < 0 && evt.velocityY < 0); diff --git a/src/components/MultiGestureCanvas/usePinchGesture.js b/src/components/MultiGestureCanvas/usePinchGesture.js index 544baa1d045e..4dbbdf53f3f0 100644 --- a/src/components/MultiGestureCanvas/usePinchGesture.js +++ b/src/components/MultiGestureCanvas/usePinchGesture.js @@ -21,7 +21,7 @@ const usePinchGesture = ({ pinchBounceTranslateX, pinchBounceTranslateY, pinchScaleOffset, - isScrolling, + isSwipingHorizontally, stopAnimation, onScaleChanged, onPinchGestureChange, @@ -45,7 +45,7 @@ const usePinchGesture = ({ const pinchGesture = Gesture.Pinch() .onTouchesDown((evt, state) => { // we don't want to activate pinch gesture when we are scrolling pager - if (!isScrolling.value) { + if (!isSwipingHorizontally.value) { return; } @@ -57,19 +57,21 @@ const usePinchGesture = ({ stopAnimation(); - const adjustFocal = getAdjustedFocal(evt.focalX, evt.focalY); + const adjustedFocal = getAdjustedFocal(evt.focalX, evt.focalY); - pinchOrigin.x.value = adjustFocal.x; - pinchOrigin.y.value = adjustFocal.y; + pinchOrigin.x.value = adjustedFocal.x; + pinchOrigin.y.value = adjustedFocal.y; }) .onChange((evt) => { const newZoomScale = pinchScaleOffset.value * evt.scale; + // limit zoom scale to zoom range and bounce if we go out of range if (zoomScale.value >= zoomRange.min * zoomScaleBounceFactors.min && zoomScale.value <= zoomRange.max * zoomScaleBounceFactors.max) { zoomScale.value = newZoomScale; pinchGestureScale.value = evt.scale; } + // calculate new pinch translation const adjustedFocal = getAdjustedFocal(evt.focalX, evt.focalY); const newPinchTranslateX = adjustedFocal.x + pinchGestureScale.value * pinchOrigin.x.value * -1; const newPinchTranslateY = adjustedFocal.y + pinchGestureScale.value * pinchOrigin.y.value * -1; @@ -78,24 +80,29 @@ const usePinchGesture = ({ pinchTranslateX.value = newPinchTranslateX; pinchTranslateY.value = newPinchTranslateY; } else { + // Store x and y translation that is produced while bouncing to separate variables + // so that we can revert the bounce once pinch gesture is released pinchBounceTranslateX.value = newPinchTranslateX - pinchTranslateX.value; pinchBounceTranslateY.value = newPinchTranslateY - pinchTranslateY.value; } }) .onEnd(() => { + // Add pinch translation to total offset offsetX.value += pinchTranslateX.value; offsetY.value += pinchTranslateY.value; + // Reset pinch gesture variables pinchTranslateX.value = 0; pinchTranslateY.value = 0; - pinchScaleOffset.value = zoomScale.value; pinchGestureScale.value = 1; - if (pinchScaleOffset.value < zoomRange.min) { + if (zoomScale.value < zoomRange.min) { pinchScaleOffset.value = zoomRange.min; zoomScale.value = withSpring(zoomRange.min, SPRING_CONFIG); - } else if (pinchScaleOffset.value > zoomRange.max) { + } else if (zoomScale.value > zoomRange.max) { pinchScaleOffset.value = zoomRange.max; zoomScale.value = withSpring(zoomRange.max, SPRING_CONFIG); + } else { + pinchScaleOffset.value = zoomScale.value; } if (pinchBounceTranslateX.value !== 0 || pinchBounceTranslateY.value !== 0) { From 7bcf3f5aa31a22d75eca5d659e8aaff21f938f6e Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Fri, 29 Dec 2023 18:43:26 +0100 Subject: [PATCH 097/580] add more docs --- .../MultiGestureCanvas/usePanGesture.js | 20 +++++++++---------- .../MultiGestureCanvas/usePinchGesture.js | 12 +++++------ 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/components/MultiGestureCanvas/usePanGesture.js b/src/components/MultiGestureCanvas/usePanGesture.js index c81af12ebb62..e8203f3bb75e 100644 --- a/src/components/MultiGestureCanvas/usePanGesture.js +++ b/src/components/MultiGestureCanvas/usePanGesture.js @@ -30,14 +30,13 @@ const usePanGesture = ({ const zoomScaledContentWidth = useDerivedValue(() => contentSize.width * totalScale.value, [contentSize.width]); const zoomScaledContentHeight = useDerivedValue(() => contentSize.height * totalScale.value, [contentSize.height]); + const previousTouch = useSharedValue(null); // pan velocity to calculate the decay const panVelocityX = useSharedValue(0); const panVelocityY = useSharedValue(0); // disable pan vertically when content is smaller than screen const canPanVertically = useDerivedValue(() => canvasSize.height < zoomScaledContentHeight.value, [canvasSize.height]); - const previousTouch = useSharedValue(null); - // calculates bounds of the scaled content // can we pan left/right/up/down // can be used to limit gesture or implementing tension effect @@ -167,6 +166,7 @@ const usePanGesture = ({ // since we're running both pinch and pan gesture handlers simultaneously // we need to make sure that we don't pan when we pinch and move fingers // since we track it as pinch focal gesture + // we also need to prevent panning when we are swiping horizontally (in the pager) if (evt.numberOfPointers > 1 || isSwipingHorizontally.value) { return; } @@ -184,20 +184,16 @@ const usePanGesture = ({ } }) .onEnd((evt) => { - previousTouch.value = null; + // add pan translation to total offset + offsetX.value += panTranslateX.value; + offsetY.value += panTranslateY.value; // If we are swiping, we don't want to return to boundaries if (isSwipingHorizontally.value) { return; } - // add pan translation to total offset - offsetX.value += panTranslateX.value; - offsetY.value += panTranslateY.value; - // reset pan gesture variables - panTranslateX.value = 0; - panTranslateY.value = 0; - + // swipe to close animation when swiping down if (isSwipingVertically.value) { const enoughVelocity = Math.abs(evt.velocityY) > 300 && Math.abs(evt.velocityX) < Math.abs(evt.velocityY); const rightDirection = (evt.translationY > 0 && evt.velocityY > 0) || (evt.translationY < 0 && evt.velocityY < 0); @@ -229,8 +225,12 @@ const usePanGesture = ({ returnToBoundaries(); + // reset pan gesture variables panVelocityX.value = 0; panVelocityY.value = 0; + panTranslateX.value = 0; + panTranslateY.value = 0; + previousTouch.value = null; }) .withRef(panGestureRef); diff --git a/src/components/MultiGestureCanvas/usePinchGesture.js b/src/components/MultiGestureCanvas/usePinchGesture.js index 4dbbdf53f3f0..cbfa525daae9 100644 --- a/src/components/MultiGestureCanvas/usePinchGesture.js +++ b/src/components/MultiGestureCanvas/usePinchGesture.js @@ -90,10 +90,6 @@ const usePinchGesture = ({ // Add pinch translation to total offset offsetX.value += pinchTranslateX.value; offsetY.value += pinchTranslateY.value; - // Reset pinch gesture variables - pinchTranslateX.value = 0; - pinchTranslateY.value = 0; - pinchGestureScale.value = 1; if (zoomScale.value < zoomRange.min) { pinchScaleOffset.value = zoomRange.min; @@ -110,11 +106,15 @@ const usePinchGesture = ({ pinchBounceTranslateY.value = withSpring(0, SPRING_CONFIG); } - pinchGestureRunning.value = false; - if (onScaleChanged != null) { runOnJS(onScaleChanged)(zoomScale.value); } + + // Reset pinch gesture variables + pinchGestureRunning.value = false; + pinchTranslateX.value = 0; + pinchTranslateY.value = 0; + pinchGestureScale.value = 1; }); // Triggers "onPinchGestureChange" callback when pinch scale changes From a08e2bb866b2f5dce29cd16f78d5160045cc099d Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Fri, 29 Dec 2023 20:14:12 +0100 Subject: [PATCH 098/580] further improve component --- src/components/MultiGestureCanvas/index.js | 36 +++++++------- .../MultiGestureCanvas/usePanGesture.js | 47 ++++++++++--------- .../MultiGestureCanvas/usePinchGesture.js | 31 ++++++------ .../MultiGestureCanvas/useTapGestures.js | 11 +++-- src/components/MultiGestureCanvas/utils.ts | 4 +- 5 files changed, 65 insertions(+), 64 deletions(-) diff --git a/src/components/MultiGestureCanvas/index.js b/src/components/MultiGestureCanvas/index.js index 671426ba66e7..adbc46112621 100644 --- a/src/components/MultiGestureCanvas/index.js +++ b/src/components/MultiGestureCanvas/index.js @@ -56,9 +56,9 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr // and not smaller than needed to fit const totalScale = useDerivedValue(() => zoomScale.value * minContentScale, [minContentScale]); - // stored offset of the canvas (used for panning and pinching) - const offsetX = useSharedValue(0); - const offsetY = useSharedValue(0); + // total offset of the canvas (panning + pinching offset) + const totalOffsetX = useSharedValue(0); + const totalOffsetY = useSharedValue(0); // pan gesture const panTranslateX = useSharedValue(0); @@ -75,8 +75,8 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr const pinchScaleOffset = useSharedValue(1); const stopAnimation = useWorkletCallback(() => { - cancelAnimation(offsetX); - cancelAnimation(offsetY); + cancelAnimation(totalOffsetX); + cancelAnimation(totalOffsetY); }); const reset = useWorkletCallback((animated) => { @@ -85,12 +85,12 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr stopAnimation(); if (animated) { - offsetX.value = withSpring(0, SPRING_CONFIG); - offsetY.value = withSpring(0, SPRING_CONFIG); + totalOffsetX.value = withSpring(0, SPRING_CONFIG); + totalOffsetY.value = withSpring(0, SPRING_CONFIG); zoomScale.value = withSpring(1, SPRING_CONFIG); } else { - offsetX.value = 0; - offsetY.value = 0; + totalOffsetX.value = 0; + totalOffsetY.value = 0; zoomScale.value = 1; panTranslateX.value = 0; panTranslateY.value = 0; @@ -105,8 +105,8 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr minContentScale, maxContentScale, panGestureRef, - offsetX, - offsetY, + totalOffsetX, + totalOffsetY, pinchScaleOffset, zoomScale, reset, @@ -125,8 +125,8 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr zoomScale, zoomRange, totalScale, - offsetX, - offsetY, + totalOffsetX, + totalOffsetY, panTranslateX, panTranslateY, isSwipingHorizontally, @@ -142,8 +142,8 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr panGesture, zoomScale, zoomRange, - offsetX, - offsetY, + totalOffsetX, + totalOffsetY, pinchTranslateX, pinchTranslateY, pinchBounceTranslateX, @@ -176,8 +176,10 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr }, [isActive, mounted, reset]); const animatedStyles = useAnimatedStyle(() => { - const x = pinchTranslateX.value + pinchBounceTranslateX.value + panTranslateX.value + offsetX.value; - const y = pinchTranslateY.value + pinchBounceTranslateY.value + panTranslateY.value + offsetY.value; + const x = pinchTranslateX.value + pinchBounceTranslateX.value + panTranslateX.value + totalOffsetX.value; + const y = pinchTranslateY.value + pinchBounceTranslateY.value + panTranslateY.value + totalOffsetY.value; + + // console.log({pinchTranslateY: pinchTranslateY.value, pinchBounceTranslateY: pinchBounceTranslateY.value, panTranslateY: panTranslateY.value, totalOffsetY: totalOffsetY.value}); if (isSwipingVertically.value) { onSwipe(y); diff --git a/src/components/MultiGestureCanvas/usePanGesture.js b/src/components/MultiGestureCanvas/usePanGesture.js index e8203f3bb75e..5d6279a8be56 100644 --- a/src/components/MultiGestureCanvas/usePanGesture.js +++ b/src/components/MultiGestureCanvas/usePanGesture.js @@ -18,8 +18,8 @@ const usePanGesture = ({ zoomScale, zoomRange, totalScale, - offsetX, - offsetY, + totalOffsetX, + totalOffsetY, panTranslateX, panTranslateY, isSwipingVertically, @@ -56,12 +56,12 @@ const usePanGesture = ({ const minVector = {x: -rightBoundary, y: -topBoundary}; const target = { - x: clamp(offsetX.value, minVector.x, maxVector.x), - y: clamp(offsetY.value, minVector.y, maxVector.y), + x: clamp(totalOffsetX.value, minVector.x, maxVector.x), + y: clamp(totalOffsetY.value, minVector.y, maxVector.y), }; - const isInBoundaryX = target.x === offsetX.value; - const isInBoundaryY = target.y === offsetY.value; + const isInBoundaryX = target.x === totalOffsetX.value; + const isInBoundaryY = target.y === totalOffsetY.value; return { target, @@ -77,21 +77,21 @@ const usePanGesture = ({ const returnToBoundaries = useWorkletCallback(() => { const {target, isInBoundaryX, isInBoundaryY, minVector, maxVector} = getBounds(); - if (zoomScale.value === zoomRange.min && offsetX.value === 0 && offsetY.value === 0 && panTranslateX.value === 0 && panTranslateY.value === 0) { + if (zoomScale.value === zoomRange.min && totalOffsetX.value === 0 && totalOffsetY.value === 0 && panTranslateX.value === 0 && panTranslateY.value === 0) { // we don't need to run any animations return; } if (zoomScale.value <= zoomRange.min) { // just center it - offsetX.value = withSpring(0, SPRING_CONFIG); - offsetY.value = withSpring(0, SPRING_CONFIG); + totalOffsetX.value = withSpring(0, SPRING_CONFIG); + totalOffsetY.value = withSpring(0, SPRING_CONFIG); return; } if (isInBoundaryX) { if (Math.abs(panVelocityX.value) > 0 && zoomScale.value <= zoomRange.max) { - offsetX.value = withDecay({ + totalOffsetX.value = withDecay({ velocity: panVelocityX.value, clamp: [minVector.x, maxVector.x], deceleration: PAN_DECAY_DECELARATION, @@ -99,27 +99,27 @@ const usePanGesture = ({ }); } } else { - offsetX.value = withSpring(target.x, SPRING_CONFIG); + totalOffsetX.value = withSpring(target.x, SPRING_CONFIG); } if (!canPanVertically.value) { - offsetY.value = withSpring(target.y, SPRING_CONFIG); + totalOffsetY.value = withSpring(target.y, SPRING_CONFIG); } else if (isInBoundaryY) { if ( Math.abs(panVelocityY.value) > 0 && zoomScale.value <= zoomRange.max && // limit vertical pan only when content is smaller than screen - offsetY.value !== minVector.y && - offsetY.value !== maxVector.y + totalOffsetY.value !== minVector.y && + totalOffsetY.value !== maxVector.y ) { - offsetY.value = withDecay({ + totalOffsetY.value = withDecay({ velocity: panVelocityY.value, clamp: [minVector.y, maxVector.y], deceleration: PAN_DECAY_DECELARATION, }); } } else { - offsetY.value = withSpring(target.y, SPRING_CONFIG, () => { + totalOffsetY.value = withSpring(target.y, SPRING_CONFIG, () => { isSwipingVertically.value = false; }); } @@ -159,7 +159,7 @@ const usePanGesture = ({ } }) .simultaneousWithExternalGesture(pagerRef, singleTap, doubleTap) - .onBegin(() => { + .onStart(() => { stopAnimation(); }) .onChange((evt) => { @@ -185,8 +185,12 @@ const usePanGesture = ({ }) .onEnd((evt) => { // add pan translation to total offset - offsetX.value += panTranslateX.value; - offsetY.value += panTranslateY.value; + totalOffsetX.value += panTranslateX.value; + totalOffsetY.value += panTranslateY.value; + // reset pan gesture variables + panTranslateX.value = 0; + panTranslateY.value = 0; + previousTouch.value = null; // If we are swiping, we don't want to return to boundaries if (isSwipingHorizontally.value) { @@ -204,7 +208,7 @@ const usePanGesture = ({ return invert ? -v : v; }; - offsetY.value = withSpring( + totalOffsetY.value = withSpring( maybeInvert(contentSize.height * 2), { stiffness: 50, @@ -228,9 +232,6 @@ const usePanGesture = ({ // reset pan gesture variables panVelocityX.value = 0; panVelocityY.value = 0; - panTranslateX.value = 0; - panTranslateY.value = 0; - previousTouch.value = null; }) .withRef(panGestureRef); diff --git a/src/components/MultiGestureCanvas/usePinchGesture.js b/src/components/MultiGestureCanvas/usePinchGesture.js index cbfa525daae9..78aed77814cd 100644 --- a/src/components/MultiGestureCanvas/usePinchGesture.js +++ b/src/components/MultiGestureCanvas/usePinchGesture.js @@ -14,8 +14,8 @@ const usePinchGesture = ({ panGesture, zoomScale, zoomRange, - offsetX, - offsetY, + totalOffsetX, + totalOffsetY, pinchTranslateX, pinchTranslateY, pinchBounceTranslateX, @@ -26,9 +26,9 @@ const usePinchGesture = ({ onScaleChanged, onPinchGestureChange, }) => { + const isPinchGestureRunning = useSharedValue(false); // used to store event scale value when we limit scale const pinchGestureScale = useSharedValue(1); - const pinchGestureRunning = useSharedValue(false); // origin of the pinch gesture const pinchOrigin = { x: useSharedValue(0), @@ -37,8 +37,8 @@ const usePinchGesture = ({ const getAdjustedFocal = useWorkletCallback( (focalX, focalY) => ({ - x: focalX - (canvasSize.width / 2 + offsetX.value), - y: focalY - (canvasSize.height / 2 + offsetY.value), + x: focalX - (canvasSize.width / 2 + totalOffsetX.value), + y: focalY - (canvasSize.height / 2 + totalOffsetY.value), }), [canvasSize.width, canvasSize.height], ); @@ -53,7 +53,7 @@ const usePinchGesture = ({ }) .simultaneousWithExternalGesture(panGesture, singleTap, doubleTap) .onStart((evt) => { - pinchGestureRunning.value = true; + isPinchGestureRunning.value = true; stopAnimation(); @@ -88,8 +88,13 @@ const usePinchGesture = ({ }) .onEnd(() => { // Add pinch translation to total offset - offsetX.value += pinchTranslateX.value; - offsetY.value += pinchTranslateY.value; + totalOffsetX.value += pinchTranslateX.value; + totalOffsetY.value += pinchTranslateY.value; + // Reset pinch gesture variables + pinchTranslateX.value = 0; + pinchTranslateY.value = 0; + pinchGestureScale.value = 1; + isPinchGestureRunning.value = false; if (zoomScale.value < zoomRange.min) { pinchScaleOffset.value = zoomRange.min; @@ -107,20 +112,14 @@ const usePinchGesture = ({ } if (onScaleChanged != null) { - runOnJS(onScaleChanged)(zoomScale.value); + runOnJS(onScaleChanged)(pinchScaleOffset.value); } - - // Reset pinch gesture variables - pinchGestureRunning.value = false; - pinchTranslateX.value = 0; - pinchTranslateY.value = 0; - pinchGestureScale.value = 1; }); // Triggers "onPinchGestureChange" callback when pinch scale changes const [isPinchGestureInUse, setIsPinchGestureInUse] = useState(false); useAnimatedReaction( - () => [zoomScale.value, pinchGestureRunning.value], + () => [zoomScale.value, isPinchGestureRunning.value], ([zoom, running]) => { const newIsPinchGestureInUse = zoom !== 1 || running; if (isPinchGestureInUse !== newIsPinchGestureInUse) { diff --git a/src/components/MultiGestureCanvas/useTapGestures.js b/src/components/MultiGestureCanvas/useTapGestures.js index 0af5076618a4..a08fbc8fed4c 100644 --- a/src/components/MultiGestureCanvas/useTapGestures.js +++ b/src/components/MultiGestureCanvas/useTapGestures.js @@ -4,8 +4,9 @@ import {Gesture} from 'react-native-gesture-handler'; import {runOnJS, useWorkletCallback, withSpring} from 'react-native-reanimated'; import * as MultiGestureCanvasUtils from './utils'; +const DOUBLE_TAP_SCALE = 3; + const clamp = MultiGestureCanvasUtils.clamp; -const DOUBLE_TAP_SCALE = MultiGestureCanvasUtils.DOUBLE_TAP_SCALE; const SPRING_CONFIG = MultiGestureCanvasUtils.SPRING_CONFIG; const useTapGestures = ({ @@ -14,8 +15,8 @@ const useTapGestures = ({ minContentScale, maxContentScale, panGestureRef, - offsetX, - offsetY, + totalOffsetX, + totalOffsetY, pinchScaleOffset, zoomScale, reset, @@ -82,8 +83,8 @@ const useTapGestures = ({ target.y = 0; } - offsetX.value = withSpring(target.x, SPRING_CONFIG); - offsetY.value = withSpring(target.y, SPRING_CONFIG); + totalOffsetX.value = withSpring(target.x, SPRING_CONFIG); + totalOffsetY.value = withSpring(target.y, SPRING_CONFIG); zoomScale.value = withSpring(doubleTapScale, SPRING_CONFIG); pinchScaleOffset.value = doubleTapScale; }, diff --git a/src/components/MultiGestureCanvas/utils.ts b/src/components/MultiGestureCanvas/utils.ts index c28f02ed0f39..da4c1133d237 100644 --- a/src/components/MultiGestureCanvas/utils.ts +++ b/src/components/MultiGestureCanvas/utils.ts @@ -1,5 +1,3 @@ -const DOUBLE_TAP_SCALE = 3; - const SPRING_CONFIG = { mass: 1, stiffness: 1000, @@ -16,4 +14,4 @@ function clamp(value: number, lowerBound: number, upperBound: number) { return Math.min(Math.max(lowerBound, value), upperBound); } -export {clamp, DOUBLE_TAP_SCALE, SPRING_CONFIG, zoomScaleBounceFactors}; +export {clamp, SPRING_CONFIG, zoomScaleBounceFactors}; From db02416c819bbc87b7c759c5b4b45414e6e277ee Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Tue, 2 Jan 2024 14:59:22 +0100 Subject: [PATCH 099/580] 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 e5cd46fbe7bcd26fcdc365d86482ad961b9ed4ba Mon Sep 17 00:00:00 2001 From: Pedro Guerreiro Date: Tue, 2 Jan 2024 14:15:20 +0000 Subject: [PATCH 100/580] refactor(typescript): migrate validateloginpage --- src/libs/Navigation/Navigation.ts | 26 +++---- src/pages/ValidateLoginPage/index.js | 61 ----------------- src/pages/ValidateLoginPage/index.tsx | 42 ++++++++++++ .../{index.website.js => index.website.tsx} | 68 ++++++------------- 4 files changed, 75 insertions(+), 122 deletions(-) delete mode 100644 src/pages/ValidateLoginPage/index.js create mode 100644 src/pages/ValidateLoginPage/index.tsx rename src/pages/ValidateLoginPage/{index.website.js => index.website.tsx} (55%) diff --git a/src/libs/Navigation/Navigation.ts b/src/libs/Navigation/Navigation.ts index 3552ff9e7410..2c250b6b89b2 100644 --- a/src/libs/Navigation/Navigation.ts +++ b/src/libs/Navigation/Navigation.ts @@ -1,18 +1,18 @@ -import {findFocusedRoute, getActionFromState} from '@react-navigation/core'; -import {CommonActions, EventArg, getPathFromState, NavigationContainerEventMap, NavigationState, PartialState, StackActions} from '@react-navigation/native'; +import { findFocusedRoute, getActionFromState } from '@react-navigation/core'; +import { CommonActions, EventArg, getPathFromState, NavigationContainerEventMap, NavigationState, PartialState, StackActions } from '@react-navigation/native'; import findLastIndex from 'lodash/findLastIndex'; import Log from '@libs/Log'; import CONST from '@src/CONST'; import NAVIGATORS from '@src/NAVIGATORS'; -import ROUTES, {Route} from '@src/ROUTES'; -import SCREENS, {PROTECTED_SCREENS} from '@src/SCREENS'; +import ROUTES, { Route } from '@src/ROUTES'; +import SCREENS, { PROTECTED_SCREENS } from '@src/SCREENS'; import getStateFromPath from './getStateFromPath'; import originalGetTopmostReportActionId from './getTopmostReportActionID'; import originalGetTopmostReportId from './getTopmostReportId'; import linkingConfig from './linkingConfig'; import linkTo from './linkTo'; import navigationRef from './navigationRef'; -import {StackNavigationAction, StateOrRoute} from './types'; +import { StackNavigationAction, StateOrRoute } from './types'; let resolveNavigationIsReadyPromise: () => void; const navigationIsReadyPromise = new Promise((resolve) => { @@ -82,7 +82,7 @@ function getDistanceFromPathInRootNavigator(path: string): number { return index; } - currentState = {...currentState, routes: currentState.routes.slice(0, -1), index: currentState.index - 1}; + currentState = { ...currentState, routes: currentState.routes.slice(0, -1), index: currentState.index - 1 }; } return -1; @@ -123,7 +123,7 @@ function isActiveRoute(routePath: Route): boolean { * @param [type] - Type of action to perform. Currently UP is supported. */ function navigate(route: Route = ROUTES.HOME, type?: string) { - if (!canNavigate('navigate', {route})) { + if (!canNavigate('navigate', { route })) { // Store intended route if the navigator is not yet available, // we will try again after the NavigationContainer is ready Log.hmmm(`[Navigation] Container not yet ready, storing route as pending: ${route}`); @@ -138,7 +138,7 @@ function navigate(route: Route = ROUTES.HOME, type?: string) { * @param shouldEnforceFallback - Enforces navigation to fallback route * @param shouldPopToTop - Should we navigate to LHN on back press */ -function goBack(fallbackRoute: Route, shouldEnforceFallback = false, shouldPopToTop = false) { +function goBack(fallbackRoute?: Route, shouldEnforceFallback = false, shouldPopToTop = false) { if (!canNavigate('goBack')) { return; } @@ -173,7 +173,7 @@ function goBack(fallbackRoute: Route, shouldEnforceFallback = false, shouldPopTo } const isCentralPaneFocused = findFocusedRoute(navigationRef.current.getState())?.name === NAVIGATORS.CENTRAL_PANE_NAVIGATOR; - const distanceFromPathInRootNavigator = getDistanceFromPathInRootNavigator(fallbackRoute); + const distanceFromPathInRootNavigator = getDistanceFromPathInRootNavigator(fallbackRoute ?? ''); // Allow CentralPane to use UP with fallback route if the path is not found in root navigator. if (isCentralPaneFocused && fallbackRoute && distanceFromPathInRootNavigator === -1) { @@ -228,9 +228,9 @@ function dismissModal(targetReportID?: string) { } else if (targetReportID && rootState.routes.some((route) => route.name === SCREENS.NOT_FOUND)) { const lastRouteIndex = rootState.routes.length - 1; const centralRouteIndex = findLastIndex(rootState.routes, (route) => route.name === NAVIGATORS.CENTRAL_PANE_NAVIGATOR); - navigationRef.current?.dispatch({...StackActions.pop(lastRouteIndex - centralRouteIndex), target: rootState.key}); + navigationRef.current?.dispatch({ ...StackActions.pop(lastRouteIndex - centralRouteIndex), target: rootState.key }); } else { - navigationRef.current?.dispatch({...StackActions.pop(), target: rootState.key}); + navigationRef.current?.dispatch({ ...StackActions.pop(), target: rootState.key }); } break; default: { @@ -315,7 +315,7 @@ function waitForProtectedRoutes() { return; } - const unsubscribe = navigationRef.current?.addListener('state', ({data}) => { + const unsubscribe = navigationRef.current?.addListener('state', ({ data }) => { const state = data?.state; if (navContainsProtectedRoutes(state)) { unsubscribe?.(); @@ -343,4 +343,4 @@ export default { waitForProtectedRoutes, }; -export {navigationRef}; +export { navigationRef }; diff --git a/src/pages/ValidateLoginPage/index.js b/src/pages/ValidateLoginPage/index.js deleted file mode 100644 index 6939ee07f665..000000000000 --- a/src/pages/ValidateLoginPage/index.js +++ /dev/null @@ -1,61 +0,0 @@ -import lodashGet from 'lodash/get'; -import PropTypes from 'prop-types'; -import React, {useEffect} from 'react'; -import {withOnyx} from 'react-native-onyx'; -import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; -import Navigation from '@libs/Navigation/Navigation'; -import * as Session from '@userActions/Session'; -import ONYXKEYS from '@src/ONYXKEYS'; -import {defaultProps as validateLinkDefaultProps, propTypes as validateLinkPropTypes} from './validateLinkPropTypes'; - -const propTypes = { - /** The accountID and validateCode are passed via the URL */ - route: validateLinkPropTypes, - - /** Session of currently logged in user */ - session: PropTypes.shape({ - /** Currently logged in user authToken */ - authToken: PropTypes.string, - }), - - /** The credentials of the person logging in */ - credentials: PropTypes.shape({ - /** The email the user logged in with */ - login: PropTypes.string, - }), -}; - -const defaultProps = { - route: validateLinkDefaultProps, - session: { - authToken: null, - }, - credentials: {}, -}; - -function ValidateLoginPage(props) { - useEffect(() => { - const accountID = lodashGet(props.route.params, 'accountID', ''); - const validateCode = lodashGet(props.route.params, 'validateCode', ''); - - if (lodashGet(props, 'session.authToken')) { - // If already signed in, do not show the validate code if not on web, - // because we don't want to block the user with the interstitial page. - Navigation.goBack(false); - } else { - Session.signInWithValidateCodeAndNavigate(accountID, validateCode); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - return ; -} - -ValidateLoginPage.defaultProps = defaultProps; -ValidateLoginPage.displayName = 'ValidateLoginPage'; -ValidateLoginPage.propTypes = propTypes; - -export default withOnyx({ - credentials: {key: ONYXKEYS.CREDENTIALS}, - session: {key: ONYXKEYS.SESSION}, -})(ValidateLoginPage); diff --git a/src/pages/ValidateLoginPage/index.tsx b/src/pages/ValidateLoginPage/index.tsx new file mode 100644 index 000000000000..597a025cf602 --- /dev/null +++ b/src/pages/ValidateLoginPage/index.tsx @@ -0,0 +1,42 @@ +import type {StackScreenProps} from '@react-navigation/stack'; +import React, {useEffect} from 'react'; +import {OnyxEntry, withOnyx} from 'react-native-onyx'; +import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; +import Navigation from '@libs/Navigation/Navigation'; +import type {AuthScreensParamList} from '@libs/Navigation/types'; +import * as Session from '@userActions/Session'; +import ONYXKEYS from '@src/ONYXKEYS'; +import SCREENS from '@src/SCREENS'; +import type {Session as SessionType} from '@src/types/onyx'; + +type ValidateLoginPageOnyxProps = { + session: OnyxEntry; +}; + +type ValidateLoginPageProps = ValidateLoginPageOnyxProps & StackScreenProps; + +function ValidateLoginPage({ + route: { + params: {accountID = '', validateCode = ''}, + }, + session, +}: ValidateLoginPageProps) { + useEffect(() => { + if (session?.authToken) { + // If already signed in, do not show the validate code if not on web, + // because we don't want to block the user with the interstitial page. + Navigation.goBack(); + } else { + Session.signInWithValidateCodeAndNavigate(Number(accountID), validateCode); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return ; +} + +ValidateLoginPage.displayName = 'ValidateLoginPage'; + +export default withOnyx({ + session: {key: ONYXKEYS.SESSION}, +})(ValidateLoginPage); diff --git a/src/pages/ValidateLoginPage/index.website.js b/src/pages/ValidateLoginPage/index.website.tsx similarity index 55% rename from src/pages/ValidateLoginPage/index.website.js rename to src/pages/ValidateLoginPage/index.website.tsx index 677abd70f6db..1a68405934bc 100644 --- a/src/pages/ValidateLoginPage/index.website.js +++ b/src/pages/ValidateLoginPage/index.website.tsx @@ -1,60 +1,34 @@ -import lodashGet from 'lodash/get'; -import PropTypes from 'prop-types'; +import type {StackScreenProps} from '@react-navigation/stack'; import React, {useEffect} from 'react'; -import {withOnyx} from 'react-native-onyx'; +import {OnyxEntry, withOnyx} from 'react-native-onyx'; import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import ExpiredValidateCodeModal from '@components/ValidateCode/ExpiredValidateCodeModal'; import JustSignedInModal from '@components/ValidateCode/JustSignedInModal'; import ValidateCodeModal from '@components/ValidateCode/ValidateCodeModal'; import Navigation from '@libs/Navigation/Navigation'; +import type {AuthScreensParamList} from '@libs/Navigation/types'; import * as Session from '@userActions/Session'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import {defaultProps as validateLinkDefaultProps, propTypes as validateLinkPropTypes} from './validateLinkPropTypes'; +import SCREENS from '@src/SCREENS'; +import type {Account, Credentials, Session as SessionType} from '@src/types/onyx'; -const propTypes = { - /** The accountID and validateCode are passed via the URL */ - route: validateLinkPropTypes, - - /** Session of currently logged in user */ - session: PropTypes.shape({ - /** Currently logged in user authToken */ - authToken: PropTypes.string, - }), - - /** The credentials of the person logging in */ - credentials: PropTypes.shape({ - /** The email the user logged in with */ - login: PropTypes.string, - - /** The validate code */ - validateCode: PropTypes.string, - }), - - /** The details about the account that the user is signing in with */ - account: PropTypes.shape({ - /** Whether a sign on form is loading (being submitted) */ - isLoading: PropTypes.bool, - }), +type ValidateLoginPageOnyxProps = { + account: OnyxEntry; + credentials: OnyxEntry; + session: OnyxEntry; }; -const defaultProps = { - route: validateLinkDefaultProps, - session: { - authToken: null, - }, - credentials: {}, - account: {}, -}; +type ValidateLoginPageProps = ValidateLoginPageOnyxProps & StackScreenProps; -function ValidateLoginPage(props) { - const login = lodashGet(props, 'credentials.login', null); - const autoAuthState = lodashGet(props, 'session.autoAuthState', CONST.AUTO_AUTH_STATE.NOT_STARTED); - const accountID = lodashGet(props.route.params, 'accountID', ''); - const validateCode = lodashGet(props.route.params, 'validateCode', ''); - const isSignedIn = Boolean(lodashGet(props, 'session.authToken', null)); - const is2FARequired = lodashGet(props, 'account.requiresTwoFactorAuth', false); - const cachedAccountID = lodashGet(props, 'credentials.accountID', null); +function ValidateLoginPage({account, credentials, route, session}: ValidateLoginPageProps) { + const login = credentials?.login; + const autoAuthState = session?.autoAuthState ?? CONST.AUTO_AUTH_STATE.NOT_STARTED; + const accountID = Number(route?.params.accountID ?? ''); + const validateCode = route.params.validateCode ?? ''; + const isSignedIn = !!session?.authToken; + const is2FARequired = !!account?.requiresTwoFactorAuth; + const cachedAccountID = credentials?.accountID; useEffect(() => { if (!login && isSignedIn && (autoAuthState === CONST.AUTO_AUTH_STATE.SIGNING_IN || autoAuthState === CONST.AUTO_AUTH_STATE.JUST_SIGNED_IN)) { @@ -74,7 +48,7 @@ function ValidateLoginPage(props) { }, []); useEffect(() => { - if (login || !cachedAccountID || !is2FARequired) { + if (!!login || !cachedAccountID || !is2FARequired) { return; } @@ -98,11 +72,9 @@ function ValidateLoginPage(props) { ); } -ValidateLoginPage.defaultProps = defaultProps; ValidateLoginPage.displayName = 'ValidateLoginPage'; -ValidateLoginPage.propTypes = propTypes; -export default withOnyx({ +export default withOnyx({ account: {key: ONYXKEYS.ACCOUNT}, credentials: {key: ONYXKEYS.CREDENTIALS}, session: {key: ONYXKEYS.SESSION}, From 2fb8f0e30c7a06f540decfbeb055217cd76c1467 Mon Sep 17 00:00:00 2001 From: Kamil Owczarz Date: Tue, 2 Jan 2024 17:04:17 +0100 Subject: [PATCH 101/580] 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 102/580] 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 e9cb53c9171ac913e641928995ade1ff9ee5e061 Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Wed, 3 Jan 2024 08:50:02 +0100 Subject: [PATCH 103/580] start migrating MagicCodeInput to TypeScript --- src/CONST.ts | 5 + src/components/MagicCodeInput-draft.tsx | 418 ++++++++++++++++++++++++ 2 files changed, 423 insertions(+) create mode 100644 src/components/MagicCodeInput-draft.tsx diff --git a/src/CONST.ts b/src/CONST.ts index abba27b0c33b..30757a269166 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -903,6 +903,7 @@ const CONST = { KEYBOARD_TYPE: { VISIBLE_PASSWORD: 'visible-password', ASCII_CAPABLE: 'ascii-capable', + NUMBER_PAD: 'number-pad', }, INPUT_MODE: { @@ -2831,12 +2832,16 @@ const CONST = { CHECKBOX: 'checkbox', /** Use for elements that allow a choice from multiple options. */ COMBOBOX: 'combobox', + /** Use for form elements. */ + FORM: 'form', /** Use with scrollable lists to represent a grid layout. */ GRID: 'grid', /** Use for section headers or titles. */ HEADING: 'heading', /** Use for image elements. */ IMG: 'img', + /** Use for input elements. */ + INPUT: 'input', /** Use for elements that navigate to other pages or content. */ LINK: 'link', /** Use to identify a list of items. */ diff --git a/src/components/MagicCodeInput-draft.tsx b/src/components/MagicCodeInput-draft.tsx new file mode 100644 index 000000000000..6c1cb1851e18 --- /dev/null +++ b/src/components/MagicCodeInput-draft.tsx @@ -0,0 +1,418 @@ +import React, {ForwardedRef, forwardRef, useEffect, useImperativeHandle, useRef, useState} from 'react'; +import {StyleSheet, View, TextInput as RNTextInput, NativeSyntheticEvent, TextInputFocusEventData} from 'react-native'; +import {HandlerStateChangeEvent, TapGestureHandler} from 'react-native-gesture-handler'; +import useNetwork from '@hooks/useNetwork'; +import useStyleUtils from '@hooks/useStyleUtils'; +import useThemeStyles from '@hooks/useThemeStyles'; +import * as Browser from '@libs/Browser'; +import * as ValidationUtils from '@libs/ValidationUtils'; +import CONST from '@src/CONST'; +import FormHelpMessage from './FormHelpMessage'; +import Text from './Text'; +import TextInput from './TextInput'; + +const TEXT_INPUT_EMPTY_STATE = ''; + +type MagicCodeInputProps = { + /** Name attribute for the input */ + name?: string, + + /** Input value */ + value?: string, + + /** Should the input auto focus */ + autoFocus?: boolean, + + /** Whether we should wait before focusing the TextInput, useful when using transitions */ + shouldDelayFocus?: boolean, + + /** Error text to display */ + errorText?: string, + + /** Specifies autocomplete hints for the system, so it can provide autofill */ + autoComplete: 'sms-otp' | 'one-time-code' | 'off', + + /* Should submit when the input is complete */ + shouldSubmitOnComplete?: boolean, + + /** Function to call when the input is changed */ + onChangeText?: (value: string) => void, + + /** Function to call when the input is submitted or fully complete */ + onFulfill?: (value: string) => void, + + /** Specifies if the input has a validation error */ + hasError?: boolean, + + /** Specifies the max length of the input */ + maxLength?: number, + + /** Specifies if the keyboard should be disabled */ + isDisableKeyboard?: boolean, + + /** Last pressed digit on BigDigitPad */ + lastPressedDigit?: string, +} + +/** + * Converts a given string into an array of numbers that must have the same + * number of elements as the number of inputs. + */ +const decomposeString = (value: string, length: number): string[] => { + let arr = value.split('').slice(0, length).map((v) => (ValidationUtils.isNumeric(v) ? v : CONST.MAGIC_CODE_EMPTY_CHAR)) + if (arr.length < length) { + arr = arr.concat(Array(length - arr.length).fill(CONST.MAGIC_CODE_EMPTY_CHAR)); + } + return arr; +}; + +/** + * Converts an array of strings into a single string. If there are undefined or + * empty values, it will replace them with a space. + */ +const composeToString = (value: string[]): string => value.map((v) => (v === undefined || v === '' ? CONST.MAGIC_CODE_EMPTY_CHAR : v)).join(''); + +const getInputPlaceholderSlots = (length: number): number[] => Array.from(Array(length).keys()); + +function MagicCodeInput({ + value = '', + name = '', + autoFocus = true, + shouldDelayFocus = false, + errorText = '', + shouldSubmitOnComplete = true, + onChangeText: onChangeTextProp = () => {}, + onFulfill = () => {}, + hasError = false, + maxLength = CONST.MAGIC_CODE_LENGTH, + isDisableKeyboard = false, + lastPressedDigit = '', + autoComplete, +}: MagicCodeInputProps, ref: ForwardedRef) { + const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); + const inputRefs = useRef(); + const [input, setInput] = useState(TEXT_INPUT_EMPTY_STATE); + const [focusedIndex, setFocusedIndex] = useState(0); + const [editIndex, setEditIndex] = useState(0); + const [wasSubmitted, setWasSubmitted] = useState(false); + const shouldFocusLast = useRef(false); + const inputWidth = useRef(0); + const lastFocusedIndex = useRef(0); + const lastValue = useRef(TEXT_INPUT_EMPTY_STATE); + + console.log("** I RENDER **") + + useEffect(() => { + lastValue.current = input.length; + }, [input]); + + const blurMagicCodeInput = () => { + inputRefs.current?.blur(); + setFocusedIndex(undefined); + }; + + const focusMagicCodeInput = () => { + setFocusedIndex(0); + lastFocusedIndex.current = 0; + setEditIndex(0); + inputRefs.current?.focus(); + }; + + const setInputAndIndex = (index: number | undefined) => { + setInput(TEXT_INPUT_EMPTY_STATE); + setFocusedIndex(index); + setEditIndex(index); + }; + + useImperativeHandle(ref, () => ({ + focus() { + focusMagicCodeInput(); + }, + focusLastSelected() { + inputRefs.current?.focus(); + }, + resetFocus() { + setInput(TEXT_INPUT_EMPTY_STATE); + focusMagicCodeInput(); + }, + clear() { + lastFocusedIndex.current = 0; + setInputAndIndex(0); + inputRefs.current?.focus(); + onChangeTextProp(''); + }, + blur() { + blurMagicCodeInput(); + }, + })); + + const validateAndSubmit = () => { + const numbers = decomposeString(value, maxLength); + if (wasSubmitted || !shouldSubmitOnComplete || numbers.filter((n) => ValidationUtils.isNumeric(n)).length !== maxLength || isOffline) { + return; + } + if (!wasSubmitted) { + setWasSubmitted(true); + } + // Blurs the input and removes focus from the last input and, if it should submit + // on complete, it will call the onFulfill callback. + blurMagicCodeInput(); + onFulfill(value); + lastValue.current = ''; + }; + + const {isOffline} = useNetwork({onReconnect: validateAndSubmit}); + + useEffect(() => { + validateAndSubmit(); + + // We have not added: + // + the editIndex as the dependency because we don't want to run this logic after focusing on an input to edit it after the user has completed the code. + // + the onFulfill as the dependency because onFulfill is changed when the preferred locale changed => avoid auto submit form when preferred locale changed. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [value, shouldSubmitOnComplete]); + + /** + * Focuses on the input when it is pressed. + * + * @param event + * @param index + */ + const onFocus = (event: NativeSyntheticEvent) => { + if (shouldFocusLast.current) { + lastValue.current = TEXT_INPUT_EMPTY_STATE; + setInputAndIndex(lastFocusedIndex.current); + } + event.preventDefault(); + }; + + /** + * Callback for the onPress event, updates the indexes + * of the currently focused input. + * + * @param index + */ + const onPress = (index: number) => { + shouldFocusLast.current = false; + // TapGestureHandler works differently on mobile web and native app + // On web gesture handler doesn't block interactions with textInput below so there is no need to run `focus()` manually + if (!Browser.isMobileChrome() && !Browser.isMobileSafari()) { + inputRefs.current?.focus(); + } + setInputAndIndex(index); + lastFocusedIndex.current = index; + }; + + /** + * Updates the magic inputs with the contents written in the + * input. It spreads each number into each input and updates + * the focused input on the next empty one, if exists. + * It handles both fast typing and only one digit at a time + * in a specific position. + * + * @param value + */ + const onChangeText = (val: string) => { + console.log('ON CHANGE', val) + if (!val || !ValidationUtils.isNumeric(val)) { + return; + } + + // Checks if one new character was added, or if the content was replaced + const hasToSlice = val.length - 1 === lastValue.current.length && val.slice(0, val.length - 1) === lastValue.current; + + // Gets the new value added by the user + const addedValue = hasToSlice ? val.slice(lastValue.current.length, val.length) : val; + + lastValue.current = val; + // Updates the focused input taking into consideration the last input + // edited and the number of digits added by the user. + const numbersArr = addedValue + .trim() + .split('') + .slice(0, maxLength - editIndex); + const updatedFocusedIndex = Math.min(editIndex + (numbersArr.length - 1) + 1, maxLength - 1); + + let numbers = decomposeString(val, maxLength); + numbers = [...numbers.slice(0, editIndex), ...numbersArr, ...numbers.slice(numbersArr.length + editIndex, maxLength)]; + + setInputAndIndex(updatedFocusedIndex); + + const finalInput = composeToString(numbers); + onChangeTextProp(finalInput); + }; + + /** + * Handles logic related to certain key presses. + * + * NOTE: when using Android Emulator, this can only be tested using + * hardware keyboard inputs. + * + * @param event + */ + const onKeyPress = ({nativeEvent: {key: keyValue}}) => { + if (keyValue === 'Backspace' || keyValue === '<') { + let numbers = decomposeString(value, maxLength); + + // If keyboard is disabled and no input is focused we need to remove + // the last entered digit and focus on the correct input + if (isDisableKeyboard && focusedIndex === undefined) { + const indexBeforeLastEditIndex = editIndex === 0 ? editIndex : editIndex - 1; + + const indexToFocus = numbers[editIndex] === CONST.MAGIC_CODE_EMPTY_CHAR ? indexBeforeLastEditIndex : editIndex; + inputRefs.current[indexToFocus].focus(); + onChangeTextProp(value.substring(0, indexToFocus)); + + return; + } + + // If the currently focused index already has a value, it will delete + // that value but maintain the focus on the same input. + if (numbers[focusedIndex] !== CONST.MAGIC_CODE_EMPTY_CHAR) { + setInput(TEXT_INPUT_EMPTY_STATE); + numbers = [...numbers.slice(0, focusedIndex), CONST.MAGIC_CODE_EMPTY_CHAR, ...numbers.slice(focusedIndex + 1, maxLength)]; + setEditIndex(focusedIndex); + onChangeTextProp(composeToString(numbers)); + return; + } + + const hasInputs = numbers.filter((n) => ValidationUtils.isNumeric(n)).length !== 0 + + // Fill the array with empty characters if there are no inputs. + if (focusedIndex === 0 && !hasInputs) { + numbers = Array(maxLength).fill(CONST.MAGIC_CODE_EMPTY_CHAR); + + // Deletes the value of the previous input and focuses on it. + } else if (focusedIndex !== 0) { + numbers = [...numbers.slice(0, Math.max(0, focusedIndex - 1)), CONST.MAGIC_CODE_EMPTY_CHAR, ...numbers.slice(focusedIndex, maxLength)]; + } + + const newFocusedIndex = Math.max(0, focusedIndex - 1); + + // Saves the input string so that it can compare to the change text + // event that will be triggered, this is a workaround for mobile that + // triggers the change text on the event after the key press. + setInputAndIndex(newFocusedIndex); + onChangeTextProp(composeToString(numbers)); + + if (newFocusedIndex !== undefined) { + inputRefs.current?.focus(); + } + } + if (keyValue === 'ArrowLeft' && focusedIndex !== undefined) { + const newFocusedIndex = Math.max(0, focusedIndex - 1); + setInputAndIndex(newFocusedIndex); + inputRefs.current?.focus(); + } else if (keyValue === 'ArrowRight' && focusedIndex !== undefined) { + const newFocusedIndex = Math.min(focusedIndex + 1, maxLength - 1); + setInputAndIndex(newFocusedIndex); + inputRefs.current?.focus(); + } else if (keyValue === 'Enter') { + // We should prevent users from submitting when it's offline. + if (isOffline) { + return; + } + setInput(TEXT_INPUT_EMPTY_STATE); + onFulfill(value); + } + }; + + /** + * If isDisableKeyboard is true we will have to call onKeyPress and onChangeText manually + * as the press on digit pad will not trigger native events. We take lastPressedDigit from props + * as it stores the last pressed digit pressed on digit pad. We take only the first character + * as anything after that is added to differentiate between two same digits passed in a row. + */ + + useEffect(() => { + if (!isDisableKeyboard) { + return; + } + + const val = lastPressedDigit.charAt(0); + onKeyPress({nativeEvent: {key: val}}); + onChangeText(val); + + // We have not added: + // + the onChangeText and onKeyPress as the dependencies because we only want to run this when lastPressedDigit changes. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [lastPressedDigit, isDisableKeyboard]); + + return ( + <> + + { + onPress(Math.floor(e.nativeEvent.x / (inputWidth.current / maxLength))); + }} + > + {/* Android does not handle touch on invisible Views so I created a wrapper around invisible TextInput just to handle taps */} + + { + inputWidth.current = e.nativeEvent.layout.width; + }} + ref={(inputRef) => (inputRefs.current = inputRef)} + autoFocus={autoFocus} + inputMode="numeric" + textContentType="oneTimeCode" + name={name} + maxLength={maxLength} + value={input} + hideFocusedState + autoComplete={input.length === 0 && autoComplete} + shouldDelayFocus={input.length === 0 && shouldDelayFocus} + keyboardType={CONST.KEYBOARD_TYPE.NUMBER_PAD} + onChangeText={(text: string) => { + onChangeText(text); + }} + onKeyPress={onKeyPress} + onFocus={onFocus} + onBlur={() => { + shouldFocusLast.current = true; + lastFocusedIndex.current = focusedIndex; + setFocusedIndex(undefined); + }} + selectionColor="transparent" + inputStyle={[styles.inputTransparent]} + role={CONST.ROLE.FORM} + style={[styles.inputTransparent]} + textInputContainerStyles={[styles.borderNone]} + /> + + + {getInputPlaceholderSlots(maxLength).map((index) => ( + + + {decomposeString(value, maxLength)[index] || ''} + + + ))} + + {errorText && ( + + )} + + ); +} + +MagicCodeInput.displayName = 'MagicCodeInput'; + +export default forwardRef(MagicCodeInput); From f81a7fa611474f9651b915e2f9452eaca197c164 Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Wed, 3 Jan 2024 12:39:00 +0100 Subject: [PATCH 104/580] fix focus issue --- .../{MagicCodeInput.js => MagicCodeInput.tsx} | 176 ++++++++++-------- 1 file changed, 103 insertions(+), 73 deletions(-) rename src/components/{MagicCodeInput.js => MagicCodeInput.tsx} (72%) diff --git a/src/components/MagicCodeInput.js b/src/components/MagicCodeInput.tsx similarity index 72% rename from src/components/MagicCodeInput.js rename to src/components/MagicCodeInput.tsx index 55a65237a691..b238c774405c 100644 --- a/src/components/MagicCodeInput.js +++ b/src/components/MagicCodeInput.tsx @@ -1,8 +1,7 @@ import PropTypes from 'prop-types'; import React, {forwardRef, useEffect, useImperativeHandle, useRef, useState} from 'react'; -import {StyleSheet, View} from 'react-native'; +import {NativeSyntheticEvent, StyleSheet, TextInputFocusEventData, View} from 'react-native'; import {TapGestureHandler} from 'react-native-gesture-handler'; -import _ from 'underscore'; import useNetwork from '@hooks/useNetwork'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -10,17 +9,12 @@ import * as Browser from '@libs/Browser'; import * as ValidationUtils from '@libs/ValidationUtils'; import CONST from '@src/CONST'; import FormHelpMessage from './FormHelpMessage'; -import networkPropTypes from './networkPropTypes'; -import {withNetwork} from './OnyxProvider'; import Text from './Text'; import TextInput from './TextInput'; const TEXT_INPUT_EMPTY_STATE = ''; const propTypes = { - /** Information about the network */ - network: networkPropTypes.isRequired, - /** Name attribute for the input */ name: PropTypes.string, @@ -63,6 +57,49 @@ const propTypes = { lastPressedDigit: PropTypes.string, }; +type MagicCodeInputProps = { + /** Name attribute for the input */ + name?: string, + + /** Input value */ + value?: string, + + /** Should the input auto focus */ + autoFocus?: boolean, + + /** Whether we should wait before focusing the TextInput, useful when using transitions */ + shouldDelayFocus?: boolean, + + /** Error text to display */ + errorText?: string, + + /** Specifies autocomplete hints for the system, so it can provide autofill */ + autoComplete: 'sms-otp' | 'one-time-code' | 'off', + + /* Should submit when the input is complete */ + shouldSubmitOnComplete?: boolean, + + /** Function to call when the input is changed */ + onChangeText?: (value: string) => void, + + /** Function to call when the input is submitted or fully complete */ + onFulfill?: (value: string) => void, + + /** Specifies if the input has a validation error */ + hasError?: boolean, + + /** Specifies the max length of the input */ + maxLength?: number, + + /** Specifies if the keyboard should be disabled */ + isDisableKeyboard?: boolean, + + /** Last pressed digit on BigDigitPad */ + lastPressedDigit?: string, + + innerRef: unknown; +} + const defaultProps = { value: '', name: '', @@ -82,13 +119,9 @@ const defaultProps = { /** * Converts a given string into an array of numbers that must have the same * number of elements as the number of inputs. - * - * @param {String} value - * @param {Number} length - * @returns {Array} */ -const decomposeString = (value, length) => { - let arr = _.map(value.split('').slice(0, length), (v) => (ValidationUtils.isNumeric(v) ? v : CONST.MAGIC_CODE_EMPTY_CHAR)); +const decomposeString = (value: string, length: number): string[] => { + let arr = value.split('').slice(0, length).map((v) => (ValidationUtils.isNumeric(v) ? v : CONST.MAGIC_CODE_EMPTY_CHAR)) if (arr.length < length) { arr = arr.concat(Array(length - arr.length).fill(CONST.MAGIC_CODE_EMPTY_CHAR)); } @@ -98,26 +131,24 @@ const decomposeString = (value, length) => { /** * Converts an array of strings into a single string. If there are undefined or * empty values, it will replace them with a space. - * - * @param {Array} value - * @returns {String} */ -const composeToString = (value) => _.map(value, (v) => (v === undefined || v === '' ? CONST.MAGIC_CODE_EMPTY_CHAR : v)).join(''); +const composeToString = (value: string[]): string => value.map((v) => (v === undefined || v === '' ? CONST.MAGIC_CODE_EMPTY_CHAR : v)).join(''); -const getInputPlaceholderSlots = (length) => Array.from(Array(length).keys()); +const getInputPlaceholderSlots = (length: number): number[] => Array.from(Array(length).keys()); -function MagicCodeInput(props) { +function MagicCodeInput(props: MagicCodeInputProps) { + const {value = '', name = '', autoFocus = true, shouldDelayFocus = false, errorText = '', shouldSubmitOnComplete = true, onChangeText: onChangeTextProp = () => {}} = props const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); const inputRefs = useRef(); const [input, setInput] = useState(TEXT_INPUT_EMPTY_STATE); - const [focusedIndex, setFocusedIndex] = useState(0); + const [focusedIndex, setFocusedIndex] = useState(0); const [editIndex, setEditIndex] = useState(0); const [wasSubmitted, setWasSubmitted] = useState(false); const shouldFocusLast = useRef(false); const inputWidth = useRef(0); const lastFocusedIndex = useRef(0); - const lastValue = useRef(TEXT_INPUT_EMPTY_STATE); + const lastValue = useRef(TEXT_INPUT_EMPTY_STATE); useEffect(() => { lastValue.current = input.length; @@ -135,7 +166,7 @@ function MagicCodeInput(props) { inputRefs.current.focus(); }; - const setInputAndIndex = (index) => { + const setInputAndIndex = (index: number) => { setInput(TEXT_INPUT_EMPTY_STATE); setFocusedIndex(index); setEditIndex(index); @@ -156,7 +187,7 @@ function MagicCodeInput(props) { lastFocusedIndex.current = 0; setInputAndIndex(0); inputRefs.current.focus(); - props.onChangeText(''); + onChangeTextProp(''); }, blur() { blurMagicCodeInput(); @@ -164,8 +195,8 @@ function MagicCodeInput(props) { })); const validateAndSubmit = () => { - const numbers = decomposeString(props.value, props.maxLength); - if (wasSubmitted || !props.shouldSubmitOnComplete || _.filter(numbers, (n) => ValidationUtils.isNumeric(n)).length !== props.maxLength || props.network.isOffline) { + const numbers = decomposeString(value, props.maxLength); + if (wasSubmitted || !shouldSubmitOnComplete || numbers.filter((n) => ValidationUtils.isNumeric(n)).length !== props.maxLength || isOffline) { return; } if (!wasSubmitted) { @@ -174,11 +205,11 @@ function MagicCodeInput(props) { // Blurs the input and removes focus from the last input and, if it should submit // on complete, it will call the onFulfill callback. blurMagicCodeInput(); - props.onFulfill(props.value); + props.onFulfill(value); lastValue.current = ''; }; - useNetwork({onReconnect: validateAndSubmit}); + const {isOffline} = useNetwork({onReconnect: validateAndSubmit}); useEffect(() => { validateAndSubmit(); @@ -187,15 +218,15 @@ function MagicCodeInput(props) { // + the editIndex as the dependency because we don't want to run this logic after focusing on an input to edit it after the user has completed the code. // + the props.onFulfill as the dependency because props.onFulfill is changed when the preferred locale changed => avoid auto submit form when preferred locale changed. // eslint-disable-next-line react-hooks/exhaustive-deps - }, [props.value, props.shouldSubmitOnComplete]); + }, [value, shouldSubmitOnComplete]); /** * Focuses on the input when it is pressed. * - * @param {Object} event - * @param {Number} index + * @param event + * @param index */ - const onFocus = (event) => { + const onFocus = (event: NativeSyntheticEvent) => { if (shouldFocusLast.current) { lastValue.current = TEXT_INPUT_EMPTY_STATE; setInputAndIndex(lastFocusedIndex.current); @@ -207,9 +238,9 @@ function MagicCodeInput(props) { * Callback for the onPress event, updates the indexes * of the currently focused input. * - * @param {Number} index + * @param index */ - const onPress = (index) => { + const onPress = (index: number) => { shouldFocusLast.current = false; // TapGestureHandler works differently on mobile web and native app // On web gesture handler doesn't block interactions with textInput below so there is no need to run `focus()` manually @@ -227,20 +258,20 @@ function MagicCodeInput(props) { * It handles both fast typing and only one digit at a time * in a specific position. * - * @param {String} value + * @param textValue */ - const onChangeText = (value) => { - if (_.isUndefined(value) || _.isEmpty(value) || !ValidationUtils.isNumeric(value)) { + const onChangeText = (textValue?: string) => { + if (!textValue?.length || !ValidationUtils.isNumeric(textValue)) { return; } // Checks if one new character was added, or if the content was replaced - const hasToSlice = value.length - 1 === lastValue.current.length && value.slice(0, value.length - 1) === lastValue.current; + const hasToSlice = typeof lastValue.current === 'string' && textValue.length - 1 === lastValue.current.length && textValue.slice(0, textValue.length - 1) === lastValue.current; - // Gets the new value added by the user - const addedValue = hasToSlice ? value.slice(lastValue.current.length, value.length) : value; + // Gets the new textValue added by the user + const addedValue = (hasToSlice && typeof lastValue.current === 'string') ? textValue.slice(lastValue.current.length, textValue.length) : textValue; - lastValue.current = value; + lastValue.current = textValue; // Updates the focused input taking into consideration the last input // edited and the number of digits added by the user. const numbersArr = addedValue @@ -249,13 +280,13 @@ function MagicCodeInput(props) { .slice(0, props.maxLength - editIndex); const updatedFocusedIndex = Math.min(editIndex + (numbersArr.length - 1) + 1, props.maxLength - 1); - let numbers = decomposeString(props.value, props.maxLength); + let numbers = decomposeString(value, props.maxLength); numbers = [...numbers.slice(0, editIndex), ...numbersArr, ...numbers.slice(numbersArr.length + editIndex, props.maxLength)]; setInputAndIndex(updatedFocusedIndex); const finalInput = composeToString(numbers); - props.onChangeText(finalInput); + onChangeTextProp(finalInput); }; /** @@ -264,11 +295,11 @@ function MagicCodeInput(props) { * NOTE: when using Android Emulator, this can only be tested using * hardware keyboard inputs. * - * @param {Object} event + * @param event */ const onKeyPress = ({nativeEvent: {key: keyValue}}) => { if (keyValue === 'Backspace' || keyValue === '<') { - let numbers = decomposeString(props.value, props.maxLength); + let numbers = decomposeString(value, props.maxLength); // If keyboard is disabled and no input is focused we need to remove // the last entered digit and focus on the correct input @@ -277,59 +308,59 @@ function MagicCodeInput(props) { const indexToFocus = numbers[editIndex] === CONST.MAGIC_CODE_EMPTY_CHAR ? indexBeforeLastEditIndex : editIndex; inputRefs.current[indexToFocus].focus(); - props.onChangeText(props.value.substring(0, indexToFocus)); + onChangeTextProp(value.substring(0, indexToFocus)); return; } // If the currently focused index already has a value, it will delete // that value but maintain the focus on the same input. - if (numbers[focusedIndex] !== CONST.MAGIC_CODE_EMPTY_CHAR) { + if (focusedIndex && numbers[focusedIndex] !== CONST.MAGIC_CODE_EMPTY_CHAR) { setInput(TEXT_INPUT_EMPTY_STATE); numbers = [...numbers.slice(0, focusedIndex), CONST.MAGIC_CODE_EMPTY_CHAR, ...numbers.slice(focusedIndex + 1, props.maxLength)]; setEditIndex(focusedIndex); - props.onChangeText(composeToString(numbers)); + onChangeTextProp(composeToString(numbers)); return; } - const hasInputs = _.filter(numbers, (n) => ValidationUtils.isNumeric(n)).length !== 0; + const hasInputs = numbers.filter((n) => ValidationUtils.isNumeric(n)).length !== 0; // Fill the array with empty characters if there are no inputs. if (focusedIndex === 0 && !hasInputs) { numbers = Array(props.maxLength).fill(CONST.MAGIC_CODE_EMPTY_CHAR); // Deletes the value of the previous input and focuses on it. - } else if (focusedIndex !== 0) { + } else if (focusedIndex && focusedIndex !== 0) { numbers = [...numbers.slice(0, Math.max(0, focusedIndex - 1)), CONST.MAGIC_CODE_EMPTY_CHAR, ...numbers.slice(focusedIndex, props.maxLength)]; } - const newFocusedIndex = Math.max(0, focusedIndex - 1); + const newFocusedIndex = Math.max(0, (focusedIndex ?? 0) - 1); // Saves the input string so that it can compare to the change text // event that will be triggered, this is a workaround for mobile that // triggers the change text on the event after the key press. setInputAndIndex(newFocusedIndex); - props.onChangeText(composeToString(numbers)); + onChangeTextProp(composeToString(numbers)); - if (!_.isUndefined(newFocusedIndex)) { + if (newFocusedIndex !== undefined) { inputRefs.current.focus(); } } - if (keyValue === 'ArrowLeft' && !_.isUndefined(focusedIndex)) { + if (keyValue === 'ArrowLeft' && focusedIndex !== undefined) { const newFocusedIndex = Math.max(0, focusedIndex - 1); setInputAndIndex(newFocusedIndex); inputRefs.current.focus(); - } else if (keyValue === 'ArrowRight' && !_.isUndefined(focusedIndex)) { + } else if (keyValue === 'ArrowRight' && focusedIndex !== undefined) { const newFocusedIndex = Math.min(focusedIndex + 1, props.maxLength - 1); setInputAndIndex(newFocusedIndex); inputRefs.current.focus(); } else if (keyValue === 'Enter') { // We should prevent users from submitting when it's offline. - if (props.network.isOffline) { + if (isOffline) { return; } setInput(TEXT_INPUT_EMPTY_STATE); - props.onFulfill(props.value); + props.onFulfill(value); } }; @@ -345,9 +376,9 @@ function MagicCodeInput(props) { return; } - const value = props.lastPressedDigit.charAt(0); - onKeyPress({nativeEvent: {key: value}}); - onChangeText(value); + const textValue = props.lastPressedDigit.charAt(0); + onKeyPress({nativeEvent: {key: textValue}}); + onChangeText(textValue); // We have not added: // + the onChangeText and onKeyPress as the dependencies because we only want to run this when lastPressedDigit changes. @@ -372,18 +403,18 @@ function MagicCodeInput(props) { inputWidth.current = e.nativeEvent.layout.width; }} ref={(ref) => (inputRefs.current = ref)} - autoFocus={props.autoFocus} + autoFocus={autoFocus} inputMode="numeric" textContentType="oneTimeCode" - name={props.name} + name={name} maxLength={props.maxLength} value={input} hideFocusedState autoComplete={input.length === 0 && props.autoComplete} - shouldDelayFocus={input.length === 0 && props.shouldDelayFocus} + shouldDelayFocus={input.length === 0 && shouldDelayFocus} keyboardType={CONST.KEYBOARD_TYPE.NUMBER_PAD} - onChangeText={(value) => { - onChangeText(value); + onChangeText={(textValue) => { + onChangeText(textValue); }} onKeyPress={onKeyPress} onFocus={onFocus} @@ -394,13 +425,14 @@ function MagicCodeInput(props) { }} selectionColor="transparent" inputStyle={[styles.inputTransparent]} - role={CONST.ACCESSIBILITY_ROLE.TEXT} + // role={CONST.ACCESSIBILITY_ROLE.TEXT} + role='none' style={[styles.inputTransparent]} textInputContainerStyles={[styles.borderNone]} /> - {_.map(getInputPlaceholderSlots(props.maxLength), (index) => ( + {getInputPlaceholderSlots(props.maxLength).map((index) => ( - {decomposeString(props.value, props.maxLength)[index] || ''} + {decomposeString(value, props.maxLength)[index] || ''} ))} - {!_.isEmpty(props.errorText) && ( + {errorText && ( )} @@ -440,6 +472,4 @@ const MagicCodeInputWithRef = forwardRef((props, ref) => ( /> )); -MagicCodeInputWithRef.displayName = 'MagicCodeInputWithRef'; - -export default withNetwork()(MagicCodeInputWithRef); +export default MagicCodeInputWithRef; From 87559169ec7a23ec4748aa2ad06f8743d16a26c2 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Wed, 3 Jan 2024 14:04:34 +0100 Subject: [PATCH 105/580] add and improve comments --- .../AttachmentCarousel/Pager/index.js | 7 +-- src/components/MultiGestureCanvas/index.js | 21 ++++---- .../MultiGestureCanvas/usePanGesture.js | 52 +++++++++++-------- .../MultiGestureCanvas/usePinchGesture.js | 3 +- .../MultiGestureCanvas/useTapGestures.js | 3 +- src/components/MultiGestureCanvas/utils.ts | 12 ++++- 6 files changed, 56 insertions(+), 42 deletions(-) diff --git a/src/components/Attachments/AttachmentCarousel/Pager/index.js b/src/components/Attachments/AttachmentCarousel/Pager/index.js index 699e2fc812cc..ad844c1df854 100644 --- a/src/components/Attachments/AttachmentCarousel/Pager/index.js +++ b/src/components/Attachments/AttachmentCarousel/Pager/index.js @@ -47,7 +47,6 @@ const pagerPropTypes = { onPageSelected: PropTypes.func, onTap: PropTypes.func, onSwipe: PropTypes.func, - onSwipeSuccess: PropTypes.func, onSwipeDown: PropTypes.func, onPinchGestureChange: PropTypes.func, forwardedRef: refPropTypes, @@ -58,13 +57,12 @@ const pagerDefaultProps = { onPageSelected: () => {}, onTap: () => {}, onSwipe: noopWorklet, - onSwipeSuccess: () => {}, onSwipeDown: () => {}, onPinchGestureChange: () => {}, forwardedRef: null, }; -function AttachmentCarouselPager({items, renderItem, initialIndex, onPageSelected, onTap, onSwipe = noopWorklet, onSwipeSuccess, onSwipeDown, onPinchGestureChange, forwardedRef}) { +function AttachmentCarouselPager({items, renderItem, initialIndex, onPageSelected, onTap, onSwipe = noopWorklet, onSwipeDown, onPinchGestureChange, forwardedRef}) { const styles = useThemeStyles(); const shouldPagerScroll = useSharedValue(true); const pagerRef = useRef(null); @@ -124,10 +122,9 @@ function AttachmentCarouselPager({items, renderItem, initialIndex, onPageSelecte onPinchGestureChange, onTap, onSwipe, - onSwipeSuccess, onSwipeDown, }), - [isSwipingHorizontally, pagerRef, shouldPagerScroll, onPinchGestureChange, onTap, onSwipe, onSwipeSuccess, onSwipeDown], + [isSwipingHorizontally, pagerRef, shouldPagerScroll, onPinchGestureChange, onTap, onSwipe, onSwipeDown], ); return ( diff --git a/src/components/MultiGestureCanvas/index.js b/src/components/MultiGestureCanvas/index.js index adbc46112621..9da8053aef07 100644 --- a/src/components/MultiGestureCanvas/index.js +++ b/src/components/MultiGestureCanvas/index.js @@ -1,7 +1,7 @@ import React, {useContext, useEffect, useMemo, useRef} from 'react'; import {View} from 'react-native'; import {Gesture, GestureDetector} from 'react-native-gesture-handler'; -import Animated, {cancelAnimation, runOnUI, useAnimatedReaction, useAnimatedStyle, useDerivedValue, useSharedValue, useWorkletCallback, withSpring} from 'react-native-reanimated'; +import Animated, {cancelAnimation, runOnUI, useAnimatedReaction, useAnimatedStyle, useDerivedValue, useSharedValue, withSpring} from 'react-native-reanimated'; import AttachmentCarouselPagerContext from '@components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -14,6 +14,7 @@ import * as MultiGestureCanvasUtils from './utils'; const SPRING_CONFIG = MultiGestureCanvasUtils.SPRING_CONFIG; const zoomScaleBounceFactors = MultiGestureCanvasUtils.zoomScaleBounceFactors; +const useWorkletCallback = MultiGestureCanvasUtils.useWorkletCallback; function getDeepDefaultProps({contentSize: contentSizeProp = {}, zoomRange: zoomRangeProp = {}}) { const contentSize = { @@ -37,10 +38,9 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr const attachmentCarouselPagerContext = useContext(AttachmentCarouselPagerContext); const pagerRefFallback = useRef(null); - const {onTap, onSwipe, onSwipeSuccess, pagerRef, shouldPagerScroll, isSwipingHorizontally, onPinchGestureChange} = attachmentCarouselPagerContext || { + const {onTap, onSwipeDown, pagerRef, shouldPagerScroll, isSwipingHorizontally, onPinchGestureChange} = attachmentCarouselPagerContext || { onTap: () => undefined, - onSwipe: () => undefined, - onSwipeSuccess: () => undefined, + onSwipeDown: () => undefined, onPinchGestureChange: () => undefined, pagerRef: pagerRefFallback, shouldPagerScroll: false, @@ -131,7 +131,7 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr panTranslateY, isSwipingHorizontally, isSwipingVertically, - onSwipeSuccess, + onSwipeDown, stopAnimation, }); @@ -155,7 +155,8 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr onPinchGestureChange, }); - // reacts to scale change and enables/disables pager scroll + // Enables/disables the pager scroll based on the zoom scale + // When the content is zoomed in/out, the pager should be disabled useAnimatedReaction( () => zoomScale.value, () => { @@ -179,11 +180,9 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr const x = pinchTranslateX.value + pinchBounceTranslateX.value + panTranslateX.value + totalOffsetX.value; const y = pinchTranslateY.value + pinchBounceTranslateY.value + panTranslateY.value + totalOffsetY.value; - // console.log({pinchTranslateY: pinchTranslateY.value, pinchBounceTranslateY: pinchBounceTranslateY.value, panTranslateY: panTranslateY.value, totalOffsetY: totalOffsetY.value}); - - if (isSwipingVertically.value) { - onSwipe(y); - } + // if (isSwipingVertically.value) { + // onSwipe(y); + // } return { transform: [ diff --git a/src/components/MultiGestureCanvas/usePanGesture.js b/src/components/MultiGestureCanvas/usePanGesture.js index 5d6279a8be56..a2b92d3bcf3c 100644 --- a/src/components/MultiGestureCanvas/usePanGesture.js +++ b/src/components/MultiGestureCanvas/usePanGesture.js @@ -1,12 +1,13 @@ /* eslint-disable no-param-reassign */ import {Gesture} from 'react-native-gesture-handler'; -import {runOnJS, useDerivedValue, useSharedValue, useWorkletCallback, withDecay, withSpring} from 'react-native-reanimated'; +import {runOnJS, useDerivedValue, useSharedValue, withDecay, withSpring} from 'react-native-reanimated'; import * as MultiGestureCanvasUtils from './utils'; const PAN_DECAY_DECELARATION = 0.9915; -const clamp = MultiGestureCanvasUtils.clamp; const SPRING_CONFIG = MultiGestureCanvasUtils.SPRING_CONFIG; +const clamp = MultiGestureCanvasUtils.clamp; +const useWorkletCallback = MultiGestureCanvasUtils.useWorkletCallback; const usePanGesture = ({ canvasSize, @@ -24,22 +25,26 @@ const usePanGesture = ({ panTranslateY, isSwipingVertically, isSwipingHorizontally, - onSwipeSuccess, + onSwipeDown, stopAnimation, }) => { + // The content size after scaling it with the current (total) zoom value const zoomScaledContentWidth = useDerivedValue(() => contentSize.width * totalScale.value, [contentSize.width]); const zoomScaledContentHeight = useDerivedValue(() => contentSize.height * totalScale.value, [contentSize.height]); + // Used to track previous touch position for the "swipe down to close" gesture const previousTouch = useSharedValue(null); - // pan velocity to calculate the decay + + // Pan velocity to calculate the decay const panVelocityX = useSharedValue(0); const panVelocityY = useSharedValue(0); - // disable pan vertically when content is smaller than screen + + // Disable "swipe down to close" gesture when content is bigger than the canvas const canPanVertically = useDerivedValue(() => canvasSize.height < zoomScaledContentHeight.value, [canvasSize.height]); - // calculates bounds of the scaled content - // can we pan left/right/up/down - // can be used to limit gesture or implementing tension effect + // Calculates bounds of the scaled content + // Can we pan left/right/up/down + // Can be used to limit gesture or implementing tension effect const getBounds = useWorkletCallback(() => { let rightBoundary = 0; let topBoundary = 0; @@ -78,12 +83,12 @@ const usePanGesture = ({ const {target, isInBoundaryX, isInBoundaryY, minVector, maxVector} = getBounds(); if (zoomScale.value === zoomRange.min && totalOffsetX.value === 0 && totalOffsetY.value === 0 && panTranslateX.value === 0 && panTranslateY.value === 0) { - // we don't need to run any animations + // We don't need to run any animations return; } + // If we are zoomed out, we want to center the content if (zoomScale.value <= zoomRange.min) { - // just center it totalOffsetX.value = withSpring(0, SPRING_CONFIG); totalOffsetY.value = withSpring(0, SPRING_CONFIG); return; @@ -108,7 +113,7 @@ const usePanGesture = ({ if ( Math.abs(panVelocityY.value) > 0 && zoomScale.value <= zoomRange.max && - // limit vertical pan only when content is smaller than screen + // Limit vertical panning when content is smaller than screen totalOffsetY.value !== minVector.y && totalOffsetY.value !== maxVector.y ) { @@ -143,10 +148,9 @@ const usePanGesture = ({ // if (Math.abs(velocityY) > velocityX && velocityY > 20) { // state.activate(); - // isSwiping.value = true; + // isSwipingVertically.value = true; // previousTouch.value = null; - // runOnJS(onSwipeDown)(); // return; // } // } @@ -163,10 +167,10 @@ const usePanGesture = ({ stopAnimation(); }) .onChange((evt) => { - // since we're running both pinch and pan gesture handlers simultaneously - // we need to make sure that we don't pan when we pinch and move fingers - // since we track it as pinch focal gesture - // we also need to prevent panning when we are swiping horizontally (in the pager) + // Since we're running both pinch and pan gesture handlers simultaneously, + // we need to make sure that we don't pan when we pinch AND move fingers + // since we track it as pinch focal gesture. + // We also need to prevent panning when we are swiping horizontally (from page to page) if (evt.numberOfPointers > 1 || isSwipingHorizontally.value) { return; } @@ -184,20 +188,22 @@ const usePanGesture = ({ } }) .onEnd((evt) => { - // add pan translation to total offset + // Add pan translation to total offset totalOffsetX.value += panTranslateX.value; totalOffsetY.value += panTranslateY.value; - // reset pan gesture variables + + // Reset pan gesture variables panTranslateX.value = 0; panTranslateY.value = 0; previousTouch.value = null; - // If we are swiping, we don't want to return to boundaries + // If we are swiping (in the pager), we don't want to return to boundaries if (isSwipingHorizontally.value) { return; } - // swipe to close animation when swiping down + // Triggers the "swipe down to close" animation and the "onSwipeDown" callback, + // which can be used to close the lightbox/carousel if (isSwipingVertically.value) { const enoughVelocity = Math.abs(evt.velocityY) > 300 && Math.abs(evt.velocityX) < Math.abs(evt.velocityY); const rightDirection = (evt.translationY > 0 && evt.velocityY > 0) || (evt.translationY < 0 && evt.velocityY < 0); @@ -220,7 +226,7 @@ const usePanGesture = ({ velocity: Math.abs(evt.velocityY) < 1200 ? maybeInvert(1200) : evt.velocityY, }, () => { - runOnJS(onSwipeSuccess)(); + runOnJS(onSwipeDown)(); }, ); return; @@ -229,7 +235,7 @@ const usePanGesture = ({ returnToBoundaries(); - // reset pan gesture variables + // Reset pan gesture variables panVelocityX.value = 0; panVelocityY.value = 0; }) diff --git a/src/components/MultiGestureCanvas/usePinchGesture.js b/src/components/MultiGestureCanvas/usePinchGesture.js index 78aed77814cd..fad23d1d409a 100644 --- a/src/components/MultiGestureCanvas/usePinchGesture.js +++ b/src/components/MultiGestureCanvas/usePinchGesture.js @@ -1,11 +1,12 @@ /* eslint-disable no-param-reassign */ import {useEffect, useState} from 'react'; import {Gesture} from 'react-native-gesture-handler'; -import {runOnJS, useAnimatedReaction, useSharedValue, useWorkletCallback, withSpring} from 'react-native-reanimated'; +import {runOnJS, useAnimatedReaction, useSharedValue, withSpring} from 'react-native-reanimated'; import * as MultiGestureCanvasUtils from './utils'; const SPRING_CONFIG = MultiGestureCanvasUtils.SPRING_CONFIG; const zoomScaleBounceFactors = MultiGestureCanvasUtils.zoomScaleBounceFactors; +const useWorkletCallback = MultiGestureCanvasUtils.useWorkletCallback; const usePinchGesture = ({ canvasSize, diff --git a/src/components/MultiGestureCanvas/useTapGestures.js b/src/components/MultiGestureCanvas/useTapGestures.js index a08fbc8fed4c..0b354ed6c54a 100644 --- a/src/components/MultiGestureCanvas/useTapGestures.js +++ b/src/components/MultiGestureCanvas/useTapGestures.js @@ -1,13 +1,14 @@ /* eslint-disable no-param-reassign */ import {useMemo} from 'react'; import {Gesture} from 'react-native-gesture-handler'; -import {runOnJS, useWorkletCallback, withSpring} from 'react-native-reanimated'; +import {runOnJS, withSpring} from 'react-native-reanimated'; import * as MultiGestureCanvasUtils from './utils'; const DOUBLE_TAP_SCALE = 3; const clamp = MultiGestureCanvasUtils.clamp; const SPRING_CONFIG = MultiGestureCanvasUtils.SPRING_CONFIG; +const useWorkletCallback = MultiGestureCanvasUtils.useWorkletCallback; const useTapGestures = ({ canvasSize, diff --git a/src/components/MultiGestureCanvas/utils.ts b/src/components/MultiGestureCanvas/utils.ts index da4c1133d237..7a4ba21358c4 100644 --- a/src/components/MultiGestureCanvas/utils.ts +++ b/src/components/MultiGestureCanvas/utils.ts @@ -1,3 +1,5 @@ +import {useCallback} from 'react'; + const SPRING_CONFIG = { mass: 1, stiffness: 1000, @@ -8,10 +10,18 @@ const zoomScaleBounceFactors = { min: 0.7, max: 1.5, }; + function clamp(value: number, lowerBound: number, upperBound: number) { 'worklet'; return Math.min(Math.max(lowerBound, value), upperBound); } -export {clamp, SPRING_CONFIG, zoomScaleBounceFactors}; +const useWorkletCallback = (callback: Parameters[0], deps: Parameters[1] = []) => { + 'worklet'; + + // eslint-disable-next-line react-hooks/exhaustive-deps + return useCallback(callback, deps); +}; + +export {SPRING_CONFIG, zoomScaleBounceFactors, clamp, useWorkletCallback}; From 0e7fe03e1a61782d40e8ce08b3144c0bf06cbb84 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Wed, 3 Jan 2024 14:17:40 +0100 Subject: [PATCH 106/580] remove "swipe down to close gesture" --- .../AttachmentCarousel/Pager/index.js | 16 +--- src/components/MultiGestureCanvas/index.js | 10 +-- .../MultiGestureCanvas/usePanGesture.js | 75 ++----------------- 3 files changed, 9 insertions(+), 92 deletions(-) diff --git a/src/components/Attachments/AttachmentCarousel/Pager/index.js b/src/components/Attachments/AttachmentCarousel/Pager/index.js index ad844c1df854..a85ae10e2328 100644 --- a/src/components/Attachments/AttachmentCarousel/Pager/index.js +++ b/src/components/Attachments/AttachmentCarousel/Pager/index.js @@ -29,12 +29,6 @@ function usePageScrollHandler(handlers, dependencies) { ); } -const noopWorklet = () => { - 'worklet'; - - // noop -}; - const pagerPropTypes = { items: PropTypes.arrayOf( PropTypes.shape({ @@ -46,8 +40,6 @@ const pagerPropTypes = { initialIndex: PropTypes.number, onPageSelected: PropTypes.func, onTap: PropTypes.func, - onSwipe: PropTypes.func, - onSwipeDown: PropTypes.func, onPinchGestureChange: PropTypes.func, forwardedRef: refPropTypes, }; @@ -56,13 +48,11 @@ const pagerDefaultProps = { initialIndex: 0, onPageSelected: () => {}, onTap: () => {}, - onSwipe: noopWorklet, - onSwipeDown: () => {}, onPinchGestureChange: () => {}, forwardedRef: null, }; -function AttachmentCarouselPager({items, renderItem, initialIndex, onPageSelected, onTap, onSwipe = noopWorklet, onSwipeDown, onPinchGestureChange, forwardedRef}) { +function AttachmentCarouselPager({items, renderItem, initialIndex, onPageSelected, onTap, onPinchGestureChange, forwardedRef}) { const styles = useThemeStyles(); const shouldPagerScroll = useSharedValue(true); const pagerRef = useRef(null); @@ -121,10 +111,8 @@ function AttachmentCarouselPager({items, renderItem, initialIndex, onPageSelecte shouldPagerScroll, onPinchGestureChange, onTap, - onSwipe, - onSwipeDown, }), - [isSwipingHorizontally, pagerRef, shouldPagerScroll, onPinchGestureChange, onTap, onSwipe, onSwipeDown], + [isSwipingHorizontally, pagerRef, shouldPagerScroll, onPinchGestureChange, onTap], ); return ( diff --git a/src/components/MultiGestureCanvas/index.js b/src/components/MultiGestureCanvas/index.js index 9da8053aef07..1d91e4cad20e 100644 --- a/src/components/MultiGestureCanvas/index.js +++ b/src/components/MultiGestureCanvas/index.js @@ -38,9 +38,8 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr const attachmentCarouselPagerContext = useContext(AttachmentCarouselPagerContext); const pagerRefFallback = useRef(null); - const {onTap, onSwipeDown, pagerRef, shouldPagerScroll, isSwipingHorizontally, onPinchGestureChange} = attachmentCarouselPagerContext || { + const {onTap, pagerRef, shouldPagerScroll, isSwipingHorizontally, onPinchGestureChange} = attachmentCarouselPagerContext || { onTap: () => undefined, - onSwipeDown: () => undefined, onPinchGestureChange: () => undefined, pagerRef: pagerRefFallback, shouldPagerScroll: false, @@ -63,7 +62,6 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr // pan gesture const panTranslateX = useSharedValue(0); const panTranslateY = useSharedValue(0); - const isSwipingVertically = useSharedValue(false); const panGestureRef = useRef(Gesture.Pan()); // pinch gesture @@ -130,8 +128,6 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr panTranslateX, panTranslateY, isSwipingHorizontally, - isSwipingVertically, - onSwipeDown, stopAnimation, }); @@ -180,10 +176,6 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr const x = pinchTranslateX.value + pinchBounceTranslateX.value + panTranslateX.value + totalOffsetX.value; const y = pinchTranslateY.value + pinchBounceTranslateY.value + panTranslateY.value + totalOffsetY.value; - // if (isSwipingVertically.value) { - // onSwipe(y); - // } - return { transform: [ { diff --git a/src/components/MultiGestureCanvas/usePanGesture.js b/src/components/MultiGestureCanvas/usePanGesture.js index a2b92d3bcf3c..807252670b43 100644 --- a/src/components/MultiGestureCanvas/usePanGesture.js +++ b/src/components/MultiGestureCanvas/usePanGesture.js @@ -1,6 +1,6 @@ /* eslint-disable no-param-reassign */ import {Gesture} from 'react-native-gesture-handler'; -import {runOnJS, useDerivedValue, useSharedValue, withDecay, withSpring} from 'react-native-reanimated'; +import {useDerivedValue, useSharedValue, withDecay, withSpring} from 'react-native-reanimated'; import * as MultiGestureCanvasUtils from './utils'; const PAN_DECAY_DECELARATION = 0.9915; @@ -23,9 +23,7 @@ const usePanGesture = ({ totalOffsetY, panTranslateX, panTranslateY, - isSwipingVertically, isSwipingHorizontally, - onSwipeDown, stopAnimation, }) => { // The content size after scaling it with the current (total) zoom value @@ -39,9 +37,6 @@ const usePanGesture = ({ const panVelocityX = useSharedValue(0); const panVelocityY = useSharedValue(0); - // Disable "swipe down to close" gesture when content is bigger than the canvas - const canPanVertically = useDerivedValue(() => canvasSize.height < zoomScaledContentHeight.value, [canvasSize.height]); - // Calculates bounds of the scaled content // Can we pan left/right/up/down // Can be used to limit gesture or implementing tension effect @@ -107,9 +102,7 @@ const usePanGesture = ({ totalOffsetX.value = withSpring(target.x, SPRING_CONFIG); } - if (!canPanVertically.value) { - totalOffsetY.value = withSpring(target.y, SPRING_CONFIG); - } else if (isInBoundaryY) { + if (isInBoundaryY) { if ( Math.abs(panVelocityY.value) > 0 && zoomScale.value <= zoomRange.max && @@ -124,9 +117,7 @@ const usePanGesture = ({ }); } } else { - totalOffsetY.value = withSpring(target.y, SPRING_CONFIG, () => { - isSwipingVertically.value = false; - }); + totalOffsetY.value = withSpring(target.y, SPRING_CONFIG); } }); @@ -138,23 +129,6 @@ const usePanGesture = ({ state.activate(); } - // TODO: Swipe down to close carousel gesture - // this needs fine tuning to work properly - // if (!isSwipingHorizontally.value && scale.value === 1 && previousTouch.value != null) { - // const velocityX = Math.abs(evt.allTouches[0].x - previousTouch.value.x); - // const velocityY = evt.allTouches[0].y - previousTouch.value.y; - - // // TODO: this needs tuning - // if (Math.abs(velocityY) > velocityX && velocityY > 20) { - // state.activate(); - - // isSwipingVertically.value = true; - // previousTouch.value = null; - - // return; - // } - // } - if (previousTouch.value == null) { previousTouch.value = { x: evt.allTouches[0].x, @@ -176,18 +150,12 @@ const usePanGesture = ({ } panVelocityX.value = evt.velocityX; - panVelocityY.value = evt.velocityY; - if (!isSwipingVertically.value) { - panTranslateX.value += evt.changeX; - } - - if (canPanVertically.value || isSwipingVertically.value) { - panTranslateY.value += evt.changeY; - } + panTranslateX.value += evt.changeX; + panTranslateY.value += evt.changeY; }) - .onEnd((evt) => { + .onEnd(() => { // Add pan translation to total offset totalOffsetX.value += panTranslateX.value; totalOffsetY.value += panTranslateY.value; @@ -202,37 +170,6 @@ const usePanGesture = ({ return; } - // Triggers the "swipe down to close" animation and the "onSwipeDown" callback, - // which can be used to close the lightbox/carousel - if (isSwipingVertically.value) { - const enoughVelocity = Math.abs(evt.velocityY) > 300 && Math.abs(evt.velocityX) < Math.abs(evt.velocityY); - const rightDirection = (evt.translationY > 0 && evt.velocityY > 0) || (evt.translationY < 0 && evt.velocityY < 0); - - if (enoughVelocity && rightDirection) { - const maybeInvert = (v) => { - const invert = evt.velocityY < 0; - return invert ? -v : v; - }; - - totalOffsetY.value = withSpring( - maybeInvert(contentSize.height * 2), - { - stiffness: 50, - damping: 30, - mass: 1, - overshootClamping: true, - restDisplacementThreshold: 300, - restSpeedThreshold: 300, - velocity: Math.abs(evt.velocityY) < 1200 ? maybeInvert(1200) : evt.velocityY, - }, - () => { - runOnJS(onSwipeDown)(); - }, - ); - return; - } - } - returnToBoundaries(); // Reset pan gesture variables From 70bd5b786ccf0f81205b1a95176f64dc55d7050a Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Wed, 3 Jan 2024 14:31:39 +0100 Subject: [PATCH 107/580] remove unused props --- src/components/AttachmentModal.js | 1 - src/components/Attachments/AttachmentCarousel/index.native.js | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/components/AttachmentModal.js b/src/components/AttachmentModal.js index 863e59aa4474..7c062366f8a7 100755 --- a/src/components/AttachmentModal.js +++ b/src/components/AttachmentModal.js @@ -449,7 +449,6 @@ function AttachmentModal(props) { report={props.report} onNavigate={onNavigate} source={props.source} - onClose={closeModal} onToggleKeyboard={updateConfirmButtonVisibility} setDownloadButtonVisibility={setDownloadButtonVisibility} /> diff --git a/src/components/Attachments/AttachmentCarousel/index.native.js b/src/components/Attachments/AttachmentCarousel/index.native.js index f5479b73abdb..003c27844fbc 100644 --- a/src/components/Attachments/AttachmentCarousel/index.native.js +++ b/src/components/Attachments/AttachmentCarousel/index.native.js @@ -18,7 +18,7 @@ import extractAttachmentsFromReport from './extractAttachmentsFromReport'; import AttachmentCarouselPager from './Pager'; import useCarouselArrows from './useCarouselArrows'; -function AttachmentCarousel({report, reportActions, parentReportActions, source, onNavigate, setDownloadButtonVisibility, translate, onClose}) { +function AttachmentCarousel({report, reportActions, parentReportActions, source, onNavigate, setDownloadButtonVisibility, translate}) { const styles = useThemeStyles(); const pagerRef = useRef(null); const [page, setPage] = useState(); @@ -147,7 +147,6 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source, setShouldShowArrows(true); } }} - onSwipeDown={onClose} ref={pagerRef} /> From f95f9c664917d60af6e9b234373a70e95eb179c5 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Wed, 3 Jan 2024 14:34:33 +0100 Subject: [PATCH 108/580] rename variable --- .../Attachments/AttachmentCarousel/Pager/index.js | 10 +++++----- src/components/MultiGestureCanvas/index.js | 8 ++++---- src/components/MultiGestureCanvas/usePanGesture.js | 6 +++--- src/components/MultiGestureCanvas/usePinchGesture.js | 4 ++-- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/components/Attachments/AttachmentCarousel/Pager/index.js b/src/components/Attachments/AttachmentCarousel/Pager/index.js index a85ae10e2328..d7d6bda1be29 100644 --- a/src/components/Attachments/AttachmentCarousel/Pager/index.js +++ b/src/components/Attachments/AttachmentCarousel/Pager/index.js @@ -57,7 +57,7 @@ function AttachmentCarouselPager({items, renderItem, initialIndex, onPageSelecte const shouldPagerScroll = useSharedValue(true); const pagerRef = useRef(null); - const isSwipingHorizontally = useSharedValue(false); + const isSwipingInPager = useSharedValue(false); const activeIndex = useSharedValue(initialIndex); const pageScrollHandler = usePageScrollHandler( @@ -66,7 +66,7 @@ function AttachmentCarouselPager({items, renderItem, initialIndex, onPageSelecte 'worklet'; activeIndex.value = e.position; - isSwipingHorizontally.value = e.offset !== 0; + isSwipingInPager.value = e.offset !== 0; }, }, [], @@ -82,7 +82,7 @@ function AttachmentCarouselPager({items, renderItem, initialIndex, onPageSelecte // we use reanimated for this since onPageSelected is called // in the middle of the pager animation useAnimatedReaction( - () => isSwipingHorizontally.value, + () => isSwipingInPager.value, (stillScrolling) => { if (stillScrolling) { return; @@ -106,13 +106,13 @@ function AttachmentCarouselPager({items, renderItem, initialIndex, onPageSelecte const contextValue = useMemo( () => ({ - isSwipingHorizontally, + isSwipingInPager, pagerRef, shouldPagerScroll, onPinchGestureChange, onTap, }), - [isSwipingHorizontally, pagerRef, shouldPagerScroll, onPinchGestureChange, onTap], + [isSwipingInPager, pagerRef, shouldPagerScroll, onPinchGestureChange, onTap], ); return ( diff --git a/src/components/MultiGestureCanvas/index.js b/src/components/MultiGestureCanvas/index.js index 1d91e4cad20e..24f01cf5f3f1 100644 --- a/src/components/MultiGestureCanvas/index.js +++ b/src/components/MultiGestureCanvas/index.js @@ -38,12 +38,12 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr const attachmentCarouselPagerContext = useContext(AttachmentCarouselPagerContext); const pagerRefFallback = useRef(null); - const {onTap, pagerRef, shouldPagerScroll, isSwipingHorizontally, onPinchGestureChange} = attachmentCarouselPagerContext || { + const {onTap, pagerRef, shouldPagerScroll, isSwipingInPager, onPinchGestureChange} = attachmentCarouselPagerContext || { onTap: () => undefined, onPinchGestureChange: () => undefined, pagerRef: pagerRefFallback, shouldPagerScroll: false, - isSwipingHorizontally: false, + isSwipingInPager: false, ...props, }; @@ -127,7 +127,7 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr totalOffsetY, panTranslateX, panTranslateY, - isSwipingHorizontally, + isSwipingInPager, stopAnimation, }); @@ -145,7 +145,7 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr pinchBounceTranslateX, pinchBounceTranslateY, pinchScaleOffset, - isSwipingHorizontally, + isSwipingInPager, stopAnimation, onScaleChanged, onPinchGestureChange, diff --git a/src/components/MultiGestureCanvas/usePanGesture.js b/src/components/MultiGestureCanvas/usePanGesture.js index 807252670b43..b6639dcac1a6 100644 --- a/src/components/MultiGestureCanvas/usePanGesture.js +++ b/src/components/MultiGestureCanvas/usePanGesture.js @@ -23,7 +23,7 @@ const usePanGesture = ({ totalOffsetY, panTranslateX, panTranslateY, - isSwipingHorizontally, + isSwipingInPager, stopAnimation, }) => { // The content size after scaling it with the current (total) zoom value @@ -145,7 +145,7 @@ const usePanGesture = ({ // we need to make sure that we don't pan when we pinch AND move fingers // since we track it as pinch focal gesture. // We also need to prevent panning when we are swiping horizontally (from page to page) - if (evt.numberOfPointers > 1 || isSwipingHorizontally.value) { + if (evt.numberOfPointers > 1 || isSwipingInPager.value) { return; } @@ -166,7 +166,7 @@ const usePanGesture = ({ previousTouch.value = null; // If we are swiping (in the pager), we don't want to return to boundaries - if (isSwipingHorizontally.value) { + if (isSwipingInPager.value) { return; } diff --git a/src/components/MultiGestureCanvas/usePinchGesture.js b/src/components/MultiGestureCanvas/usePinchGesture.js index fad23d1d409a..630ace4e3042 100644 --- a/src/components/MultiGestureCanvas/usePinchGesture.js +++ b/src/components/MultiGestureCanvas/usePinchGesture.js @@ -22,7 +22,7 @@ const usePinchGesture = ({ pinchBounceTranslateX, pinchBounceTranslateY, pinchScaleOffset, - isSwipingHorizontally, + isSwipingInPager, stopAnimation, onScaleChanged, onPinchGestureChange, @@ -46,7 +46,7 @@ const usePinchGesture = ({ const pinchGesture = Gesture.Pinch() .onTouchesDown((evt, state) => { // we don't want to activate pinch gesture when we are scrolling pager - if (!isSwipingHorizontally.value) { + if (!isSwipingInPager.value) { return; } From 610f59fa1713148cb971ddcb913b9b782bd23cd0 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Wed, 3 Jan 2024 14:56:43 +0100 Subject: [PATCH 109/580] simplify pinch gesture --- src/components/MultiGestureCanvas/index.js | 56 ++++++------- .../MultiGestureCanvas/usePinchGesture.js | 78 ++++++++++++------- .../MultiGestureCanvas/useTapGestures.js | 6 +- 3 files changed, 80 insertions(+), 60 deletions(-) diff --git a/src/components/MultiGestureCanvas/index.js b/src/components/MultiGestureCanvas/index.js index 24f01cf5f3f1..ade0a7b54c38 100644 --- a/src/components/MultiGestureCanvas/index.js +++ b/src/components/MultiGestureCanvas/index.js @@ -50,27 +50,24 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr const {minScale: minContentScale, maxScale: maxContentScale} = useMemo(() => getCanvasFitScale({canvasSize, contentSize}), [canvasSize, contentSize]); const zoomScale = useSharedValue(1); - // Adding together the pinch zoom scale and the initial scale to fit the content into the canvas - // Using the smaller content scale, so that the immage is not bigger than the canvas + + // Adding together zoom scale and the initial scale to fit the content into the canvas + // Using the minimum content scale, so that the image is not bigger than the canvas // and not smaller than needed to fit const totalScale = useDerivedValue(() => zoomScale.value * minContentScale, [minContentScale]); - // total offset of the canvas (panning + pinching offset) - const totalOffsetX = useSharedValue(0); - const totalOffsetY = useSharedValue(0); - - // pan gesture const panTranslateX = useSharedValue(0); const panTranslateY = useSharedValue(0); const panGestureRef = useRef(Gesture.Pan()); - // pinch gesture + const pinchScale = useSharedValue(1); const pinchTranslateX = useSharedValue(0); const pinchTranslateY = useSharedValue(0); - const pinchBounceTranslateX = useSharedValue(0); - const pinchBounceTranslateY = useSharedValue(0); - // scale in between gestures - const pinchScaleOffset = useSharedValue(1); + + // Total offset of the canvas + // Contains both offsets from panning and pinching gestures + const totalOffsetX = useSharedValue(0); + const totalOffsetY = useSharedValue(0); const stopAnimation = useWorkletCallback(() => { cancelAnimation(totalOffsetX); @@ -78,23 +75,30 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr }); const reset = useWorkletCallback((animated) => { - pinchScaleOffset.value = 1; + pinchScale.value = 1; stopAnimation(); + pinchScale.value = 1; + if (animated) { totalOffsetX.value = withSpring(0, SPRING_CONFIG); totalOffsetY.value = withSpring(0, SPRING_CONFIG); + panTranslateX.value = withSpring(0, SPRING_CONFIG); + panTranslateY.value = withSpring(0, SPRING_CONFIG); + pinchTranslateX.value = withSpring(0, SPRING_CONFIG); + pinchTranslateY.value = withSpring(0, SPRING_CONFIG); zoomScale.value = withSpring(1, SPRING_CONFIG); - } else { - totalOffsetX.value = 0; - totalOffsetY.value = 0; - zoomScale.value = 1; - panTranslateX.value = 0; - panTranslateY.value = 0; - pinchTranslateX.value = 0; - pinchTranslateY.value = 0; + return; } + + totalOffsetX.value = 0; + totalOffsetY.value = 0; + panTranslateX.value = 0; + panTranslateY.value = 0; + pinchTranslateX.value = 0; + pinchTranslateY.value = 0; + zoomScale.value = 1; }); const {singleTap, doubleTap} = useTapGestures({ @@ -105,7 +109,7 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr panGestureRef, totalOffsetX, totalOffsetY, - pinchScaleOffset, + pinchScale, zoomScale, reset, stopAnimation, @@ -142,9 +146,7 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr totalOffsetY, pinchTranslateX, pinchTranslateY, - pinchBounceTranslateX, - pinchBounceTranslateY, - pinchScaleOffset, + pinchScale, isSwipingInPager, stopAnimation, onScaleChanged, @@ -173,8 +175,8 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr }, [isActive, mounted, reset]); const animatedStyles = useAnimatedStyle(() => { - const x = pinchTranslateX.value + pinchBounceTranslateX.value + panTranslateX.value + totalOffsetX.value; - const y = pinchTranslateY.value + pinchBounceTranslateY.value + panTranslateY.value + totalOffsetY.value; + const x = pinchTranslateX.value + panTranslateX.value + totalOffsetX.value; + const y = pinchTranslateY.value + panTranslateY.value + totalOffsetY.value; return { transform: [ diff --git a/src/components/MultiGestureCanvas/usePinchGesture.js b/src/components/MultiGestureCanvas/usePinchGesture.js index 630ace4e3042..47964aa7bb2e 100644 --- a/src/components/MultiGestureCanvas/usePinchGesture.js +++ b/src/components/MultiGestureCanvas/usePinchGesture.js @@ -17,25 +17,32 @@ const usePinchGesture = ({ zoomRange, totalOffsetX, totalOffsetY, - pinchTranslateX, - pinchTranslateY, - pinchBounceTranslateX, - pinchBounceTranslateY, - pinchScaleOffset, + totalPinchTranslateX, + totalPinchTranslateY, + pinchScale, isSwipingInPager, stopAnimation, onScaleChanged, onPinchGestureChange, }) => { const isPinchGestureRunning = useSharedValue(false); - // used to store event scale value when we limit scale - const pinchGestureScale = useSharedValue(1); - // origin of the pinch gesture + + // Used to store event scale value when we limit scale + const currentPinchScale = useSharedValue(1); + + // Origin of the pinch gesture const pinchOrigin = { x: useSharedValue(0), y: useSharedValue(0), }; + const pinchTranslateX = useSharedValue(0); + const pinchTranslateY = useSharedValue(0); + // In order to keep track of the "bounce" effect when pinching over/under the min/max zoom scale + // we need to have extra "bounce" translation variables + const pinchBounceTranslateX = useSharedValue(0); + const pinchBounceTranslateY = useSharedValue(0); + const getAdjustedFocal = useWorkletCallback( (focalX, focalY) => ({ x: focalX - (canvasSize.width / 2 + totalOffsetX.value), @@ -45,7 +52,7 @@ const usePinchGesture = ({ ); const pinchGesture = Gesture.Pinch() .onTouchesDown((evt, state) => { - // we don't want to activate pinch gesture when we are scrolling pager + // We don't want to activate pinch gesture when we are scrolling pager if (!isSwipingInPager.value) { return; } @@ -64,60 +71,70 @@ const usePinchGesture = ({ pinchOrigin.y.value = adjustedFocal.y; }) .onChange((evt) => { - const newZoomScale = pinchScaleOffset.value * evt.scale; + const newZoomScale = pinchScale.value * evt.scale; - // limit zoom scale to zoom range and bounce if we go out of range + // Limit zoom scale to zoom range including bounce range if (zoomScale.value >= zoomRange.min * zoomScaleBounceFactors.min && zoomScale.value <= zoomRange.max * zoomScaleBounceFactors.max) { zoomScale.value = newZoomScale; - pinchGestureScale.value = evt.scale; + currentPinchScale.value = evt.scale; } - // calculate new pinch translation + // Calculate new pinch translation const adjustedFocal = getAdjustedFocal(evt.focalX, evt.focalY); - const newPinchTranslateX = adjustedFocal.x + pinchGestureScale.value * pinchOrigin.x.value * -1; - const newPinchTranslateY = adjustedFocal.y + pinchGestureScale.value * pinchOrigin.y.value * -1; + const newPinchTranslateX = adjustedFocal.x + currentPinchScale.value * pinchOrigin.x.value * -1; + const newPinchTranslateY = adjustedFocal.y + currentPinchScale.value * pinchOrigin.y.value * -1; if (zoomScale.value >= zoomRange.min && zoomScale.value <= zoomRange.max) { pinchTranslateX.value = newPinchTranslateX; pinchTranslateY.value = newPinchTranslateY; } else { - // Store x and y translation that is produced while bouncing to separate variables - // so that we can revert the bounce once pinch gesture is released + // Store x and y translation that is produced while bouncing + // so we can revert the bounce once pinch gesture is released pinchBounceTranslateX.value = newPinchTranslateX - pinchTranslateX.value; pinchBounceTranslateY.value = newPinchTranslateY - pinchTranslateY.value; } + + totalPinchTranslateX.value = pinchTranslateX.value + pinchBounceTranslateX.value; + totalPinchTranslateY.value = pinchTranslateY.value + pinchBounceTranslateY.value; }) .onEnd(() => { // Add pinch translation to total offset - totalOffsetX.value += pinchTranslateX.value; - totalOffsetY.value += pinchTranslateY.value; + totalOffsetX.value += totalPinchTranslateX.value; + totalOffsetY.value += totalPinchTranslateX.value; + // Reset pinch gesture variables pinchTranslateX.value = 0; pinchTranslateY.value = 0; - pinchGestureScale.value = 1; + totalPinchTranslateX.value = 0; + totalPinchTranslateY.value = 0; + currentPinchScale.value = 1; isPinchGestureRunning.value = false; + // If the content was "overzoomed" or "underzoomed", we need to bounce back with an animation + if (pinchBounceTranslateX.value !== 0 || pinchBounceTranslateY.value !== 0) { + pinchBounceTranslateX.value = withSpring(0, SPRING_CONFIG); + pinchBounceTranslateY.value = withSpring(0, SPRING_CONFIG); + } + if (zoomScale.value < zoomRange.min) { - pinchScaleOffset.value = zoomRange.min; + // If the zoom scale is less than the minimum zoom scale, we need to set the zoom scale to the minimum + pinchScale.value = zoomRange.min; zoomScale.value = withSpring(zoomRange.min, SPRING_CONFIG); } else if (zoomScale.value > zoomRange.max) { - pinchScaleOffset.value = zoomRange.max; + // If the zoom scale is higher than the maximum zoom scale, we need to set the zoom scale to the maximum + pinchScale.value = zoomRange.max; zoomScale.value = withSpring(zoomRange.max, SPRING_CONFIG); } else { - pinchScaleOffset.value = zoomScale.value; - } - - if (pinchBounceTranslateX.value !== 0 || pinchBounceTranslateY.value !== 0) { - pinchBounceTranslateX.value = withSpring(0, SPRING_CONFIG); - pinchBounceTranslateY.value = withSpring(0, SPRING_CONFIG); + // Otherwise, we just update the pinch scale offset + pinchScale.value = zoomScale.value; } if (onScaleChanged != null) { - runOnJS(onScaleChanged)(pinchScaleOffset.value); + runOnJS(onScaleChanged)(pinchScale.value); } }); - // Triggers "onPinchGestureChange" callback when pinch scale changes + // The "useAnimatedReaction" triggers a state update to run the "onPinchGestureChange" only once per re-render const [isPinchGestureInUse, setIsPinchGestureInUse] = useState(false); useAnimatedReaction( () => [zoomScale.value, isPinchGestureRunning.value], @@ -128,6 +145,7 @@ const usePinchGesture = ({ } }, ); + // eslint-disable-next-line react-hooks/exhaustive-deps useEffect(() => onPinchGestureChange(isPinchGestureInUse), [isPinchGestureInUse]); diff --git a/src/components/MultiGestureCanvas/useTapGestures.js b/src/components/MultiGestureCanvas/useTapGestures.js index 0b354ed6c54a..dbba208801e7 100644 --- a/src/components/MultiGestureCanvas/useTapGestures.js +++ b/src/components/MultiGestureCanvas/useTapGestures.js @@ -18,7 +18,7 @@ const useTapGestures = ({ panGestureRef, totalOffsetX, totalOffsetY, - pinchScaleOffset, + pinchScale, zoomScale, reset, stopAnimation, @@ -28,7 +28,7 @@ const useTapGestures = ({ const scaledWidth = useMemo(() => contentSize.width * minContentScale, [contentSize.width, minContentScale]); const scaledHeight = useMemo(() => contentSize.height * minContentScale, [contentSize.height, minContentScale]); - // On double tap zoom to fill, but at least 3x zoom + // On double tap zoom to fill, but at least zoom by 3x const doubleTapScale = useMemo(() => Math.max(DOUBLE_TAP_SCALE, maxContentScale / minContentScale), [maxContentScale, minContentScale]); const zoomToCoordinates = useWorkletCallback( @@ -87,7 +87,7 @@ const useTapGestures = ({ totalOffsetX.value = withSpring(target.x, SPRING_CONFIG); totalOffsetY.value = withSpring(target.y, SPRING_CONFIG); zoomScale.value = withSpring(doubleTapScale, SPRING_CONFIG); - pinchScaleOffset.value = doubleTapScale; + pinchScale.value = doubleTapScale; }, [scaledWidth, scaledHeight, canvasSize, doubleTapScale], ); From 730a9981d6494598e6c3bc50973f38926cdf8e14 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Wed, 3 Jan 2024 15:19:36 +0100 Subject: [PATCH 110/580] fix: variable names --- src/components/MultiGestureCanvas/usePinchGesture.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/MultiGestureCanvas/usePinchGesture.js b/src/components/MultiGestureCanvas/usePinchGesture.js index 47964aa7bb2e..01737ec0efb0 100644 --- a/src/components/MultiGestureCanvas/usePinchGesture.js +++ b/src/components/MultiGestureCanvas/usePinchGesture.js @@ -17,8 +17,8 @@ const usePinchGesture = ({ zoomRange, totalOffsetX, totalOffsetY, - totalPinchTranslateX, - totalPinchTranslateY, + pinchTranslateX: totalPinchTranslateX, + pinchTranslateY: totalPinchTranslateY, pinchScale, isSwipingInPager, stopAnimation, From 0b205545bf84910dc8538e0c759643d684f61996 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Wed, 3 Jan 2024 15:34:14 +0100 Subject: [PATCH 111/580] fix: calculation of total pinch translation --- .../MultiGestureCanvas/usePinchGesture.js | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/components/MultiGestureCanvas/usePinchGesture.js b/src/components/MultiGestureCanvas/usePinchGesture.js index 01737ec0efb0..3d37f16e789e 100644 --- a/src/components/MultiGestureCanvas/usePinchGesture.js +++ b/src/components/MultiGestureCanvas/usePinchGesture.js @@ -43,6 +43,14 @@ const usePinchGesture = ({ const pinchBounceTranslateX = useSharedValue(0); const pinchBounceTranslateY = useSharedValue(0); + useAnimatedReaction( + () => [pinchTranslateX.value, pinchTranslateY.value, pinchBounceTranslateX.value, pinchBounceTranslateY.value], + ([translateX, translateY, bounceX, bounceY]) => { + totalPinchTranslateX.value = translateX + bounceX; + totalPinchTranslateY.value = translateY + bounceY; + }, + ); + const getAdjustedFocal = useWorkletCallback( (focalX, focalY) => ({ x: focalX - (canvasSize.width / 2 + totalOffsetX.value), @@ -93,9 +101,6 @@ const usePinchGesture = ({ pinchBounceTranslateX.value = newPinchTranslateX - pinchTranslateX.value; pinchBounceTranslateY.value = newPinchTranslateY - pinchTranslateY.value; } - - totalPinchTranslateX.value = pinchTranslateX.value + pinchBounceTranslateX.value; - totalPinchTranslateY.value = pinchTranslateY.value + pinchBounceTranslateY.value; }) .onEnd(() => { // Add pinch translation to total offset @@ -105,8 +110,6 @@ const usePinchGesture = ({ // Reset pinch gesture variables pinchTranslateX.value = 0; pinchTranslateY.value = 0; - totalPinchTranslateX.value = 0; - totalPinchTranslateY.value = 0; currentPinchScale.value = 1; isPinchGestureRunning.value = false; From c353d74c9192bf3993f4d4104cf8f02d51b52f59 Mon Sep 17 00:00:00 2001 From: Kamil Owczarz Date: Wed, 3 Jan 2024 15:51:49 +0100 Subject: [PATCH 112/580] 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 9483b8f79878524bca452ec168ec92a4622f86a6 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Wed, 3 Jan 2024 16:21:46 +0100 Subject: [PATCH 113/580] rename variables --- src/components/MultiGestureCanvas/index.js | 47 +++++++++---------- .../MultiGestureCanvas/usePanGesture.js | 6 +-- .../MultiGestureCanvas/usePinchGesture.js | 18 +++---- .../MultiGestureCanvas/useTapGestures.js | 20 ++------ 4 files changed, 38 insertions(+), 53 deletions(-) diff --git a/src/components/MultiGestureCanvas/index.js b/src/components/MultiGestureCanvas/index.js index ade0a7b54c38..a1969cb5a904 100644 --- a/src/components/MultiGestureCanvas/index.js +++ b/src/components/MultiGestureCanvas/index.js @@ -64,14 +64,13 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr const pinchTranslateX = useSharedValue(0); const pinchTranslateY = useSharedValue(0); - // Total offset of the canvas - // Contains both offsets from panning and pinching gestures - const totalOffsetX = useSharedValue(0); - const totalOffsetY = useSharedValue(0); + // Total offset of the content including previous translations from panning and pinching gestures + const offsetX = useSharedValue(0); + const offsetY = useSharedValue(0); const stopAnimation = useWorkletCallback(() => { - cancelAnimation(totalOffsetX); - cancelAnimation(totalOffsetY); + cancelAnimation(offsetX); + cancelAnimation(offsetY); }); const reset = useWorkletCallback((animated) => { @@ -82,8 +81,8 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr pinchScale.value = 1; if (animated) { - totalOffsetX.value = withSpring(0, SPRING_CONFIG); - totalOffsetY.value = withSpring(0, SPRING_CONFIG); + offsetX.value = withSpring(0, SPRING_CONFIG); + offsetY.value = withSpring(0, SPRING_CONFIG); panTranslateX.value = withSpring(0, SPRING_CONFIG); panTranslateY.value = withSpring(0, SPRING_CONFIG); pinchTranslateX.value = withSpring(0, SPRING_CONFIG); @@ -92,8 +91,8 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr return; } - totalOffsetX.value = 0; - totalOffsetY.value = 0; + offsetX.value = 0; + offsetY.value = 0; panTranslateX.value = 0; panTranslateY.value = 0; pinchTranslateX.value = 0; @@ -101,14 +100,14 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr zoomScale.value = 1; }); - const {singleTap, doubleTap} = useTapGestures({ + const {singleTapGesture, doubleTapGesture} = useTapGestures({ canvasSize, contentSize, minContentScale, maxContentScale, panGestureRef, - totalOffsetX, - totalOffsetY, + offsetX, + offsetY, pinchScale, zoomScale, reset, @@ -120,15 +119,15 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr const panGesture = usePanGesture({ canvasSize, contentSize, + singleTapGesture, + doubleTapGesture, panGestureRef, pagerRef, - singleTap, - doubleTap, zoomScale, zoomRange, totalScale, - totalOffsetX, - totalOffsetY, + offsetX, + offsetY, panTranslateX, panTranslateY, isSwipingInPager, @@ -137,13 +136,13 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr const pinchGesture = usePinchGesture({ canvasSize, - singleTap, - doubleTap, + singleTapGesture, + doubleTapGesture, panGesture, zoomScale, zoomRange, - totalOffsetX, - totalOffsetY, + offsetX, + offsetY, pinchTranslateX, pinchTranslateY, pinchScale, @@ -175,8 +174,8 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr }, [isActive, mounted, reset]); const animatedStyles = useAnimatedStyle(() => { - const x = pinchTranslateX.value + panTranslateX.value + totalOffsetX.value; - const y = pinchTranslateY.value + panTranslateY.value + totalOffsetY.value; + const x = pinchTranslateX.value + panTranslateX.value + offsetX.value; + const y = pinchTranslateY.value + panTranslateY.value + offsetY.value; return { transform: [ @@ -202,7 +201,7 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr }, ]} > - + { stopAnimation(); }) diff --git a/src/components/MultiGestureCanvas/usePinchGesture.js b/src/components/MultiGestureCanvas/usePinchGesture.js index 3d37f16e789e..1756365cec88 100644 --- a/src/components/MultiGestureCanvas/usePinchGesture.js +++ b/src/components/MultiGestureCanvas/usePinchGesture.js @@ -10,13 +10,13 @@ const useWorkletCallback = MultiGestureCanvasUtils.useWorkletCallback; const usePinchGesture = ({ canvasSize, - singleTap, - doubleTap, + singleTapGesture, + doubleTapGesture, panGesture, zoomScale, zoomRange, - totalOffsetX, - totalOffsetY, + offsetX, + offsetY, pinchTranslateX: totalPinchTranslateX, pinchTranslateY: totalPinchTranslateY, pinchScale, @@ -53,8 +53,8 @@ const usePinchGesture = ({ const getAdjustedFocal = useWorkletCallback( (focalX, focalY) => ({ - x: focalX - (canvasSize.width / 2 + totalOffsetX.value), - y: focalY - (canvasSize.height / 2 + totalOffsetY.value), + x: focalX - (canvasSize.width / 2 + offsetX.value), + y: focalY - (canvasSize.height / 2 + offsetY.value), }), [canvasSize.width, canvasSize.height], ); @@ -67,7 +67,7 @@ const usePinchGesture = ({ state.fail(); }) - .simultaneousWithExternalGesture(panGesture, singleTap, doubleTap) + .simultaneousWithExternalGesture(panGesture, singleTapGesture, doubleTapGesture) .onStart((evt) => { isPinchGestureRunning.value = true; @@ -104,8 +104,8 @@ const usePinchGesture = ({ }) .onEnd(() => { // Add pinch translation to total offset - totalOffsetX.value += totalPinchTranslateX.value; - totalOffsetY.value += totalPinchTranslateX.value; + offsetX.value += totalPinchTranslateX.value; + offsetY.value += totalPinchTranslateX.value; // Reset pinch gesture variables pinchTranslateX.value = 0; diff --git a/src/components/MultiGestureCanvas/useTapGestures.js b/src/components/MultiGestureCanvas/useTapGestures.js index dbba208801e7..c2df4392f96c 100644 --- a/src/components/MultiGestureCanvas/useTapGestures.js +++ b/src/components/MultiGestureCanvas/useTapGestures.js @@ -10,21 +10,7 @@ const clamp = MultiGestureCanvasUtils.clamp; const SPRING_CONFIG = MultiGestureCanvasUtils.SPRING_CONFIG; const useWorkletCallback = MultiGestureCanvasUtils.useWorkletCallback; -const useTapGestures = ({ - canvasSize, - contentSize, - minContentScale, - maxContentScale, - panGestureRef, - totalOffsetX, - totalOffsetY, - pinchScale, - zoomScale, - reset, - stopAnimation, - onScaleChanged, - onTap, -}) => { +const useTapGestures = ({canvasSize, contentSize, minContentScale, maxContentScale, panGestureRef, offsetX, offsetY, pinchScale, zoomScale, reset, stopAnimation, onScaleChanged, onTap}) => { const scaledWidth = useMemo(() => contentSize.width * minContentScale, [contentSize.width, minContentScale]); const scaledHeight = useMemo(() => contentSize.height * minContentScale, [contentSize.height, minContentScale]); @@ -84,8 +70,8 @@ const useTapGestures = ({ target.y = 0; } - totalOffsetX.value = withSpring(target.x, SPRING_CONFIG); - totalOffsetY.value = withSpring(target.y, SPRING_CONFIG); + offsetX.value = withSpring(target.x, SPRING_CONFIG); + offsetY.value = withSpring(target.y, SPRING_CONFIG); zoomScale.value = withSpring(doubleTapScale, SPRING_CONFIG); pinchScale.value = doubleTapScale; }, From 91aca42c933efc37225bff9b3cbff28bc86bf93e Mon Sep 17 00:00:00 2001 From: Kamil Owczarz Date: Wed, 3 Jan 2024 16:25:34 +0100 Subject: [PATCH 114/580] 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 115/580] 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 14f8bb545566726b8ad19efe5a5acf037a75eddc Mon Sep 17 00:00:00 2001 From: Pedro Guerreiro Date: Wed, 3 Jan 2024 16:00:18 +0000 Subject: [PATCH 116/580] chore: apply pull request feedback --- src/libs/Navigation/Navigation.ts | 22 +++++++++---------- src/pages/ValidateLoginPage/index.website.tsx | 7 +++++- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/src/libs/Navigation/Navigation.ts b/src/libs/Navigation/Navigation.ts index 2c250b6b89b2..23277fe30636 100644 --- a/src/libs/Navigation/Navigation.ts +++ b/src/libs/Navigation/Navigation.ts @@ -1,18 +1,18 @@ -import { findFocusedRoute, getActionFromState } from '@react-navigation/core'; -import { CommonActions, EventArg, getPathFromState, NavigationContainerEventMap, NavigationState, PartialState, StackActions } from '@react-navigation/native'; +import {findFocusedRoute, getActionFromState} from '@react-navigation/core'; +import {CommonActions, EventArg, getPathFromState, NavigationContainerEventMap, NavigationState, PartialState, StackActions} from '@react-navigation/native'; import findLastIndex from 'lodash/findLastIndex'; import Log from '@libs/Log'; import CONST from '@src/CONST'; import NAVIGATORS from '@src/NAVIGATORS'; -import ROUTES, { Route } from '@src/ROUTES'; -import SCREENS, { PROTECTED_SCREENS } from '@src/SCREENS'; +import ROUTES, {Route} from '@src/ROUTES'; +import SCREENS, {PROTECTED_SCREENS} from '@src/SCREENS'; import getStateFromPath from './getStateFromPath'; import originalGetTopmostReportActionId from './getTopmostReportActionID'; import originalGetTopmostReportId from './getTopmostReportId'; import linkingConfig from './linkingConfig'; import linkTo from './linkTo'; import navigationRef from './navigationRef'; -import { StackNavigationAction, StateOrRoute } from './types'; +import {StackNavigationAction, StateOrRoute} from './types'; let resolveNavigationIsReadyPromise: () => void; const navigationIsReadyPromise = new Promise((resolve) => { @@ -82,7 +82,7 @@ function getDistanceFromPathInRootNavigator(path: string): number { return index; } - currentState = { ...currentState, routes: currentState.routes.slice(0, -1), index: currentState.index - 1 }; + currentState = {...currentState, routes: currentState.routes.slice(0, -1), index: currentState.index - 1}; } return -1; @@ -123,7 +123,7 @@ function isActiveRoute(routePath: Route): boolean { * @param [type] - Type of action to perform. Currently UP is supported. */ function navigate(route: Route = ROUTES.HOME, type?: string) { - if (!canNavigate('navigate', { route })) { + if (!canNavigate('navigate', {route})) { // Store intended route if the navigator is not yet available, // we will try again after the NavigationContainer is ready Log.hmmm(`[Navigation] Container not yet ready, storing route as pending: ${route}`); @@ -228,9 +228,9 @@ function dismissModal(targetReportID?: string) { } else if (targetReportID && rootState.routes.some((route) => route.name === SCREENS.NOT_FOUND)) { const lastRouteIndex = rootState.routes.length - 1; const centralRouteIndex = findLastIndex(rootState.routes, (route) => route.name === NAVIGATORS.CENTRAL_PANE_NAVIGATOR); - navigationRef.current?.dispatch({ ...StackActions.pop(lastRouteIndex - centralRouteIndex), target: rootState.key }); + navigationRef.current?.dispatch({...StackActions.pop(lastRouteIndex - centralRouteIndex), target: rootState.key}); } else { - navigationRef.current?.dispatch({ ...StackActions.pop(), target: rootState.key }); + navigationRef.current?.dispatch({...StackActions.pop(), target: rootState.key}); } break; default: { @@ -315,7 +315,7 @@ function waitForProtectedRoutes() { return; } - const unsubscribe = navigationRef.current?.addListener('state', ({ data }) => { + const unsubscribe = navigationRef.current?.addListener('state', ({data}) => { const state = data?.state; if (navContainsProtectedRoutes(state)) { unsubscribe?.(); @@ -343,4 +343,4 @@ export default { waitForProtectedRoutes, }; -export { navigationRef }; +export {navigationRef}; diff --git a/src/pages/ValidateLoginPage/index.website.tsx b/src/pages/ValidateLoginPage/index.website.tsx index 1a68405934bc..12e680172198 100644 --- a/src/pages/ValidateLoginPage/index.website.tsx +++ b/src/pages/ValidateLoginPage/index.website.tsx @@ -14,8 +14,13 @@ import SCREENS from '@src/SCREENS'; import type {Account, Credentials, Session as SessionType} from '@src/types/onyx'; type ValidateLoginPageOnyxProps = { + /** The details about the account that the user is signing in with */ account: OnyxEntry; + + /** The credentials of the person logging in */ credentials: OnyxEntry; + + /** Session of currently logged in user */ session: OnyxEntry; }; @@ -24,7 +29,7 @@ type ValidateLoginPageProps = ValidateLoginPageOnyxProps & StackScreenProps Date: Wed, 3 Jan 2024 17:22:02 +0100 Subject: [PATCH 117/580] improve comments and remove aliases --- src/components/MultiGestureCanvas/index.js | 25 +++-- .../MultiGestureCanvas/usePanGesture.js | 42 ++++---- .../MultiGestureCanvas/usePinchGesture.js | 19 ++-- .../MultiGestureCanvas/useTapGestures.js | 95 +++++++++++-------- 4 files changed, 92 insertions(+), 89 deletions(-) diff --git a/src/components/MultiGestureCanvas/index.js b/src/components/MultiGestureCanvas/index.js index a1969cb5a904..0964f63913fd 100644 --- a/src/components/MultiGestureCanvas/index.js +++ b/src/components/MultiGestureCanvas/index.js @@ -12,10 +12,6 @@ import usePinchGesture from './usePinchGesture'; import useTapGestures from './useTapGestures'; import * as MultiGestureCanvasUtils from './utils'; -const SPRING_CONFIG = MultiGestureCanvasUtils.SPRING_CONFIG; -const zoomScaleBounceFactors = MultiGestureCanvasUtils.zoomScaleBounceFactors; -const useWorkletCallback = MultiGestureCanvasUtils.useWorkletCallback; - function getDeepDefaultProps({contentSize: contentSizeProp = {}, zoomRange: zoomRangeProp = {}}) { const contentSize = { width: contentSizeProp.width == null ? 1 : contentSizeProp.width, @@ -68,12 +64,12 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr const offsetX = useSharedValue(0); const offsetY = useSharedValue(0); - const stopAnimation = useWorkletCallback(() => { + const stopAnimation = MultiGestureCanvasUtils.useWorkletCallback(() => { cancelAnimation(offsetX); cancelAnimation(offsetY); }); - const reset = useWorkletCallback((animated) => { + const reset = MultiGestureCanvasUtils.useWorkletCallback((animated) => { pinchScale.value = 1; stopAnimation(); @@ -81,13 +77,13 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr pinchScale.value = 1; if (animated) { - offsetX.value = withSpring(0, SPRING_CONFIG); - offsetY.value = withSpring(0, SPRING_CONFIG); - panTranslateX.value = withSpring(0, SPRING_CONFIG); - panTranslateY.value = withSpring(0, SPRING_CONFIG); - pinchTranslateX.value = withSpring(0, SPRING_CONFIG); - pinchTranslateY.value = withSpring(0, SPRING_CONFIG); - zoomScale.value = withSpring(1, SPRING_CONFIG); + offsetX.value = withSpring(0, MultiGestureCanvasUtils.SPRING_CONFIG); + offsetY.value = withSpring(0, MultiGestureCanvasUtils.SPRING_CONFIG); + panTranslateX.value = withSpring(0, MultiGestureCanvasUtils.SPRING_CONFIG); + panTranslateY.value = withSpring(0, MultiGestureCanvasUtils.SPRING_CONFIG); + pinchTranslateX.value = withSpring(0, MultiGestureCanvasUtils.SPRING_CONFIG); + pinchTranslateY.value = withSpring(0, MultiGestureCanvasUtils.SPRING_CONFIG); + zoomScale.value = withSpring(1, MultiGestureCanvasUtils.SPRING_CONFIG); return; } @@ -222,4 +218,5 @@ MultiGestureCanvas.defaultProps = multiGestureCanvasDefaultProps; MultiGestureCanvas.displayName = 'MultiGestureCanvas'; export default MultiGestureCanvas; -export {defaultZoomRange, zoomScaleBounceFactors}; +export {defaultZoomRange}; +export {zoomScaleBounceFactors} from './utils'; diff --git a/src/components/MultiGestureCanvas/usePanGesture.js b/src/components/MultiGestureCanvas/usePanGesture.js index f43b67a0f1f4..f842d1fe4329 100644 --- a/src/components/MultiGestureCanvas/usePanGesture.js +++ b/src/components/MultiGestureCanvas/usePanGesture.js @@ -5,10 +5,6 @@ import * as MultiGestureCanvasUtils from './utils'; const PAN_DECAY_DECELARATION = 0.9915; -const SPRING_CONFIG = MultiGestureCanvasUtils.SPRING_CONFIG; -const clamp = MultiGestureCanvasUtils.clamp; -const useWorkletCallback = MultiGestureCanvasUtils.useWorkletCallback; - const usePanGesture = ({ canvasSize, contentSize, @@ -19,8 +15,8 @@ const usePanGesture = ({ zoomScale, zoomRange, totalScale, - totalOffsetX, - totalOffsetY, + offsetX, + offsetY, panTranslateX, panTranslateY, isSwipingInPager, @@ -40,7 +36,7 @@ const usePanGesture = ({ // Calculates bounds of the scaled content // Can we pan left/right/up/down // Can be used to limit gesture or implementing tension effect - const getBounds = useWorkletCallback(() => { + const getBounds = MultiGestureCanvasUtils.useWorkletCallback(() => { let rightBoundary = 0; let topBoundary = 0; @@ -56,12 +52,12 @@ const usePanGesture = ({ const minVector = {x: -rightBoundary, y: -topBoundary}; const target = { - x: clamp(totalOffsetX.value, minVector.x, maxVector.x), - y: clamp(totalOffsetY.value, minVector.y, maxVector.y), + x: MultiGestureCanvasUtils.clamp(offsetX.value, minVector.x, maxVector.x), + y: MultiGestureCanvasUtils.clamp(offsetY.value, minVector.y, maxVector.y), }; - const isInBoundaryX = target.x === totalOffsetX.value; - const isInBoundaryY = target.y === totalOffsetY.value; + const isInBoundaryX = target.x === offsetX.value; + const isInBoundaryY = target.y === offsetY.value; return { target, @@ -74,24 +70,24 @@ const usePanGesture = ({ }; }, [canvasSize.width, canvasSize.height]); - const returnToBoundaries = useWorkletCallback(() => { + const returnToBoundaries = MultiGestureCanvasUtils.useWorkletCallback(() => { const {target, isInBoundaryX, isInBoundaryY, minVector, maxVector} = getBounds(); - if (zoomScale.value === zoomRange.min && totalOffsetX.value === 0 && totalOffsetY.value === 0 && panTranslateX.value === 0 && panTranslateY.value === 0) { + if (zoomScale.value === zoomRange.min && offsetX.value === 0 && offsetY.value === 0 && panTranslateX.value === 0 && panTranslateY.value === 0) { // We don't need to run any animations return; } // If we are zoomed out, we want to center the content if (zoomScale.value <= zoomRange.min) { - totalOffsetX.value = withSpring(0, SPRING_CONFIG); - totalOffsetY.value = withSpring(0, SPRING_CONFIG); + offsetX.value = withSpring(0, MultiGestureCanvasUtils.SPRING_CONFIG); + offsetY.value = withSpring(0, MultiGestureCanvasUtils.SPRING_CONFIG); return; } if (isInBoundaryX) { if (Math.abs(panVelocityX.value) > 0 && zoomScale.value <= zoomRange.max) { - totalOffsetX.value = withDecay({ + offsetX.value = withDecay({ velocity: panVelocityX.value, clamp: [minVector.x, maxVector.x], deceleration: PAN_DECAY_DECELARATION, @@ -99,7 +95,7 @@ const usePanGesture = ({ }); } } else { - totalOffsetX.value = withSpring(target.x, SPRING_CONFIG); + offsetX.value = withSpring(target.x, MultiGestureCanvasUtils.SPRING_CONFIG); } if (isInBoundaryY) { @@ -107,17 +103,17 @@ const usePanGesture = ({ Math.abs(panVelocityY.value) > 0 && zoomScale.value <= zoomRange.max && // Limit vertical panning when content is smaller than screen - totalOffsetY.value !== minVector.y && - totalOffsetY.value !== maxVector.y + offsetY.value !== minVector.y && + offsetY.value !== maxVector.y ) { - totalOffsetY.value = withDecay({ + offsetY.value = withDecay({ velocity: panVelocityY.value, clamp: [minVector.y, maxVector.y], deceleration: PAN_DECAY_DECELARATION, }); } } else { - totalOffsetY.value = withSpring(target.y, SPRING_CONFIG); + offsetY.value = withSpring(target.y, MultiGestureCanvasUtils.SPRING_CONFIG); } }); @@ -157,8 +153,8 @@ const usePanGesture = ({ }) .onEnd(() => { // Add pan translation to total offset - totalOffsetX.value += panTranslateX.value; - totalOffsetY.value += panTranslateY.value; + offsetX.value += panTranslateX.value; + offsetY.value += panTranslateY.value; // Reset pan gesture variables panTranslateX.value = 0; diff --git a/src/components/MultiGestureCanvas/usePinchGesture.js b/src/components/MultiGestureCanvas/usePinchGesture.js index 1756365cec88..3f79f8aedbf3 100644 --- a/src/components/MultiGestureCanvas/usePinchGesture.js +++ b/src/components/MultiGestureCanvas/usePinchGesture.js @@ -4,10 +4,6 @@ import {Gesture} from 'react-native-gesture-handler'; import {runOnJS, useAnimatedReaction, useSharedValue, withSpring} from 'react-native-reanimated'; import * as MultiGestureCanvasUtils from './utils'; -const SPRING_CONFIG = MultiGestureCanvasUtils.SPRING_CONFIG; -const zoomScaleBounceFactors = MultiGestureCanvasUtils.zoomScaleBounceFactors; -const useWorkletCallback = MultiGestureCanvasUtils.useWorkletCallback; - const usePinchGesture = ({ canvasSize, singleTapGesture, @@ -51,7 +47,7 @@ const usePinchGesture = ({ }, ); - const getAdjustedFocal = useWorkletCallback( + const getAdjustedFocal = MultiGestureCanvasUtils.useWorkletCallback( (focalX, focalY) => ({ x: focalX - (canvasSize.width / 2 + offsetX.value), y: focalY - (canvasSize.height / 2 + offsetY.value), @@ -82,7 +78,10 @@ const usePinchGesture = ({ const newZoomScale = pinchScale.value * evt.scale; // Limit zoom scale to zoom range including bounce range - if (zoomScale.value >= zoomRange.min * zoomScaleBounceFactors.min && zoomScale.value <= zoomRange.max * zoomScaleBounceFactors.max) { + if ( + zoomScale.value >= zoomRange.min * MultiGestureCanvasUtils.zoomScaleBounceFactors.min && + zoomScale.value <= zoomRange.max * MultiGestureCanvasUtils.zoomScaleBounceFactors.max + ) { zoomScale.value = newZoomScale; currentPinchScale.value = evt.scale; } @@ -115,18 +114,18 @@ const usePinchGesture = ({ // If the content was "overzoomed" or "underzoomed", we need to bounce back with an animation if (pinchBounceTranslateX.value !== 0 || pinchBounceTranslateY.value !== 0) { - pinchBounceTranslateX.value = withSpring(0, SPRING_CONFIG); - pinchBounceTranslateY.value = withSpring(0, SPRING_CONFIG); + pinchBounceTranslateX.value = withSpring(0, MultiGestureCanvasUtils.SPRING_CONFIG); + pinchBounceTranslateY.value = withSpring(0, MultiGestureCanvasUtils.SPRING_CONFIG); } if (zoomScale.value < zoomRange.min) { // If the zoom scale is less than the minimum zoom scale, we need to set the zoom scale to the minimum pinchScale.value = zoomRange.min; - zoomScale.value = withSpring(zoomRange.min, SPRING_CONFIG); + zoomScale.value = withSpring(zoomRange.min, MultiGestureCanvasUtils.SPRING_CONFIG); } else if (zoomScale.value > zoomRange.max) { // If the zoom scale is higher than the maximum zoom scale, we need to set the zoom scale to the maximum pinchScale.value = zoomRange.max; - zoomScale.value = withSpring(zoomRange.max, SPRING_CONFIG); + zoomScale.value = withSpring(zoomRange.max, MultiGestureCanvasUtils.SPRING_CONFIG); } else { // Otherwise, we just update the pinch scale offset pinchScale.value = zoomScale.value; diff --git a/src/components/MultiGestureCanvas/useTapGestures.js b/src/components/MultiGestureCanvas/useTapGestures.js index c2df4392f96c..3b64b02e56b5 100644 --- a/src/components/MultiGestureCanvas/useTapGestures.js +++ b/src/components/MultiGestureCanvas/useTapGestures.js @@ -6,83 +6,94 @@ import * as MultiGestureCanvasUtils from './utils'; const DOUBLE_TAP_SCALE = 3; -const clamp = MultiGestureCanvasUtils.clamp; -const SPRING_CONFIG = MultiGestureCanvasUtils.SPRING_CONFIG; -const useWorkletCallback = MultiGestureCanvasUtils.useWorkletCallback; - const useTapGestures = ({canvasSize, contentSize, minContentScale, maxContentScale, panGestureRef, offsetX, offsetY, pinchScale, zoomScale, reset, stopAnimation, onScaleChanged, onTap}) => { - const scaledWidth = useMemo(() => contentSize.width * minContentScale, [contentSize.width, minContentScale]); - const scaledHeight = useMemo(() => contentSize.height * minContentScale, [contentSize.height, minContentScale]); + // The content size after scaling it with minimum scale to fit the content into the canvas + const scaledContentWidth = useMemo(() => contentSize.width * minContentScale, [contentSize.width, minContentScale]); + const scaledContentHeight = useMemo(() => contentSize.height * minContentScale, [contentSize.height, minContentScale]); - // On double tap zoom to fill, but at least zoom by 3x + // On double tap the content should be zoomed to fill, but at least zoomed by DOUBLE_TAP_SCALE const doubleTapScale = useMemo(() => Math.max(DOUBLE_TAP_SCALE, maxContentScale / minContentScale), [maxContentScale, minContentScale]); - const zoomToCoordinates = useWorkletCallback( - (canvasFocalX, canvasFocalY) => { + const zoomToCoordinates = MultiGestureCanvasUtils.useWorkletCallback( + (focalX, focalY) => { 'worklet'; stopAnimation(); - const canvasOffsetX = Math.max(0, (canvasSize.width - scaledWidth) / 2); - const canvasOffsetY = Math.max(0, (canvasSize.height - scaledHeight) / 2); + // By how much the canvas is bigger than the content horizontally and vertically per side + const horizontalCanvasOffset = Math.max(0, (canvasSize.width - scaledContentWidth) / 2); + const verticalCanvasOffset = Math.max(0, (canvasSize.height - scaledContentHeight) / 2); - const contentFocal = { - x: clamp(canvasFocalX - canvasOffsetX, 0, scaledWidth), - y: clamp(canvasFocalY - canvasOffsetY, 0, scaledHeight), + // We need to adjust the focal point to take into account the canvas offset + // The focal point cannot be outside of the content's bounds + const adjustedFocalPoint = { + x: MultiGestureCanvasUtils.clamp(focalX - horizontalCanvasOffset, 0, scaledContentWidth), + y: MultiGestureCanvasUtils.clamp(focalY - verticalCanvasOffset, 0, scaledContentHeight), }; + // The center of the canvas const canvasCenter = { x: canvasSize.width / 2, y: canvasSize.height / 2, }; - const originContentCenter = { - x: scaledWidth / 2, - y: scaledHeight / 2, + // The center of the content before zooming + const originalContentCenter = { + x: scaledContentWidth / 2, + y: scaledContentHeight / 2, }; - const targetContentSize = { - width: scaledWidth * doubleTapScale, - height: scaledHeight * doubleTapScale, + // The size of the content after zooming + const zoomedContentSize = { + width: scaledContentWidth * doubleTapScale, + height: scaledContentHeight * doubleTapScale, }; - const targetContentCenter = { - x: targetContentSize.width / 2, - y: targetContentSize.height / 2, + // The center of the zoomed content + const zoomedContentCenter = { + x: zoomedContentSize.width / 2, + y: zoomedContentSize.height / 2, }; - const currentOrigin = { - x: (targetContentCenter.x - canvasCenter.x) * -1, - y: (targetContentCenter.y - canvasCenter.y) * -1, + // By how much the zoomed content is bigger/smaller than the canvas. + const zoomedContentOffset = { + x: zoomedContentCenter.x - canvasCenter.x, + y: zoomedContentCenter.y - canvasCenter.y, }; - const koef = { - x: (1 / originContentCenter.x) * contentFocal.x - 1, - y: (1 / originContentCenter.y) * contentFocal.y - 1, + // How much the content needs to be shifted based on the focal point + const shiftingFactor = { + x: adjustedFocalPoint.x / originalContentCenter.x - 1, + y: adjustedFocalPoint.y / originalContentCenter.y - 1, }; - const target = { - x: currentOrigin.x * koef.x, - y: currentOrigin.y * koef.y, + // The offset after applying the focal point adjusted shift. + // We need to invert the shift, because the content is moving in the opposite direction (* -1) + const offsetAfterZooming = { + x: zoomedContentOffset.x * (shiftingFactor.x * -1), + y: zoomedContentOffset.y * (shiftingFactor.y * -1), }; - if (targetContentSize.height < canvasSize.height) { - target.y = 0; + // If the zoomed content is less tall than the canvas, we need to reset the vertical offset + if (zoomedContentSize.height < canvasSize.height) { + offsetAfterZooming.y = 0; } - offsetX.value = withSpring(target.x, SPRING_CONFIG); - offsetY.value = withSpring(target.y, SPRING_CONFIG); - zoomScale.value = withSpring(doubleTapScale, SPRING_CONFIG); + offsetX.value = withSpring(offsetAfterZooming.x, MultiGestureCanvasUtils.SPRING_CONFIG); + offsetY.value = withSpring(offsetAfterZooming.y, MultiGestureCanvasUtils.SPRING_CONFIG); + zoomScale.value = withSpring(doubleTapScale, MultiGestureCanvasUtils.SPRING_CONFIG); pinchScale.value = doubleTapScale; }, - [scaledWidth, scaledHeight, canvasSize, doubleTapScale], + [scaledContentWidth, scaledContentHeight, canvasSize, doubleTapScale], ); - const doubleTap = Gesture.Tap() + const doubleTapGesture = Gesture.Tap() .numberOfTaps(2) .maxDelay(150) .maxDistance(20) .onEnd((evt) => { + // If the content is already zoomed, we want to reset the zoom, + // otherwwise we want to zoom in if (zoomScale.value > 1) { reset(true); } else { @@ -94,10 +105,10 @@ const useTapGestures = ({canvasSize, contentSize, minContentScale, maxContentSca } }); - const singleTap = Gesture.Tap() + const singleTapGesture = Gesture.Tap() .numberOfTaps(1) .maxDuration(50) - .requireExternalGestureToFail(doubleTap, panGestureRef) + .requireExternalGestureToFail(doubleTapGesture, panGestureRef) .onBegin(() => { stopAnimation(); }) @@ -109,7 +120,7 @@ const useTapGestures = ({canvasSize, contentSize, minContentScale, maxContentSca runOnJS(onTap)(); }); - return {singleTap, doubleTap}; + return {singleTapGesture, doubleTapGesture}; }; export default useTapGestures; From ba75ac922cde2585ecbf3c6037c7c94d34045ec3 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Wed, 3 Jan 2024 17:37:38 +0100 Subject: [PATCH 118/580] improve pan gesture code --- .../MultiGestureCanvas/usePanGesture.js | 84 ++++++++----------- 1 file changed, 35 insertions(+), 49 deletions(-) diff --git a/src/components/MultiGestureCanvas/usePanGesture.js b/src/components/MultiGestureCanvas/usePanGesture.js index f842d1fe4329..657b51661145 100644 --- a/src/components/MultiGestureCanvas/usePanGesture.js +++ b/src/components/MultiGestureCanvas/usePanGesture.js @@ -22,12 +22,9 @@ const usePanGesture = ({ isSwipingInPager, stopAnimation, }) => { - // The content size after scaling it with the current (total) zoom value - const zoomScaledContentWidth = useDerivedValue(() => contentSize.width * totalScale.value, [contentSize.width]); - const zoomScaledContentHeight = useDerivedValue(() => contentSize.height * totalScale.value, [contentSize.height]); - - // Used to track previous touch position for the "swipe down to close" gesture - const previousTouch = useSharedValue(null); + // The content size after fitting it to the canvas and zooming + const zoomedContentWidth = useDerivedValue(() => contentSize.width * totalScale.value, [contentSize.width]); + const zoomedContentHeight = useDerivedValue(() => contentSize.height * totalScale.value, [contentSize.height]); // Pan velocity to calculate the decay const panVelocityX = useSharedValue(0); @@ -40,62 +37,57 @@ const usePanGesture = ({ let rightBoundary = 0; let topBoundary = 0; - if (canvasSize.width < zoomScaledContentWidth.value) { - rightBoundary = Math.abs(canvasSize.width - zoomScaledContentWidth.value) / 2; + if (canvasSize.width < zoomedContentWidth.value) { + rightBoundary = Math.abs(canvasSize.width - zoomedContentWidth.value) / 2; } - if (canvasSize.height < zoomScaledContentHeight.value) { - topBoundary = Math.abs(zoomScaledContentHeight.value - canvasSize.height) / 2; + if (canvasSize.height < zoomedContentHeight.value) { + topBoundary = Math.abs(zoomedContentHeight.value - canvasSize.height) / 2; } - const maxVector = {x: rightBoundary, y: topBoundary}; - const minVector = {x: -rightBoundary, y: -topBoundary}; + const minBoundaries = {x: -rightBoundary, y: -topBoundary}; + const maxBoundaries = {x: rightBoundary, y: topBoundary}; - const target = { - x: MultiGestureCanvasUtils.clamp(offsetX.value, minVector.x, maxVector.x), - y: MultiGestureCanvasUtils.clamp(offsetY.value, minVector.y, maxVector.y), + const clampedOffset = { + x: MultiGestureCanvasUtils.clamp(offsetX.value, minBoundaries.x, maxBoundaries.x), + y: MultiGestureCanvasUtils.clamp(offsetY.value, minBoundaries.y, maxBoundaries.y), }; - const isInBoundaryX = target.x === offsetX.value; - const isInBoundaryY = target.y === offsetY.value; + // If the horizontal/vertical offset is the same after clamping to the min/max boundaries, the content is within the boundaries + const isInBoundaryX = clampedOffset.x === offsetX.value; + const isInBoundaryY = clampedOffset.y === offsetY.value; return { - target, + minBoundaries, + maxBoundaries, + clampedOffset, isInBoundaryX, isInBoundaryY, - minVector, - maxVector, - canPanLeft: target.x < maxVector.x, - canPanRight: target.x > minVector.x, }; }, [canvasSize.width, canvasSize.height]); - const returnToBoundaries = MultiGestureCanvasUtils.useWorkletCallback(() => { - const {target, isInBoundaryX, isInBoundaryY, minVector, maxVector} = getBounds(); - - if (zoomScale.value === zoomRange.min && offsetX.value === 0 && offsetY.value === 0 && panTranslateX.value === 0 && panTranslateY.value === 0) { - // We don't need to run any animations + // We want to smoothly gesture by phasing out the pan animation + // In case the content is outside of the boundaries of the canvas, + // we need to return to the view to the boundaries + const finishPanGesture = MultiGestureCanvasUtils.useWorkletCallback(() => { + // If the content is centered within the canvas, we don't need to run any animations + if (offsetX.value === 0 && offsetY.value === 0 && panTranslateX.value === 0 && panTranslateY.value === 0) { return; } - // If we are zoomed out, we want to center the content - if (zoomScale.value <= zoomRange.min) { - offsetX.value = withSpring(0, MultiGestureCanvasUtils.SPRING_CONFIG); - offsetY.value = withSpring(0, MultiGestureCanvasUtils.SPRING_CONFIG); - return; - } + const {clampedOffset, isInBoundaryX, isInBoundaryY, minBoundaries, maxBoundaries} = getBounds(); if (isInBoundaryX) { if (Math.abs(panVelocityX.value) > 0 && zoomScale.value <= zoomRange.max) { offsetX.value = withDecay({ velocity: panVelocityX.value, - clamp: [minVector.x, maxVector.x], + clamp: [minBoundaries.x, maxBoundaries.x], deceleration: PAN_DECAY_DECELARATION, rubberBandEffect: false, }); } } else { - offsetX.value = withSpring(target.x, MultiGestureCanvasUtils.SPRING_CONFIG); + offsetX.value = withSpring(clampedOffset.x, MultiGestureCanvasUtils.SPRING_CONFIG); } if (isInBoundaryY) { @@ -103,17 +95,17 @@ const usePanGesture = ({ Math.abs(panVelocityY.value) > 0 && zoomScale.value <= zoomRange.max && // Limit vertical panning when content is smaller than screen - offsetY.value !== minVector.y && - offsetY.value !== maxVector.y + offsetY.value !== minBoundaries.y && + offsetY.value !== maxBoundaries.y ) { offsetY.value = withDecay({ velocity: panVelocityY.value, - clamp: [minVector.y, maxVector.y], + clamp: [minBoundaries.y, maxBoundaries.y], deceleration: PAN_DECAY_DECELARATION, }); } } else { - offsetY.value = withSpring(target.y, MultiGestureCanvasUtils.SPRING_CONFIG); + offsetY.value = withSpring(clampedOffset.y, MultiGestureCanvasUtils.SPRING_CONFIG); } }); @@ -121,16 +113,11 @@ const usePanGesture = ({ .manualActivation(true) .averageTouches(true) .onTouchesMove((evt, state) => { - if (zoomScale.value > 1) { - state.activate(); + if (zoomScale.value <= 1) { + return; } - if (previousTouch.value == null) { - previousTouch.value = { - x: evt.allTouches[0].x, - y: evt.allTouches[0].y, - }; - } + state.activate(); }) .simultaneousWithExternalGesture(pagerRef, singleTapGesture, doubleTapGesture) .onStart(() => { @@ -159,14 +146,13 @@ const usePanGesture = ({ // Reset pan gesture variables panTranslateX.value = 0; panTranslateY.value = 0; - previousTouch.value = null; // If we are swiping (in the pager), we don't want to return to boundaries if (isSwipingInPager.value) { return; } - returnToBoundaries(); + finishPanGesture(); // Reset pan gesture variables panVelocityX.value = 0; From 1de9b257d77ee2ddc6a34674ce5929f4e27e7550 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Wed, 3 Jan 2024 17:38:24 +0100 Subject: [PATCH 119/580] fix: pinch gesture --- .../MultiGestureCanvas/usePinchGesture.js | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/components/MultiGestureCanvas/usePinchGesture.js b/src/components/MultiGestureCanvas/usePinchGesture.js index 3f79f8aedbf3..ba6d71e87df5 100644 --- a/src/components/MultiGestureCanvas/usePinchGesture.js +++ b/src/components/MultiGestureCanvas/usePinchGesture.js @@ -54,7 +54,17 @@ const usePinchGesture = ({ }), [canvasSize.width, canvasSize.height], ); + + const [pinchEnabled, setPinchEnabled] = useState(true); + useEffect(() => { + if (pinchEnabled) { + return; + } + setPinchEnabled(true); + }, [pinchEnabled]); + const pinchGesture = Gesture.Pinch() + .enabled(pinchEnabled) .onTouchesDown((evt, state) => { // We don't want to activate pinch gesture when we are scrolling pager if (!isSwipingInPager.value) { @@ -75,6 +85,11 @@ const usePinchGesture = ({ pinchOrigin.y.value = adjustedFocal.y; }) .onChange((evt) => { + if (evt.numberOfPointers !== 2) { + runOnJS(setPinchEnabled)(false); + return; + } + const newZoomScale = pinchScale.value * evt.scale; // Limit zoom scale to zoom range including bounce range @@ -103,8 +118,8 @@ const usePinchGesture = ({ }) .onEnd(() => { // Add pinch translation to total offset - offsetX.value += totalPinchTranslateX.value; - offsetY.value += totalPinchTranslateX.value; + offsetX.value += pinchTranslateX.value; + offsetY.value += pinchTranslateY.value; // Reset pinch gesture variables pinchTranslateX.value = 0; From f32eb83736b3165042601f8c008736245d707865 Mon Sep 17 00:00:00 2001 From: Kamil Owczarz Date: Wed, 3 Jan 2024 18:05:52 +0100 Subject: [PATCH 120/580] 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 3d93b5e6fcf849e32f2ad18b64218001386f690b Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Wed, 3 Jan 2024 18:06:58 +0100 Subject: [PATCH 121/580] improve pan gesture code and remove inter-depdendencies of gestures --- src/components/MultiGestureCanvas/index.js | 17 ++-- .../MultiGestureCanvas/usePanGesture.js | 81 ++++++++----------- .../MultiGestureCanvas/usePinchGesture.js | 4 - .../MultiGestureCanvas/useTapGestures.js | 3 +- src/components/MultiGestureCanvas/utils.ts | 19 +++++ 5 files changed, 59 insertions(+), 65 deletions(-) diff --git a/src/components/MultiGestureCanvas/index.js b/src/components/MultiGestureCanvas/index.js index 0964f63913fd..0577ec79d0d6 100644 --- a/src/components/MultiGestureCanvas/index.js +++ b/src/components/MultiGestureCanvas/index.js @@ -96,7 +96,7 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr zoomScale.value = 1; }); - const {singleTapGesture, doubleTapGesture} = useTapGestures({ + const {singleTapGesture: basicSingleTapGesture, doubleTapGesture} = useTapGestures({ canvasSize, contentSize, minContentScale, @@ -111,16 +111,12 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr onScaleChanged, onTap, }); + const singleTapGesture = basicSingleTapGesture.requireExternalGestureToFail(doubleTapGesture, panGestureRef); const panGesture = usePanGesture({ canvasSize, contentSize, - singleTapGesture, - doubleTapGesture, - panGestureRef, - pagerRef, zoomScale, - zoomRange, totalScale, offsetX, offsetY, @@ -128,13 +124,12 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr panTranslateY, isSwipingInPager, stopAnimation, - }); + }) + .simultaneousWithExternalGesture(pagerRef, singleTapGesture, doubleTapGesture) + .withRef(panGestureRef); const pinchGesture = usePinchGesture({ canvasSize, - singleTapGesture, - doubleTapGesture, - panGesture, zoomScale, zoomRange, offsetX, @@ -146,7 +141,7 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr stopAnimation, onScaleChanged, onPinchGestureChange, - }); + }).simultaneousWithExternalGesture(panGesture, singleTapGesture, doubleTapGesture); // Enables/disables the pager scroll based on the zoom scale // When the content is zoomed in/out, the pager should be disabled diff --git a/src/components/MultiGestureCanvas/usePanGesture.js b/src/components/MultiGestureCanvas/usePanGesture.js index 657b51661145..07498e770aa5 100644 --- a/src/components/MultiGestureCanvas/usePanGesture.js +++ b/src/components/MultiGestureCanvas/usePanGesture.js @@ -5,23 +5,7 @@ import * as MultiGestureCanvasUtils from './utils'; const PAN_DECAY_DECELARATION = 0.9915; -const usePanGesture = ({ - canvasSize, - contentSize, - singleTapGesture, - doubleTapGesture, - panGestureRef, - pagerRef, - zoomScale, - zoomRange, - totalScale, - offsetX, - offsetY, - panTranslateX, - panTranslateY, - isSwipingInPager, - stopAnimation, -}) => { +const usePanGesture = ({canvasSize, contentSize, zoomScale, totalScale, offsetX, offsetY, panTranslateX, panTranslateY, isSwipingInPager, stopAnimation}) => { // The content size after fitting it to the canvas and zooming const zoomedContentWidth = useDerivedValue(() => contentSize.width * totalScale.value, [contentSize.width]); const zoomedContentHeight = useDerivedValue(() => contentSize.height * totalScale.value, [contentSize.height]); @@ -34,35 +18,35 @@ const usePanGesture = ({ // Can we pan left/right/up/down // Can be used to limit gesture or implementing tension effect const getBounds = MultiGestureCanvasUtils.useWorkletCallback(() => { - let rightBoundary = 0; - let topBoundary = 0; + let horizontalBoundary = 0; + let verticalBoundary = 0; if (canvasSize.width < zoomedContentWidth.value) { - rightBoundary = Math.abs(canvasSize.width - zoomedContentWidth.value) / 2; + horizontalBoundary = Math.abs(canvasSize.width - zoomedContentWidth.value) / 2; } if (canvasSize.height < zoomedContentHeight.value) { - topBoundary = Math.abs(zoomedContentHeight.value - canvasSize.height) / 2; + verticalBoundary = Math.abs(zoomedContentHeight.value - canvasSize.height) / 2; } - const minBoundaries = {x: -rightBoundary, y: -topBoundary}; - const maxBoundaries = {x: rightBoundary, y: topBoundary}; + const horizontalBoundaries = {min: -horizontalBoundary, max: horizontalBoundary}; + const verticalBoundaries = {min: -verticalBoundary, max: verticalBoundary}; const clampedOffset = { - x: MultiGestureCanvasUtils.clamp(offsetX.value, minBoundaries.x, maxBoundaries.x), - y: MultiGestureCanvasUtils.clamp(offsetY.value, minBoundaries.y, maxBoundaries.y), + x: MultiGestureCanvasUtils.clamp(offsetX.value, horizontalBoundaries.min, horizontalBoundaries.max), + y: MultiGestureCanvasUtils.clamp(offsetY.value, verticalBoundaries.min, verticalBoundaries.max), }; // If the horizontal/vertical offset is the same after clamping to the min/max boundaries, the content is within the boundaries - const isInBoundaryX = clampedOffset.x === offsetX.value; - const isInBoundaryY = clampedOffset.y === offsetY.value; + const isInHoriztontalBoundary = clampedOffset.x === offsetX.value; + const isInVerticalBoundary = clampedOffset.y === offsetY.value; return { - minBoundaries, - maxBoundaries, + horizontalBoundaries, + verticalBoundaries, clampedOffset, - isInBoundaryX, - isInBoundaryY, + isInHoriztontalBoundary, + isInVerticalBoundary, }; }, [canvasSize.width, canvasSize.height]); @@ -75,36 +59,38 @@ const usePanGesture = ({ return; } - const {clampedOffset, isInBoundaryX, isInBoundaryY, minBoundaries, maxBoundaries} = getBounds(); + const {clampedOffset, isInHoriztontalBoundary, isInVerticalBoundary, horizontalBoundaries, verticalBoundaries} = getBounds(); - if (isInBoundaryX) { - if (Math.abs(panVelocityX.value) > 0 && zoomScale.value <= zoomRange.max) { + // If the content is within the horizontal/vertical boundaries of the canvas, we can smoothly phase out the animation + // If not, we need to snap back to the boundaries + if (isInHoriztontalBoundary) { + // If the (absolute) velocity is 0, we don't need to run an animation + if (Math.abs(panVelocityX.value) !== 0) { + // Phase out the pan animation offsetX.value = withDecay({ velocity: panVelocityX.value, - clamp: [minBoundaries.x, maxBoundaries.x], + clamp: [horizontalBoundaries.min, horizontalBoundaries.max], deceleration: PAN_DECAY_DECELARATION, rubberBandEffect: false, }); } } else { + // Animated back to the boundary offsetX.value = withSpring(clampedOffset.x, MultiGestureCanvasUtils.SPRING_CONFIG); } - if (isInBoundaryY) { - if ( - Math.abs(panVelocityY.value) > 0 && - zoomScale.value <= zoomRange.max && - // Limit vertical panning when content is smaller than screen - offsetY.value !== minBoundaries.y && - offsetY.value !== maxBoundaries.y - ) { + if (isInVerticalBoundary) { + // If the (absolute) velocity is 0, we don't need to run an animation + if (Math.abs(panVelocityY.value) !== 0) { + // Phase out the pan animation offsetY.value = withDecay({ velocity: panVelocityY.value, - clamp: [minBoundaries.y, maxBoundaries.y], + clamp: [verticalBoundaries.min, verticalBoundaries.max], deceleration: PAN_DECAY_DECELARATION, }); } } else { + // Animated back to the boundary offsetY.value = withSpring(clampedOffset.y, MultiGestureCanvasUtils.SPRING_CONFIG); } }); @@ -112,14 +98,14 @@ const usePanGesture = ({ const panGesture = Gesture.Pan() .manualActivation(true) .averageTouches(true) - .onTouchesMove((evt, state) => { + .onTouchesMove((_evt, state) => { + // We only allow panning when the content is zoomed in if (zoomScale.value <= 1) { return; } state.activate(); }) - .simultaneousWithExternalGesture(pagerRef, singleTapGesture, doubleTapGesture) .onStart(() => { stopAnimation(); }) @@ -157,8 +143,7 @@ const usePanGesture = ({ // Reset pan gesture variables panVelocityX.value = 0; panVelocityY.value = 0; - }) - .withRef(panGestureRef); + }); return panGesture; }; diff --git a/src/components/MultiGestureCanvas/usePinchGesture.js b/src/components/MultiGestureCanvas/usePinchGesture.js index ba6d71e87df5..f40c58955c32 100644 --- a/src/components/MultiGestureCanvas/usePinchGesture.js +++ b/src/components/MultiGestureCanvas/usePinchGesture.js @@ -6,9 +6,6 @@ import * as MultiGestureCanvasUtils from './utils'; const usePinchGesture = ({ canvasSize, - singleTapGesture, - doubleTapGesture, - panGesture, zoomScale, zoomRange, offsetX, @@ -73,7 +70,6 @@ const usePinchGesture = ({ state.fail(); }) - .simultaneousWithExternalGesture(panGesture, singleTapGesture, doubleTapGesture) .onStart((evt) => { isPinchGestureRunning.value = true; diff --git a/src/components/MultiGestureCanvas/useTapGestures.js b/src/components/MultiGestureCanvas/useTapGestures.js index 3b64b02e56b5..eefe8c506b33 100644 --- a/src/components/MultiGestureCanvas/useTapGestures.js +++ b/src/components/MultiGestureCanvas/useTapGestures.js @@ -6,7 +6,7 @@ import * as MultiGestureCanvasUtils from './utils'; const DOUBLE_TAP_SCALE = 3; -const useTapGestures = ({canvasSize, contentSize, minContentScale, maxContentScale, panGestureRef, offsetX, offsetY, pinchScale, zoomScale, reset, stopAnimation, onScaleChanged, onTap}) => { +const useTapGestures = ({canvasSize, contentSize, minContentScale, maxContentScale, offsetX, offsetY, pinchScale, zoomScale, reset, stopAnimation, onScaleChanged, onTap}) => { // The content size after scaling it with minimum scale to fit the content into the canvas const scaledContentWidth = useMemo(() => contentSize.width * minContentScale, [contentSize.width, minContentScale]); const scaledContentHeight = useMemo(() => contentSize.height * minContentScale, [contentSize.height, minContentScale]); @@ -108,7 +108,6 @@ const useTapGestures = ({canvasSize, contentSize, minContentScale, maxContentSca const singleTapGesture = Gesture.Tap() .numberOfTaps(1) .maxDuration(50) - .requireExternalGestureToFail(doubleTapGesture, panGestureRef) .onBegin(() => { stopAnimation(); }) diff --git a/src/components/MultiGestureCanvas/utils.ts b/src/components/MultiGestureCanvas/utils.ts index 7a4ba21358c4..5cddd009117a 100644 --- a/src/components/MultiGestureCanvas/utils.ts +++ b/src/components/MultiGestureCanvas/utils.ts @@ -1,22 +1,41 @@ import {useCallback} from 'react'; +// The spring config is used to determine the physics of the spring animation +// Details and a playground for testing different configs can be found at +// https://docs.swmansion.com/react-native-reanimated/docs/animations/withSpring const SPRING_CONFIG = { mass: 1, stiffness: 1000, damping: 500, }; +// The zoom scale bounce factors are used to determine the amount of bounce +// that is allowed when the user zooms more than the min or max zoom levels const zoomScaleBounceFactors = { min: 0.7, max: 1.5, }; +/** + * Clamps a value between a lower and upper bound + * @param value + * @param lowerBound + * @param upperBound + * @returns + */ function clamp(value: number, lowerBound: number, upperBound: number) { 'worklet'; return Math.min(Math.max(lowerBound, value), upperBound); } +/** + * Creates a memoized callback on the UI thread + * Same as `useWorkletCallback` from `react-native-reanimated` but without the deprecation warning + * @param callback + * @param deps + * @returns + */ const useWorkletCallback = (callback: Parameters[0], deps: Parameters[1] = []) => { 'worklet'; From 078b5779c06b770c13c6e6c84adc03ac4f10f863 Mon Sep 17 00:00:00 2001 From: Kamil Owczarz Date: Wed, 3 Jan 2024 18:08:44 +0100 Subject: [PATCH 122/580] 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 f0c542bef393ad1a612fbbe0eac4d66481246f51 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Wed, 3 Jan 2024 18:11:16 +0100 Subject: [PATCH 123/580] improve pan gesture code --- .../MultiGestureCanvas/usePanGesture.js | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/src/components/MultiGestureCanvas/usePanGesture.js b/src/components/MultiGestureCanvas/usePanGesture.js index 07498e770aa5..8ab2078466de 100644 --- a/src/components/MultiGestureCanvas/usePanGesture.js +++ b/src/components/MultiGestureCanvas/usePanGesture.js @@ -93,6 +93,10 @@ const usePanGesture = ({canvasSize, contentSize, zoomScale, totalScale, offsetX, // Animated back to the boundary offsetY.value = withSpring(clampedOffset.y, MultiGestureCanvasUtils.SPRING_CONFIG); } + + // Reset velocity variables after we finished the pan gesture + panVelocityX.value = 0; + panVelocityY.value = 0; }); const panGesture = Gesture.Pan() @@ -100,7 +104,7 @@ const usePanGesture = ({canvasSize, contentSize, zoomScale, totalScale, offsetX, .averageTouches(true) .onTouchesMove((_evt, state) => { // We only allow panning when the content is zoomed in - if (zoomScale.value <= 1) { + if (zoomScale.value <= 1 || isSwipingInPager.value) { return; } @@ -111,9 +115,8 @@ const usePanGesture = ({canvasSize, contentSize, zoomScale, totalScale, offsetX, }) .onChange((evt) => { // Since we're running both pinch and pan gesture handlers simultaneously, - // we need to make sure that we don't pan when we pinch AND move fingers - // since we track it as pinch focal gesture. - // We also need to prevent panning when we are swiping horizontally (from page to page) + // we need to make sure that we don't pan when we pinch since we track it as pinch focal gesture. + // We also need to prevent panning when we are swiping horizontally in the pager if (evt.numberOfPointers > 1 || isSwipingInPager.value) { return; } @@ -125,11 +128,9 @@ const usePanGesture = ({canvasSize, contentSize, zoomScale, totalScale, offsetX, panTranslateY.value += evt.changeY; }) .onEnd(() => { - // Add pan translation to total offset + // Add pan translation to total offset and reset gesture variables offsetX.value += panTranslateX.value; offsetY.value += panTranslateY.value; - - // Reset pan gesture variables panTranslateX.value = 0; panTranslateY.value = 0; @@ -139,10 +140,6 @@ const usePanGesture = ({canvasSize, contentSize, zoomScale, totalScale, offsetX, } finishPanGesture(); - - // Reset pan gesture variables - panVelocityX.value = 0; - panVelocityY.value = 0; }); return panGesture; From 58fe25b485ba7b3d6617df2b30757ac5c8368167 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Wed, 3 Jan 2024 18:20:38 +0100 Subject: [PATCH 124/580] simplify pinch gesture callback --- .../MultiGestureCanvas/usePinchGesture.js | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/src/components/MultiGestureCanvas/usePinchGesture.js b/src/components/MultiGestureCanvas/usePinchGesture.js index f40c58955c32..2d0b836623a6 100644 --- a/src/components/MultiGestureCanvas/usePinchGesture.js +++ b/src/components/MultiGestureCanvas/usePinchGesture.js @@ -18,8 +18,6 @@ const usePinchGesture = ({ onScaleChanged, onPinchGestureChange, }) => { - const isPinchGestureRunning = useSharedValue(false); - // Used to store event scale value when we limit scale const currentPinchScale = useSharedValue(1); @@ -71,8 +69,6 @@ const usePinchGesture = ({ state.fail(); }) .onStart((evt) => { - isPinchGestureRunning.value = true; - stopAnimation(); const adjustedFocal = getAdjustedFocal(evt.focalX, evt.focalY); @@ -121,7 +117,6 @@ const usePinchGesture = ({ pinchTranslateX.value = 0; pinchTranslateY.value = 0; currentPinchScale.value = 1; - isPinchGestureRunning.value = false; // If the content was "overzoomed" or "underzoomed", we need to bounce back with an animation if (pinchBounceTranslateX.value !== 0 || pinchBounceTranslateY.value !== 0) { @@ -148,19 +143,18 @@ const usePinchGesture = ({ }); // The "useAnimatedReaction" triggers a state update to run the "onPinchGestureChange" only once per re-render - const [isPinchGestureInUse, setIsPinchGestureInUse] = useState(false); + const [isPinchGestureRunning, setIsPinchGestureRunning] = useState(false); useAnimatedReaction( () => [zoomScale.value, isPinchGestureRunning.value], - ([zoom, running]) => { - const newIsPinchGestureInUse = zoom !== 1 || running; - if (isPinchGestureInUse !== newIsPinchGestureInUse) { - runOnJS(setIsPinchGestureInUse)(newIsPinchGestureInUse); + ([zoom]) => { + const newIsPinchGestureInUse = zoom !== 1; + if (isPinchGestureRunning !== newIsPinchGestureInUse) { + runOnJS(setIsPinchGestureRunning)(newIsPinchGestureInUse); } }, ); - // eslint-disable-next-line react-hooks/exhaustive-deps - useEffect(() => onPinchGestureChange(isPinchGestureInUse), [isPinchGestureInUse]); + useEffect(() => onPinchGestureChange(isPinchGestureRunning), [isPinchGestureRunning, onPinchGestureChange]); return pinchGesture; }; From 60b6404c3cdabc6b1e23b980770bd9ed57121ef2 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Wed, 3 Jan 2024 18:28:40 +0100 Subject: [PATCH 125/580] improve comments --- .../MultiGestureCanvas/usePanGesture.js | 3 +- .../MultiGestureCanvas/usePinchGesture.js | 35 +++++++++++++------ 2 files changed, 25 insertions(+), 13 deletions(-) diff --git a/src/components/MultiGestureCanvas/usePanGesture.js b/src/components/MultiGestureCanvas/usePanGesture.js index 8ab2078466de..4ab872394cb2 100644 --- a/src/components/MultiGestureCanvas/usePanGesture.js +++ b/src/components/MultiGestureCanvas/usePanGesture.js @@ -116,8 +116,7 @@ const usePanGesture = ({canvasSize, contentSize, zoomScale, totalScale, offsetX, .onChange((evt) => { // Since we're running both pinch and pan gesture handlers simultaneously, // we need to make sure that we don't pan when we pinch since we track it as pinch focal gesture. - // We also need to prevent panning when we are swiping horizontally in the pager - if (evt.numberOfPointers > 1 || isSwipingInPager.value) { + if (evt.numberOfPointers > 1) { return; } diff --git a/src/components/MultiGestureCanvas/usePinchGesture.js b/src/components/MultiGestureCanvas/usePinchGesture.js index 2d0b836623a6..54dd2da7943e 100644 --- a/src/components/MultiGestureCanvas/usePinchGesture.js +++ b/src/components/MultiGestureCanvas/usePinchGesture.js @@ -18,7 +18,7 @@ const usePinchGesture = ({ onScaleChanged, onPinchGestureChange, }) => { - // Used to store event scale value when we limit scale + // The current pinch gesture event scale const currentPinchScale = useSharedValue(1); // Origin of the pinch gesture @@ -27,13 +27,18 @@ const usePinchGesture = ({ y: useSharedValue(0), }; + // How much the content is translated during the pinch gesture + // While the pinch gesture is running, the pan gesture is disabled + // Therefore we need to add the translation separately const pinchTranslateX = useSharedValue(0); const pinchTranslateY = useSharedValue(0); - // In order to keep track of the "bounce" effect when pinching over/under the min/max zoom scale + + // In order to keep track of the "bounce" effect when "overzooming"/"underzooming", // we need to have extra "bounce" translation variables const pinchBounceTranslateX = useSharedValue(0); const pinchBounceTranslateY = useSharedValue(0); + // Update the total (pinch) translation based on the regular pinch + bounce useAnimatedReaction( () => [pinchTranslateX.value, pinchTranslateY.value, pinchBounceTranslateX.value, pinchBounceTranslateY.value], ([translateX, translateY, bounceX, bounceY]) => { @@ -42,6 +47,10 @@ const usePinchGesture = ({ }, ); + /** + * Calculates the adjusted focal point of the pinch gesture, + * based on the canvas size and the current offset + */ const getAdjustedFocal = MultiGestureCanvasUtils.useWorkletCallback( (focalX, focalY) => ({ x: focalX - (canvasSize.width / 2 + offsetX.value), @@ -50,6 +59,8 @@ const usePinchGesture = ({ [canvasSize.width, canvasSize.height], ); + // The pinch gesture is disabled when we release one of the fingers + // On the next render, we need to re-enable the pinch gesture const [pinchEnabled, setPinchEnabled] = useState(true); useEffect(() => { if (pinchEnabled) { @@ -60,8 +71,8 @@ const usePinchGesture = ({ const pinchGesture = Gesture.Pinch() .enabled(pinchEnabled) - .onTouchesDown((evt, state) => { - // We don't want to activate pinch gesture when we are scrolling pager + .onTouchesDown((_evt, state) => { + // We don't want to activate pinch gesture when we are swiping in the pager if (!isSwipingInPager.value) { return; } @@ -71,12 +82,14 @@ const usePinchGesture = ({ .onStart((evt) => { stopAnimation(); + // Set the origin focal point of the pinch gesture at the start of the gesture const adjustedFocal = getAdjustedFocal(evt.focalX, evt.focalY); - pinchOrigin.x.value = adjustedFocal.x; pinchOrigin.y.value = adjustedFocal.y; }) .onChange((evt) => { + // Disable the pinch gesture if one finger is released, + // to prevent the content from shaking/jumping if (evt.numberOfPointers !== 2) { runOnJS(setPinchEnabled)(false); return; @@ -84,7 +97,7 @@ const usePinchGesture = ({ const newZoomScale = pinchScale.value * evt.scale; - // Limit zoom scale to zoom range including bounce range + // Limit the zoom scale to zoom range including bounce range if ( zoomScale.value >= zoomRange.min * MultiGestureCanvasUtils.zoomScaleBounceFactors.min && zoomScale.value <= zoomRange.max * MultiGestureCanvasUtils.zoomScaleBounceFactors.max @@ -98,6 +111,8 @@ const usePinchGesture = ({ const newPinchTranslateX = adjustedFocal.x + currentPinchScale.value * pinchOrigin.x.value * -1; const newPinchTranslateY = adjustedFocal.y + currentPinchScale.value * pinchOrigin.y.value * -1; + // If the zoom scale is within the zoom range, we perform the regular pinch translation + // Otherwise it means that we are "overzoomed" or "underzoomed", so we need to bounce back if (zoomScale.value >= zoomRange.min && zoomScale.value <= zoomRange.max) { pinchTranslateX.value = newPinchTranslateX; pinchTranslateY.value = newPinchTranslateY; @@ -109,11 +124,9 @@ const usePinchGesture = ({ } }) .onEnd(() => { - // Add pinch translation to total offset + // Add pinch translation to total offset and reset gesture variables offsetX.value += pinchTranslateX.value; offsetY.value += pinchTranslateY.value; - - // Reset pinch gesture variables pinchTranslateX.value = 0; pinchTranslateY.value = 0; currentPinchScale.value = 1; @@ -142,7 +155,8 @@ const usePinchGesture = ({ } }); - // The "useAnimatedReaction" triggers a state update to run the "onPinchGestureChange" only once per re-render + // The "useAnimatedReaction" triggers a state update only when the value changed, + // which then triggers the "onPinchGestureChange" callback const [isPinchGestureRunning, setIsPinchGestureRunning] = useState(false); useAnimatedReaction( () => [zoomScale.value, isPinchGestureRunning.value], @@ -153,7 +167,6 @@ const usePinchGesture = ({ } }, ); - useEffect(() => onPinchGestureChange(isPinchGestureRunning), [isPinchGestureRunning, onPinchGestureChange]); return pinchGesture; From b1b592a60642b6bf226de80a524f93554577c832 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Wed, 3 Jan 2024 18:35:10 +0100 Subject: [PATCH 126/580] add more comments --- src/components/MultiGestureCanvas/index.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/components/MultiGestureCanvas/index.js b/src/components/MultiGestureCanvas/index.js index 0577ec79d0d6..128f1100b338 100644 --- a/src/components/MultiGestureCanvas/index.js +++ b/src/components/MultiGestureCanvas/index.js @@ -34,6 +34,7 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr const attachmentCarouselPagerContext = useContext(AttachmentCarouselPagerContext); const pagerRefFallback = useRef(null); + // If the MultiGestureCanvas used inside a AttachmentCarouselPager, we need to adapt the behaviour based on the pager state const {onTap, pagerRef, shouldPagerScroll, isSwipingInPager, onPinchGestureChange} = attachmentCarouselPagerContext || { onTap: () => undefined, onPinchGestureChange: () => undefined, @@ -43,6 +44,9 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr ...props, }; + // Based on the (original) content size and the canvas size, we calculate the horizontal and vertical scale factors + // to fit the content inside the canvas + // We later use the lower of the two scale factors to fit the content inside the canvas const {minScale: minContentScale, maxScale: maxContentScale} = useMemo(() => getCanvasFitScale({canvasSize, contentSize}), [canvasSize, contentSize]); const zoomScale = useSharedValue(1); @@ -64,11 +68,17 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr const offsetX = useSharedValue(0); const offsetY = useSharedValue(0); + /** + * Stops any currently running decay animation from panning + */ const stopAnimation = MultiGestureCanvasUtils.useWorkletCallback(() => { cancelAnimation(offsetX); cancelAnimation(offsetY); }); + /** + * Resets the canvas to the initial state and animates back smoothly + */ const reset = MultiGestureCanvasUtils.useWorkletCallback((animated) => { pinchScale.value = 1; @@ -152,6 +162,7 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr }, ); + // Trigger a reset when the canvas gets inactive, but only if it was already mounted before const mounted = useRef(false); useEffect(() => { if (!mounted.current) { @@ -164,6 +175,7 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr } }, [isActive, mounted, reset]); + // Animate the x and y position of the content within the canvas based on all of the gestures const animatedStyles = useAnimatedStyle(() => { const x = pinchTranslateX.value + panTranslateX.value + offsetX.value; const y = pinchTranslateY.value + panTranslateY.value + offsetY.value; From 8856caf3e53be49289c34476d3c780d713cacbd5 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Thu, 4 Jan 2024 00:05:56 +0100 Subject: [PATCH 127/580] fix: eslint --- .../Attachments/AttachmentCarousel/Pager/index.tsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/components/Attachments/AttachmentCarousel/Pager/index.tsx b/src/components/Attachments/AttachmentCarousel/Pager/index.tsx index 693c9b86fae9..2f86bb98f796 100644 --- a/src/components/Attachments/AttachmentCarousel/Pager/index.tsx +++ b/src/components/Attachments/AttachmentCarousel/Pager/index.tsx @@ -1,8 +1,11 @@ import React, {useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react'; import {View} from 'react-native'; -import {createNativeWrapper, NativeViewGestureHandlerProps} from 'react-native-gesture-handler'; -import PagerView, {PagerViewProps} from 'react-native-pager-view'; -import Animated, {AnimatedProps, runOnJS, useAnimatedProps, useAnimatedReaction, useSharedValue} from 'react-native-reanimated'; +import type {NativeViewGestureHandlerProps} from 'react-native-gesture-handler'; +import {createNativeWrapper} from 'react-native-gesture-handler'; +import type {PagerViewProps} from 'react-native-pager-view'; +import PagerView from 'react-native-pager-view'; +import type {AnimatedProps} from 'react-native-reanimated'; +import Animated, {runOnJS, useAnimatedProps, useAnimatedReaction, useSharedValue} from 'react-native-reanimated'; import useThemeStyles from '@hooks/useThemeStyles'; import AttachmentCarouselPagerContext from './AttachmentCarouselPagerContext'; import usePageScrollHandler from './usePageScrollHandler'; From b6bbdece8beda8ea9cc4bea91d81f3fb29b30028 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Thu, 4 Jan 2024 00:10:04 +0100 Subject: [PATCH 128/580] fix: eslint --- .../Pager/AttachmentCarouselPagerContext.ts | 4 ++-- .../AttachmentCarousel/Pager/usePageScrollHandler.ts | 2 +- src/components/Lightbox.js | 7 ++++--- src/components/MultiGestureCanvas/index.tsx | 7 ++++--- 4 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts b/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts index 85f3e6adbda5..846cafb7d443 100644 --- a/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts +++ b/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts @@ -1,6 +1,6 @@ import {createContext} from 'react'; -import PagerView from 'react-native-pager-view'; -import {SharedValue} from 'react-native-reanimated'; +import type PagerView from 'react-native-pager-view'; +import type {SharedValue} from 'react-native-reanimated'; type AttachmentCarouselPagerContextType = { onTap: () => void; diff --git a/src/components/Attachments/AttachmentCarousel/Pager/usePageScrollHandler.ts b/src/components/Attachments/AttachmentCarousel/Pager/usePageScrollHandler.ts index 9841129d036c..bcc616883d72 100644 --- a/src/components/Attachments/AttachmentCarousel/Pager/usePageScrollHandler.ts +++ b/src/components/Attachments/AttachmentCarousel/Pager/usePageScrollHandler.ts @@ -1,4 +1,4 @@ -import {PagerViewProps} from 'react-native-pager-view'; +import type {PagerViewProps} from 'react-native-pager-view'; import {useEvent, useHandler} from 'react-native-reanimated'; type PageScrollHandler = NonNullable; diff --git a/src/components/Lightbox.js b/src/components/Lightbox.js index c78a3569d73a..64d6e25c9c36 100644 --- a/src/components/Lightbox.js +++ b/src/components/Lightbox.js @@ -6,7 +6,6 @@ import useStyleUtils from '@hooks/useStyleUtils'; import * as AttachmentsPropTypes from './Attachments/propTypes'; import Image from './Image'; import MultiGestureCanvas from './MultiGestureCanvas'; -import {zoomRangeDefaultProps, zoomRangePropTypes} from './MultiGestureCanvas/propTypes'; import getCanvasFitScale from './MultiGestureCanvas/utils'; // Increase/decrease this number to change the number of concurrent lightboxes @@ -20,7 +19,8 @@ const cachedDimensions = new Map(); * On the native layer, we use a image library to handle zoom functionality */ const propTypes = { - ...zoomRangePropTypes, + // TODO: Add TS types for zoom range + // ...zoomRangePropTypes, /** Function for handle on press */ onPress: PropTypes.func, @@ -48,7 +48,8 @@ const propTypes = { }; const defaultProps = { - ...zoomRangeDefaultProps, + // TODO: Add TS default values + // ...zoomRangeDefaultProps, isAuthTokenRequired: false, index: 0, diff --git a/src/components/MultiGestureCanvas/index.tsx b/src/components/MultiGestureCanvas/index.tsx index c3760434fa97..21624f235092 100644 --- a/src/components/MultiGestureCanvas/index.tsx +++ b/src/components/MultiGestureCanvas/index.tsx @@ -1,14 +1,15 @@ import React, {useContext, useEffect, useMemo, useRef} from 'react'; import {View} from 'react-native'; import {Gesture, GestureDetector} from 'react-native-gesture-handler'; -import PagerView from 'react-native-pager-view'; +import type PagerView from 'react-native-pager-view'; import Animated, {cancelAnimation, runOnUI, useAnimatedReaction, useAnimatedStyle, useDerivedValue, useSharedValue, withSpring} from 'react-native-reanimated'; -import AttachmentCarouselPagerContext, {AttachmentCarouselPagerContextType} from '@components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext'; +import type {AttachmentCarouselPagerContextType} from '@components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext'; +import AttachmentCarouselPagerContext from '@components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; import {defaultZoomRange} from './constants'; import getCanvasFitScale from './getCanvasFitScale'; -import {ContentSizeProp, ZoomRangeProp} from './types'; +import type {ContentSizeProp, ZoomRangeProp} from './types'; import usePanGesture from './usePanGesture'; import usePinchGesture from './usePinchGesture'; import useTapGestures from './useTapGestures'; From d5278ef1a208f07ace0f121b34bd9848ba5ae878 Mon Sep 17 00:00:00 2001 From: DylanDylann Date: Thu, 4 Jan 2024 15:37:10 +0700 Subject: [PATCH 129/580] fix duplicate phone number can be invited --- src/libs/OptionsListUtils.js | 2 +- src/pages/RoomInvitePage.js | 10 ++++++++-- src/pages/workspace/WorkspaceInvitePage.js | 2 +- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/libs/OptionsListUtils.js b/src/libs/OptionsListUtils.js index fa3538b58ca6..bd869f3d8e0d 100644 --- a/src/libs/OptionsListUtils.js +++ b/src/libs/OptionsListUtils.js @@ -1479,7 +1479,7 @@ 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)) { + if (_.some(optionsToExclude, (optionToExclude) => optionToExclude.login === addSMSDomainIfPhoneNumber(personalDetailOption.login))) { return; } const {searchText, participantsList, isChatRoom} = personalDetailOption; diff --git a/src/pages/RoomInvitePage.js b/src/pages/RoomInvitePage.js index aebdec047895..b440be16823d 100644 --- a/src/pages/RoomInvitePage.js +++ b/src/pages/RoomInvitePage.js @@ -70,7 +70,13 @@ function RoomInvitePage(props) { const [userToInvite, setUserToInvite] = useState(null); // Any existing participants and Expensify emails should not be eligible for invitation - const excludedUsers = useMemo(() => [...PersonalDetailsUtils.getLoginsByAccountIDs(lodashGet(props.report, 'participantAccountIDs', [])), ...CONST.EXPENSIFY_EMAILS], [props.report]); + const excludedUsers = useMemo( + () => + _.map([...PersonalDetailsUtils.getLoginsByAccountIDs(lodashGet(props.report, 'participantAccountIDs', [])), ...CONST.EXPENSIFY_EMAILS], (participant) => + OptionsListUtils.addSMSDomainIfPhoneNumber(participant), + ), + [props.report], + ); useEffect(() => { const inviteOptions = OptionsListUtils.getMemberInviteOptions(props.personalDetails, props.betas, searchTerm, excludedUsers); @@ -191,7 +197,7 @@ function RoomInvitePage(props) { if (!userToInvite && CONST.EXPENSIFY_EMAILS.includes(searchValue)) { return translate('messages.errorMessageInvalidEmail'); } - if (!userToInvite && excludedUsers.includes(searchValue)) { + if (!userToInvite && excludedUsers.includes(OptionsListUtils.addSMSDomainIfPhoneNumber(searchValue).toLowerCase())) { return translate('messages.userIsAlreadyMember', {login: searchValue, name: reportName}); } return OptionsListUtils.getHeaderMessage(personalDetails.length !== 0, Boolean(userToInvite), searchValue); diff --git a/src/pages/workspace/WorkspaceInvitePage.js b/src/pages/workspace/WorkspaceInvitePage.js index 589c4971506b..b3c6e6da839c 100644 --- a/src/pages/workspace/WorkspaceInvitePage.js +++ b/src/pages/workspace/WorkspaceInvitePage.js @@ -237,7 +237,7 @@ function WorkspaceInvitePage(props) { if (usersToInvite.length === 0 && CONST.EXPENSIFY_EMAILS.includes(searchValue)) { return translate('messages.errorMessageInvalidEmail'); } - if (usersToInvite.length === 0 && excludedUsers.includes(searchValue)) { + if (usersToInvite.length === 0 && excludedUsers.includes(OptionsListUtils.addSMSDomainIfPhoneNumber(searchValue))) { return translate('messages.userIsAlreadyMember', {login: searchValue, name: policyName}); } return OptionsListUtils.getHeaderMessage(personalDetails.length !== 0, usersToInvite.length > 0, searchValue); 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 130/580] 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 c68ff38657481a03452949ff974751e158acfdda Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Thu, 4 Jan 2024 12:02:25 +0100 Subject: [PATCH 131/580] 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 132/580] 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 133/580] 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 134/580] 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 135/580] 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 136/580] 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 137/580] 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 138/580] 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 139/580] 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 140/580] 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 f8445e493b80c57c660bf22345c44e49f3533e8d Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Thu, 4 Jan 2024 16:36:18 +0100 Subject: [PATCH 141/580] remove pager ref --- src/components/MultiGestureCanvas/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/MultiGestureCanvas/index.tsx b/src/components/MultiGestureCanvas/index.tsx index 21624f235092..50b68665556b 100644 --- a/src/components/MultiGestureCanvas/index.tsx +++ b/src/components/MultiGestureCanvas/index.tsx @@ -174,7 +174,7 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr isSwipingInPager, stopAnimation, }) - .simultaneousWithExternalGesture(pagerRef, singleTapGesture, doubleTapGesture) + .simultaneousWithExternalGesture(singleTapGesture, doubleTapGesture) .withRef(panGestureRef); const pinchGesture = usePinchGesture({ From ad6fe37fc7cee5eb2008f45995a4e09ac0de9bd2 Mon Sep 17 00:00:00 2001 From: Ishpaul Singh Date: Thu, 4 Jan 2024 21:08:56 +0530 Subject: [PATCH 142/580] 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 143/580] 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 937b4b16cbc8c769e0a8b3016face74b0554777a Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Thu, 4 Jan 2024 16:50:34 +0100 Subject: [PATCH 144/580] update comments --- src/components/MultiGestureCanvas/usePanGesture.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/components/MultiGestureCanvas/usePanGesture.js b/src/components/MultiGestureCanvas/usePanGesture.js index 4ab872394cb2..aec24cb2e99e 100644 --- a/src/components/MultiGestureCanvas/usePanGesture.js +++ b/src/components/MultiGestureCanvas/usePanGesture.js @@ -3,6 +3,9 @@ import {Gesture} from 'react-native-gesture-handler'; import {useDerivedValue, useSharedValue, withDecay, withSpring} from 'react-native-reanimated'; import * as MultiGestureCanvasUtils from './utils'; +// This value determines how fast the pan animation should phase out +// We're using a "withDecay" animation to smoothly phase out the pan animation +// https://docs.swmansion.com/react-native-reanimated/docs/animations/withDecay/ const PAN_DECAY_DECELARATION = 0.9915; const usePanGesture = ({canvasSize, contentSize, zoomScale, totalScale, offsetX, offsetY, panTranslateX, panTranslateY, isSwipingInPager, stopAnimation}) => { @@ -10,7 +13,8 @@ const usePanGesture = ({canvasSize, contentSize, zoomScale, totalScale, offsetX, const zoomedContentWidth = useDerivedValue(() => contentSize.width * totalScale.value, [contentSize.width]); const zoomedContentHeight = useDerivedValue(() => contentSize.height * totalScale.value, [contentSize.height]); - // Pan velocity to calculate the decay + // Velocity of the pan gesture + // We need to keep track of the velocity to properly phase out/decay the pan animation const panVelocityX = useSharedValue(0); const panVelocityY = useSharedValue(0); @@ -50,9 +54,9 @@ const usePanGesture = ({canvasSize, contentSize, zoomScale, totalScale, offsetX, }; }, [canvasSize.width, canvasSize.height]); - // We want to smoothly gesture by phasing out the pan animation + // We want to smoothly decay/end the gesture by phasing out the pan animation // In case the content is outside of the boundaries of the canvas, - // we need to return to the view to the boundaries + // we need to move the content back into the boundaries const finishPanGesture = MultiGestureCanvasUtils.useWorkletCallback(() => { // If the content is centered within the canvas, we don't need to run any animations if (offsetX.value === 0 && offsetY.value === 0 && panTranslateX.value === 0 && panTranslateY.value === 0) { From 1f979ee695dd528d99ce4d7e3c774f66abc02c8a Mon Sep 17 00:00:00 2001 From: Ishpaul Singh Date: Thu, 4 Jan 2024 21:21:13 +0530 Subject: [PATCH 145/580] 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 7e148eefe0fe3e1e8502b3a1476e88574753eef5 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Thu, 4 Jan 2024 16:56:51 +0100 Subject: [PATCH 146/580] improve lightbox styles --- src/components/Lightbox.js | 8 +++++++- src/styles/utils/index.ts | 2 ++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/components/Lightbox.js b/src/components/Lightbox.js index 0b09ed1e745a..1510f38eea20 100644 --- a/src/components/Lightbox.js +++ b/src/components/Lightbox.js @@ -93,6 +93,12 @@ function Lightbox({isAuthTokenRequired, source, onScaleChanged, onPress, onError }, [activeIndex, index]); const isLightboxVisible = isLightboxInRange && (isActive || isLightboxLoaded || isFallbackLoaded); + // If the fallback image is currently visible, we want to hide the Lightbox until the fallback gets hidden, + // so that we don't see two overlapping images at the same time. + // If there the Lightbox is not used within a carousel, we don't need to hide the Lightbox, + // because it's only going to be rendered after the fallback image is hidden. + const shouldHideLightbox = hasSiblingCarouselItems && isFallbackVisible; + const isLoading = isActive && (!isContainerLoaded || !isImageLoaded); const updateCanvasSize = useCallback( @@ -168,7 +174,7 @@ function Lightbox({isAuthTokenRequired, source, onScaleChanged, onPress, onError {isContainerLoaded && ( <> {isLightboxVisible && ( - + ({ }, getFullscreenCenteredContentStyles: () => [StyleSheet.absoluteFill, styles.justifyContentCenter, styles.alignItemsCenter], + + getLightboxVisibilityStyle: (isHidden: boolean) => ({opacity: isHidden ? 0 : 1}), }); type StyleUtilsType = ReturnType; From 9b6c8fef5b118e039f1850eb6841f1c1c6f110ce Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Thu, 4 Jan 2024 18:45:22 +0100 Subject: [PATCH 147/580] remove onPinchGestureChange callback --- .../AttachmentCarousel/Pager/index.js | 14 +++---- src/components/Lightbox.js | 4 ++ src/components/MultiGestureCanvas/index.js | 40 +++++++++++++------ 3 files changed, 39 insertions(+), 19 deletions(-) diff --git a/src/components/Attachments/AttachmentCarousel/Pager/index.js b/src/components/Attachments/AttachmentCarousel/Pager/index.js index d7d6bda1be29..e0f652e47e4c 100644 --- a/src/components/Attachments/AttachmentCarousel/Pager/index.js +++ b/src/components/Attachments/AttachmentCarousel/Pager/index.js @@ -40,7 +40,7 @@ const pagerPropTypes = { initialIndex: PropTypes.number, onPageSelected: PropTypes.func, onTap: PropTypes.func, - onPinchGestureChange: PropTypes.func, + onScaleChanged: PropTypes.func, forwardedRef: refPropTypes, }; @@ -48,11 +48,11 @@ const pagerDefaultProps = { initialIndex: 0, onPageSelected: () => {}, onTap: () => {}, - onPinchGestureChange: () => {}, + onScaleChanged: () => {}, forwardedRef: null, }; -function AttachmentCarouselPager({items, renderItem, initialIndex, onPageSelected, onTap, onPinchGestureChange, forwardedRef}) { +function AttachmentCarouselPager({items, renderItem, initialIndex, onPageSelected, onTap, onScaleChanged, forwardedRef}) { const styles = useThemeStyles(); const shouldPagerScroll = useSharedValue(true); const pagerRef = useRef(null); @@ -106,13 +106,13 @@ function AttachmentCarouselPager({items, renderItem, initialIndex, onPageSelecte const contextValue = useMemo( () => ({ - isSwipingInPager, + onTap, + onScaleChanged, pagerRef, shouldPagerScroll, - onPinchGestureChange, - onTap, + isSwipingInPager, }), - [isSwipingInPager, pagerRef, shouldPagerScroll, onPinchGestureChange, onTap], + [isSwipingInPager, shouldPagerScroll, onScaleChanged, onTap], ); return ( diff --git a/src/components/Lightbox.js b/src/components/Lightbox.js index 1510f38eea20..366759cc6cd7 100644 --- a/src/components/Lightbox.js +++ b/src/components/Lightbox.js @@ -22,6 +22,9 @@ const cachedDimensions = new Map(); const propTypes = { ...zoomRangePropTypes, + /** Triggers whenever the zoom scale changes */ + onScaleChanged: PropTypes.func, + /** Function for handle on press */ onPress: PropTypes.func, @@ -54,6 +57,7 @@ const defaultProps = { index: 0, activeIndex: 0, hasSiblingCarouselItems: false, + onScaleChanged: () => {}, onPress: () => {}, onError: () => {}, style: {}, diff --git a/src/components/MultiGestureCanvas/index.js b/src/components/MultiGestureCanvas/index.js index 128f1100b338..ed0b0db38124 100644 --- a/src/components/MultiGestureCanvas/index.js +++ b/src/components/MultiGestureCanvas/index.js @@ -26,23 +26,40 @@ function getDeepDefaultProps({contentSize: contentSizeProp = {}, zoomRange: zoom return {contentSize, zoomRange}; } -function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, children, ...props}) { +function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged: onScaleChangedProp, children, ...props}) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); const {contentSize, zoomRange} = getDeepDefaultProps(props); - const attachmentCarouselPagerContext = useContext(AttachmentCarouselPagerContext); - const pagerRefFallback = useRef(null); + // If the MultiGestureCanvas used inside a AttachmentCarouselPager, we need to adapt the behaviour based on the pager state - const {onTap, pagerRef, shouldPagerScroll, isSwipingInPager, onPinchGestureChange} = attachmentCarouselPagerContext || { - onTap: () => undefined, - onPinchGestureChange: () => undefined, - pagerRef: pagerRefFallback, - shouldPagerScroll: false, - isSwipingInPager: false, - ...props, - }; + const attachmentCarouselPagerContext = useContext(AttachmentCarouselPagerContext); + const { + onTap, + onScaleChanged: onScaleChangedContext, + pagerRef, + shouldPagerScroll, + isSwipingInPager, + } = useMemo( + () => + attachmentCarouselPagerContext || { + onTap: () => {}, + onScaleChanged: () => {}, + pagerRef: pagerRefFallback, + shouldPagerScroll: false, + isSwipingInPager: false, + }, + [attachmentCarouselPagerContext], + ); + + const onScaleChanged = useMemo( + (newScale) => { + onScaleChangedProp(newScale); + onScaleChangedContext(newScale); + }, + [onScaleChangedContext, onScaleChangedProp], + ); // Based on the (original) content size and the canvas size, we calculate the horizontal and vertical scale factors // to fit the content inside the canvas @@ -150,7 +167,6 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr isSwipingInPager, stopAnimation, onScaleChanged, - onPinchGestureChange, }).simultaneousWithExternalGesture(panGesture, singleTapGesture, doubleTapGesture); // Enables/disables the pager scroll based on the zoom scale From 351eb53407a240a406e73c8737aca4a8c863a492 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Thu, 4 Jan 2024 18:49:43 +0100 Subject: [PATCH 148/580] fix: carousel arrows --- .../AttachmentCarousel/index.native.js | 25 ++++++++----- src/components/MultiGestureCanvas/index.js | 4 +-- .../MultiGestureCanvas/usePinchGesture.js | 36 ++++++++----------- 3 files changed, 34 insertions(+), 31 deletions(-) diff --git a/src/components/Attachments/AttachmentCarousel/index.native.js b/src/components/Attachments/AttachmentCarousel/index.native.js index 003c27844fbc..8f168093c217 100644 --- a/src/components/Attachments/AttachmentCarousel/index.native.js +++ b/src/components/Attachments/AttachmentCarousel/index.native.js @@ -23,7 +23,7 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source, const pagerRef = useRef(null); const [page, setPage] = useState(); const [attachments, setAttachments] = useState([]); - const [isPinchGestureRunning, setIsPinchGestureRunning] = useState(true); + const [isZoomedOut, setIsZoomedOut] = useState(true); const [shouldShowArrows, setShouldShowArrows, autoHideArrows, cancelAutoHideArrows] = useCarouselArrows(); const [activeSource, setActiveSource] = useState(source); @@ -107,6 +107,20 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source, [activeSource, attachments.length, page, setShouldShowArrows, shouldShowArrows], ); + const handleScaleChange = useCallback( + (newScale) => { + const newIsZoomedOut = newScale === 1; + + if (isZoomedOut === newIsZoomedOut) { + return; + } + + setIsZoomedOut(newIsZoomedOut); + setShouldShowArrows(newIsZoomedOut); + }, + [isZoomedOut, setShouldShowArrows], + ); + return ( cycleThroughAttachments(-1)} @@ -141,12 +155,7 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source, renderItem={renderItem} initialIndex={page} onPageSelected={({nativeEvent: {position: newPage}}) => updatePage(newPage)} - onPinchGestureChange={(newIsPinchGestureRunning) => { - setIsPinchGestureRunning(newIsPinchGestureRunning); - if (!newIsPinchGestureRunning && !shouldShowArrows) { - setShouldShowArrows(true); - } - }} + onScaleChanged={handleScaleChange} ref={pagerRef} /> diff --git a/src/components/MultiGestureCanvas/index.js b/src/components/MultiGestureCanvas/index.js index ed0b0db38124..8c455836300d 100644 --- a/src/components/MultiGestureCanvas/index.js +++ b/src/components/MultiGestureCanvas/index.js @@ -1,4 +1,4 @@ -import React, {useContext, useEffect, useMemo, useRef} from 'react'; +import React, {useCallback, useContext, useEffect, useMemo, useRef} from 'react'; import {View} from 'react-native'; import {Gesture, GestureDetector} from 'react-native-gesture-handler'; import Animated, {cancelAnimation, runOnUI, useAnimatedReaction, useAnimatedStyle, useDerivedValue, useSharedValue, withSpring} from 'react-native-reanimated'; @@ -53,7 +53,7 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged: onScal [attachmentCarouselPagerContext], ); - const onScaleChanged = useMemo( + const onScaleChanged = useCallback( (newScale) => { onScaleChangedProp(newScale); onScaleChangedContext(newScale); diff --git a/src/components/MultiGestureCanvas/usePinchGesture.js b/src/components/MultiGestureCanvas/usePinchGesture.js index 54dd2da7943e..21c5e55e0117 100644 --- a/src/components/MultiGestureCanvas/usePinchGesture.js +++ b/src/components/MultiGestureCanvas/usePinchGesture.js @@ -16,7 +16,6 @@ const usePinchGesture = ({ isSwipingInPager, stopAnimation, onScaleChanged, - onPinchGestureChange, }) => { // The current pinch gesture event scale const currentPinchScale = useSharedValue(1); @@ -104,6 +103,10 @@ const usePinchGesture = ({ ) { zoomScale.value = newZoomScale; currentPinchScale.value = evt.scale; + + if (onScaleChanged != null) { + runOnJS(onScaleChanged)(zoomScale.value); + } } // Calculate new pinch translation @@ -137,38 +140,29 @@ const usePinchGesture = ({ pinchBounceTranslateY.value = withSpring(0, MultiGestureCanvasUtils.SPRING_CONFIG); } + const triggerScaleChangeCallback = () => { + if (onScaleChanged == null) { + return; + } + + runOnJS(onScaleChanged)(zoomScale.value); + }; + if (zoomScale.value < zoomRange.min) { // If the zoom scale is less than the minimum zoom scale, we need to set the zoom scale to the minimum pinchScale.value = zoomRange.min; - zoomScale.value = withSpring(zoomRange.min, MultiGestureCanvasUtils.SPRING_CONFIG); + zoomScale.value = withSpring(zoomRange.min, MultiGestureCanvasUtils.SPRING_CONFIG, triggerScaleChangeCallback); } else if (zoomScale.value > zoomRange.max) { // If the zoom scale is higher than the maximum zoom scale, we need to set the zoom scale to the maximum pinchScale.value = zoomRange.max; - zoomScale.value = withSpring(zoomRange.max, MultiGestureCanvasUtils.SPRING_CONFIG); + zoomScale.value = withSpring(zoomRange.max, MultiGestureCanvasUtils.SPRING_CONFIG, triggerScaleChangeCallback); } else { // Otherwise, we just update the pinch scale offset pinchScale.value = zoomScale.value; - } - - if (onScaleChanged != null) { - runOnJS(onScaleChanged)(pinchScale.value); + triggerScaleChangeCallback(); } }); - // The "useAnimatedReaction" triggers a state update only when the value changed, - // which then triggers the "onPinchGestureChange" callback - const [isPinchGestureRunning, setIsPinchGestureRunning] = useState(false); - useAnimatedReaction( - () => [zoomScale.value, isPinchGestureRunning.value], - ([zoom]) => { - const newIsPinchGestureInUse = zoom !== 1; - if (isPinchGestureRunning !== newIsPinchGestureInUse) { - runOnJS(setIsPinchGestureRunning)(newIsPinchGestureInUse); - } - }, - ); - useEffect(() => onPinchGestureChange(isPinchGestureRunning), [isPinchGestureRunning, onPinchGestureChange]); - return pinchGesture; }; From 375597ed47f98bcdeade201685450f422c26d36f Mon Sep 17 00:00:00 2001 From: Someshwar Tripathi Date: Fri, 5 Jan 2024 06:58:15 +0530 Subject: [PATCH 149/580] Reorder imports --- src/pages/home/HeaderView.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/home/HeaderView.js b/src/pages/home/HeaderView.js index f0f97a1c7e1a..4dad55f0aad1 100644 --- a/src/pages/home/HeaderView.js +++ b/src/pages/home/HeaderView.js @@ -6,8 +6,8 @@ import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; import GoogleMeetIcon from '@assets/images/google-meet.svg'; import ZoomIcon from '@assets/images/zoom-icon.svg'; -import ConfirmModal from '@components/ConfirmModal'; import Button from '@components/Button'; +import ConfirmModal from '@components/ConfirmModal'; import DisplayNames from '@components/DisplayNames'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; From 3398196cabff8abf05dd7589496a1a41bdec6f24 Mon Sep 17 00:00:00 2001 From: Someshwar Tripathi Date: Fri, 5 Jan 2024 07:11:06 +0530 Subject: [PATCH 150/580] Change copy to use the 'Delete' keyword --- src/languages/en.ts | 4 ++-- src/languages/es.ts | 4 ++-- src/pages/home/HeaderView.js | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index f3c5437bacad..7159198c84f0 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -1745,8 +1745,8 @@ export default { markAsIncomplete: 'Mark as incomplete', assigneeError: 'There was an error assigning this task, please try another assignee.', genericCreateTaskFailureMessage: 'Unexpected error create task, please try again later.', - cancelTask: 'Cancel task', - cancelConfirmation: 'Are you sure that you want to cancel this task?', + deleteTask: 'Delete task', + deleteConfirmation: 'Are you sure that you want to delete this task?', }, statementPage: { title: (year, monthName) => `${monthName} ${year} statement`, diff --git a/src/languages/es.ts b/src/languages/es.ts index 7b33a2b1e428..8c4e99c24d34 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -1771,8 +1771,8 @@ export default { markAsIncomplete: 'Marcar como incompleta', assigneeError: 'Hubo un error al asignar esta tarea, inténtalo con otro usuario.', genericCreateTaskFailureMessage: 'Error inesperado al crear el tarea, por favor, inténtalo más tarde.', - cancelTask: 'Cancelar tarea', - cancelConfirmation: '¿Estás seguro de que quieres cancelar esta tarea?', + deleteTask: 'Eliminar tarea', + deleteConfirmation: '¿Estás seguro de que quieres eliminar esta tarea?', }, statementPage: { title: (year, monthName) => `Estado de cuenta de ${monthName} ${year}`, diff --git a/src/pages/home/HeaderView.js b/src/pages/home/HeaderView.js index 4dad55f0aad1..3ab0a2798eb7 100644 --- a/src/pages/home/HeaderView.js +++ b/src/pages/home/HeaderView.js @@ -325,8 +325,8 @@ function HeaderView(props) { Session.checkIfActionIsAllowed(Task.deleteTask(props.report.reportID, props.report.reportName, props.report.stateNum, props.report.statusNum)); }} onCancel={() => setIsCancelTaskConfirmModalVisible(false)} - title={translate('task.cancelTask')} - prompt={translate('task.cancelConfirmation')} + title={translate('task.deleteTask')} + prompt={translate('task.deleteConfirmation')} confirmText={translate('common.delete')} cancelText={translate('common.cancel')} danger From 5dbf2c96bebe469a6b64af701e2ef5f7738753ea Mon Sep 17 00:00:00 2001 From: tienifr Date: Fri, 5 Jan 2024 16:24:37 +0700 Subject: [PATCH 151/580] 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 1aaf1689389b7ea515a59ab405ff41545ea8ff5f Mon Sep 17 00:00:00 2001 From: tienifr Date: Fri, 5 Jan 2024 17:12:22 +0700 Subject: [PATCH 152/580] add js docs --- src/libs/actions/IOU.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index a5e6ab492bdc..efd7ec594cc2 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -322,6 +322,7 @@ function getReceiptError(receipt, filename, isScanRequest = true) { * @param {Array} optimisticPolicyRecentlyUsedTags * @param {boolean} isNewChatReport * @param {boolean} isNewIOUReport + * @param {String} policyID * @param {Object} policy - May be undefined, an empty object, or an object matching the Policy type (src/types/onyx/Policy.ts) * @param {Array} policyTags * @param {Array} policyCategories From 216aad48bc422cc6599cb8e58692405855b160af Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Fri, 5 Jan 2024 13:29:59 +0100 Subject: [PATCH 153/580] 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 fcfa05997786695e6d74995a1c84b237ee9f1889 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Fri, 5 Jan 2024 17:32:19 +0100 Subject: [PATCH 154/580] migrate hooks --- src/components/MultiGestureCanvas/index.tsx | 37 ++---------- src/components/MultiGestureCanvas/types.ts | 58 ++++++++++++++++++- .../{usePanGesture.js => usePanGesture.ts} | 18 +++++- ...{usePinchGesture.js => usePinchGesture.ts} | 21 ++++++- .../{useTapGestures.js => useTapGestures.ts} | 35 ++++++++++- src/components/MultiGestureCanvas/utils.ts | 8 ++- 6 files changed, 135 insertions(+), 42 deletions(-) rename src/components/MultiGestureCanvas/{usePanGesture.js => usePanGesture.ts} (88%) rename src/components/MultiGestureCanvas/{usePinchGesture.js => usePinchGesture.ts} (88%) rename src/components/MultiGestureCanvas/{useTapGestures.js => useTapGestures.ts} (80%) diff --git a/src/components/MultiGestureCanvas/index.tsx b/src/components/MultiGestureCanvas/index.tsx index 7759c5c9b018..efe75f21af96 100644 --- a/src/components/MultiGestureCanvas/index.tsx +++ b/src/components/MultiGestureCanvas/index.tsx @@ -8,46 +8,19 @@ import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; import {defaultZoomRange} from './constants'; import getCanvasFitScale from './getCanvasFitScale'; -import type {ContentSizeProp, ZoomRangeProp} from './types'; +import type {ContentSize, MultiGestureCanvasProps, ZoomRange} from './types'; import usePanGesture from './usePanGesture'; import usePinchGesture from './usePinchGesture'; import useTapGestures from './useTapGestures'; import * as MultiGestureCanvasUtils from './utils'; -type MultiGestureCanvasProps = React.PropsWithChildren<{ - /** - * Wheter the canvas is currently active (in the screen) or not. - * Disables certain gestures and functionality - */ - isActive: boolean; - - /** Handles scale changed event */ - onScaleChanged: (zoomScale: number) => void; - - /** The width and height of the canvas. - * This is needed in order to properly scale the content in the canvas - */ - canvasSize: { - width: number; - height: number; - }; - - /** The width and height of the content. - * This is needed in order to properly scale the content in the canvas - */ - contentSize: ContentSizeProp; - - /** Range of zoom that can be applied to the content by pinching or double tapping. */ - zoomRange?: ZoomRangeProp; -}>; - type Props = { - contentSize?: ContentSizeProp; - zoomRange?: ZoomRangeProp; + contentSize?: ContentSize; + zoomRange?: ZoomRange; }; type PropsWithDefault = { - contentSize: ContentSizeProp; - zoomRange: Required; + contentSize: ContentSize; + zoomRange: Required; }; const getDeepDefaultProps = ({contentSize: contentSizeProp, zoomRange: zoomRangeProp}: Props): PropsWithDefault => { const contentSize = { diff --git a/src/components/MultiGestureCanvas/types.ts b/src/components/MultiGestureCanvas/types.ts index 11dfc767aacf..4ca0f5a1fe05 100644 --- a/src/components/MultiGestureCanvas/types.ts +++ b/src/components/MultiGestureCanvas/types.ts @@ -1,11 +1,63 @@ -type ContentSizeProp = { +import type {SharedValue} from 'react-native-reanimated'; +import type {WorkletFunction} from 'react-native-reanimated/lib/typescript/reanimated2/commonTypes'; + +type CanvasSize = { width: number; height: number; }; -type ZoomRangeProp = { +type ContentSize = { + width: number; + height: number; +}; + +type ZoomRange = { min?: number; max?: number; }; -export type {ContentSizeProp, ZoomRangeProp}; +type OnScaleChangedCallback = (zoomScale: number) => void; + +type MultiGestureCanvasProps = React.PropsWithChildren<{ + /** + * Wheter the canvas is currently active (in the screen) or not. + * Disables certain gestures and functionality + */ + isActive: boolean; + + /** Handles scale changed event */ + onScaleChanged: OnScaleChangedCallback; + + /** The width and height of the canvas. + * This is needed in order to properly scale the content in the canvas + */ + canvasSize: CanvasSize; + + /** The width and height of the content. + * This is needed in order to properly scale the content in the canvas + */ + contentSize: ContentSize; + + /** Range of zoom that can be applied to the content by pinching or double tapping. */ + zoomRange?: ZoomRange; +}>; + +type MultiGestureCanvasVariables = { + minContentScale: number; + maxContentScale: number; + isSwipingInPager: SharedValue; + zoomScale: SharedValue; + totalScale: SharedValue; + pinchScale: SharedValue; + offsetX: SharedValue; + offsetY: SharedValue; + panTranslateX: SharedValue; + panTranslateY: SharedValue; + pinchTranslateX: SharedValue; + pinchTranslateY: SharedValue; + stopAnimation: WorkletFunction<[], void>; + reset: WorkletFunction<[boolean], void>; + onTap: () => void; +}; + +export type {MultiGestureCanvasProps, CanvasSize, ContentSize, ZoomRange, OnScaleChangedCallback, MultiGestureCanvasVariables}; diff --git a/src/components/MultiGestureCanvas/usePanGesture.js b/src/components/MultiGestureCanvas/usePanGesture.ts similarity index 88% rename from src/components/MultiGestureCanvas/usePanGesture.js rename to src/components/MultiGestureCanvas/usePanGesture.ts index aec24cb2e99e..7e0aff08368f 100644 --- a/src/components/MultiGestureCanvas/usePanGesture.js +++ b/src/components/MultiGestureCanvas/usePanGesture.ts @@ -1,6 +1,8 @@ /* eslint-disable no-param-reassign */ +import type {PanGesture} from 'react-native-gesture-handler'; import {Gesture} from 'react-native-gesture-handler'; import {useDerivedValue, useSharedValue, withDecay, withSpring} from 'react-native-reanimated'; +import type {CanvasSize, ContentSize, MultiGestureCanvasVariables} from './types'; import * as MultiGestureCanvasUtils from './utils'; // This value determines how fast the pan animation should phase out @@ -8,7 +10,20 @@ import * as MultiGestureCanvasUtils from './utils'; // https://docs.swmansion.com/react-native-reanimated/docs/animations/withDecay/ const PAN_DECAY_DECELARATION = 0.9915; -const usePanGesture = ({canvasSize, contentSize, zoomScale, totalScale, offsetX, offsetY, panTranslateX, panTranslateY, isSwipingInPager, stopAnimation}) => { +type UsePanGestureProps = { + canvasSize: CanvasSize; + contentSize: ContentSize; + zoomScale: MultiGestureCanvasVariables['zoomScale']; + totalScale: MultiGestureCanvasVariables['totalScale']; + offsetX: MultiGestureCanvasVariables['offsetX']; + offsetY: MultiGestureCanvasVariables['offsetY']; + panTranslateX: MultiGestureCanvasVariables['panTranslateX']; + panTranslateY: MultiGestureCanvasVariables['panTranslateY']; + isSwipingInPager: MultiGestureCanvasVariables['isSwipingInPager']; + stopAnimation: MultiGestureCanvasVariables['stopAnimation']; +}; + +const usePanGesture = ({canvasSize, contentSize, zoomScale, totalScale, offsetX, offsetY, panTranslateX, panTranslateY, isSwipingInPager, stopAnimation}: UsePanGestureProps): PanGesture => { // The content size after fitting it to the canvas and zooming const zoomedContentWidth = useDerivedValue(() => contentSize.width * totalScale.value, [contentSize.width]); const zoomedContentHeight = useDerivedValue(() => contentSize.height * totalScale.value, [contentSize.height]); @@ -106,6 +121,7 @@ const usePanGesture = ({canvasSize, contentSize, zoomScale, totalScale, offsetX, const panGesture = Gesture.Pan() .manualActivation(true) .averageTouches(true) + // eslint-disable-next-line @typescript-eslint/naming-convention .onTouchesMove((_evt, state) => { // We only allow panning when the content is zoomed in if (zoomScale.value <= 1 || isSwipingInPager.value) { diff --git a/src/components/MultiGestureCanvas/usePinchGesture.js b/src/components/MultiGestureCanvas/usePinchGesture.ts similarity index 88% rename from src/components/MultiGestureCanvas/usePinchGesture.js rename to src/components/MultiGestureCanvas/usePinchGesture.ts index 21c5e55e0117..50c256933af5 100644 --- a/src/components/MultiGestureCanvas/usePinchGesture.js +++ b/src/components/MultiGestureCanvas/usePinchGesture.ts @@ -1,9 +1,25 @@ /* eslint-disable no-param-reassign */ import {useEffect, useState} from 'react'; +import type {PinchGesture} from 'react-native-gesture-handler'; import {Gesture} from 'react-native-gesture-handler'; import {runOnJS, useAnimatedReaction, useSharedValue, withSpring} from 'react-native-reanimated'; +import type {CanvasSize, MultiGestureCanvasVariables, OnScaleChangedCallback, ZoomRange} from './types'; import * as MultiGestureCanvasUtils from './utils'; +type UsePinchGestureProps = { + canvasSize: CanvasSize; + zoomScale: MultiGestureCanvasVariables['zoomScale']; + zoomRange: Required; + offsetX: MultiGestureCanvasVariables['offsetX']; + offsetY: MultiGestureCanvasVariables['offsetY']; + pinchTranslateX: MultiGestureCanvasVariables['pinchTranslateX']; + pinchTranslateY: MultiGestureCanvasVariables['pinchTranslateY']; + pinchScale: MultiGestureCanvasVariables['pinchScale']; + isSwipingInPager: MultiGestureCanvasVariables['isSwipingInPager']; + stopAnimation: MultiGestureCanvasVariables['stopAnimation']; + onScaleChanged: OnScaleChangedCallback; +}; + const usePinchGesture = ({ canvasSize, zoomScale, @@ -16,7 +32,7 @@ const usePinchGesture = ({ isSwipingInPager, stopAnimation, onScaleChanged, -}) => { +}: UsePinchGestureProps): PinchGesture => { // The current pinch gesture event scale const currentPinchScale = useSharedValue(1); @@ -51,7 +67,7 @@ const usePinchGesture = ({ * based on the canvas size and the current offset */ const getAdjustedFocal = MultiGestureCanvasUtils.useWorkletCallback( - (focalX, focalY) => ({ + (focalX: number, focalY: number) => ({ x: focalX - (canvasSize.width / 2 + offsetX.value), y: focalY - (canvasSize.height / 2 + offsetY.value), }), @@ -70,6 +86,7 @@ const usePinchGesture = ({ const pinchGesture = Gesture.Pinch() .enabled(pinchEnabled) + // eslint-disable-next-line @typescript-eslint/naming-convention .onTouchesDown((_evt, state) => { // We don't want to activate pinch gesture when we are swiping in the pager if (!isSwipingInPager.value) { diff --git a/src/components/MultiGestureCanvas/useTapGestures.js b/src/components/MultiGestureCanvas/useTapGestures.ts similarity index 80% rename from src/components/MultiGestureCanvas/useTapGestures.js rename to src/components/MultiGestureCanvas/useTapGestures.ts index eefe8c506b33..217981a1238d 100644 --- a/src/components/MultiGestureCanvas/useTapGestures.js +++ b/src/components/MultiGestureCanvas/useTapGestures.ts @@ -1,12 +1,42 @@ /* eslint-disable no-param-reassign */ import {useMemo} from 'react'; +import type {TapGesture} from 'react-native-gesture-handler'; import {Gesture} from 'react-native-gesture-handler'; import {runOnJS, withSpring} from 'react-native-reanimated'; +import type {CanvasSize, ContentSize, MultiGestureCanvasVariables, OnScaleChangedCallback} from './types'; import * as MultiGestureCanvasUtils from './utils'; const DOUBLE_TAP_SCALE = 3; -const useTapGestures = ({canvasSize, contentSize, minContentScale, maxContentScale, offsetX, offsetY, pinchScale, zoomScale, reset, stopAnimation, onScaleChanged, onTap}) => { +type UseTapGesturesProps = { + canvasSize: CanvasSize; + contentSize: ContentSize; + minContentScale: MultiGestureCanvasVariables['minContentScale']; + maxContentScale: MultiGestureCanvasVariables['maxContentScale']; + offsetX: MultiGestureCanvasVariables['offsetX']; + offsetY: MultiGestureCanvasVariables['offsetY']; + pinchScale: MultiGestureCanvasVariables['pinchScale']; + zoomScale: MultiGestureCanvasVariables['zoomScale']; + reset: MultiGestureCanvasVariables['reset']; + stopAnimation: MultiGestureCanvasVariables['stopAnimation']; + onScaleChanged: OnScaleChangedCallback; + onTap: MultiGestureCanvasVariables['onTap']; +}; + +const useTapGestures = ({ + canvasSize, + contentSize, + minContentScale, + maxContentScale, + offsetX, + offsetY, + pinchScale, + zoomScale, + reset, + stopAnimation, + onScaleChanged, + onTap, +}: UseTapGesturesProps): {singleTapGesture: TapGesture; doubleTapGesture: TapGesture} => { // The content size after scaling it with minimum scale to fit the content into the canvas const scaledContentWidth = useMemo(() => contentSize.width * minContentScale, [contentSize.width, minContentScale]); const scaledContentHeight = useMemo(() => contentSize.height * minContentScale, [contentSize.height, minContentScale]); @@ -15,7 +45,7 @@ const useTapGestures = ({canvasSize, contentSize, minContentScale, maxContentSca const doubleTapScale = useMemo(() => Math.max(DOUBLE_TAP_SCALE, maxContentScale / minContentScale), [maxContentScale, minContentScale]); const zoomToCoordinates = MultiGestureCanvasUtils.useWorkletCallback( - (focalX, focalY) => { + (focalX: number, focalY: number) => { 'worklet'; stopAnimation(); @@ -111,6 +141,7 @@ const useTapGestures = ({canvasSize, contentSize, minContentScale, maxContentSca .onBegin(() => { stopAnimation(); }) + // eslint-disable-next-line @typescript-eslint/naming-convention .onFinalize((_evt, success) => { if (!success || !onTap) { return; diff --git a/src/components/MultiGestureCanvas/utils.ts b/src/components/MultiGestureCanvas/utils.ts index 66fea9694180..0f081568462e 100644 --- a/src/components/MultiGestureCanvas/utils.ts +++ b/src/components/MultiGestureCanvas/utils.ts @@ -1,4 +1,5 @@ import {useCallback} from 'react'; +import type {WorkletFunction} from 'react-native-reanimated/lib/typescript/reanimated2/commonTypes'; // The spring config is used to determine the physics of the spring animation // Details and a playground for testing different configs can be found at @@ -37,11 +38,14 @@ function clamp(value: number, lowerBound: number, upperBound: number) { * @returns */ // eslint-disable-next-line @typescript-eslint/ban-types -function useWorkletCallback(callback: Parameters>[0], deps: Parameters[1] = []): T { +function useWorkletCallback( + callback: Parameters ReturnValue>>[0], + deps: Parameters>[1] = [], +): WorkletFunction { 'worklet'; // eslint-disable-next-line react-hooks/exhaustive-deps - return useCallback(callback, deps); + return useCallback<(...args: Args) => ReturnValue>(callback, deps) as WorkletFunction; } export {SPRING_CONFIG, zoomScaleBounceFactors, clamp, useWorkletCallback}; From 77ef44201944c63e0e608ec9c0d3d491f8e7cfb0 Mon Sep 17 00:00:00 2001 From: Kamil Owczarz Date: Fri, 5 Jan 2024 19:12:56 +0100 Subject: [PATCH 155/580] 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 156/580] 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 157/580] 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 158/580] 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 159/580] 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 160/580] 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 945fd35a0e06ca84aafe587b5475ac5663847bd9 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Sun, 7 Jan 2024 12:39:02 +0100 Subject: [PATCH 161/580] move types --- .../MultiGestureCanvas/getCanvasFitScale.ts | 13 +--- src/components/MultiGestureCanvas/index.tsx | 68 ++++++++++++------- src/components/MultiGestureCanvas/types.ts | 26 +------ src/components/MultiGestureCanvas/utils.ts | 2 +- 4 files changed, 49 insertions(+), 60 deletions(-) diff --git a/src/components/MultiGestureCanvas/getCanvasFitScale.ts b/src/components/MultiGestureCanvas/getCanvasFitScale.ts index e3e402fb066b..8fbb72e1f294 100644 --- a/src/components/MultiGestureCanvas/getCanvasFitScale.ts +++ b/src/components/MultiGestureCanvas/getCanvasFitScale.ts @@ -1,13 +1,6 @@ -type GetCanvasFitScale = (props: { - canvasSize: { - width: number; - height: number; - }; - contentSize: { - width: number; - height: number; - }; -}) => {scaleX: number; scaleY: number; minScale: number; maxScale: number}; +import type {CanvasSize, ContentSize} from './types'; + +type GetCanvasFitScale = (props: {canvasSize: CanvasSize; contentSize: ContentSize}) => {scaleX: number; scaleY: number; minScale: number; maxScale: number}; const getCanvasFitScale: GetCanvasFitScale = ({canvasSize, contentSize}) => { const scaleX = canvasSize.width / contentSize.width; diff --git a/src/components/MultiGestureCanvas/index.tsx b/src/components/MultiGestureCanvas/index.tsx index efe75f21af96..8b86ca5660b5 100644 --- a/src/components/MultiGestureCanvas/index.tsx +++ b/src/components/MultiGestureCanvas/index.tsx @@ -8,38 +8,39 @@ import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; import {defaultZoomRange} from './constants'; import getCanvasFitScale from './getCanvasFitScale'; -import type {ContentSize, MultiGestureCanvasProps, ZoomRange} from './types'; +import type {CanvasSize, ContentSize, OnScaleChangedCallback, ZoomRange} from './types'; import usePanGesture from './usePanGesture'; import usePinchGesture from './usePinchGesture'; import useTapGestures from './useTapGestures'; import * as MultiGestureCanvasUtils from './utils'; -type Props = { - contentSize?: ContentSize; - zoomRange?: ZoomRange; -}; -type PropsWithDefault = { +type MultiGestureCanvasProps = React.PropsWithChildren<{ + /** + * Wheter the canvas is currently active (in the screen) or not. + * Disables certain gestures and functionality + */ + isActive: boolean; + + /** Handles scale changed event */ + onScaleChanged: OnScaleChangedCallback; + + /** The width and height of the canvas. + * This is needed in order to properly scale the content in the canvas + */ + canvasSize: CanvasSize; + + /** The width and height of the content. + * This is needed in order to properly scale the content in the canvas + */ contentSize: ContentSize; - zoomRange: Required; -}; -const getDeepDefaultProps = ({contentSize: contentSizeProp, zoomRange: zoomRangeProp}: Props): PropsWithDefault => { - const contentSize = { - width: contentSizeProp?.width ?? 1, - height: contentSizeProp?.height ?? 1, - }; - - const zoomRange = { - min: zoomRangeProp?.min ?? defaultZoomRange.min, - max: zoomRangeProp?.max ?? defaultZoomRange.max, - }; - - return {contentSize, zoomRange}; -}; - -function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged: onScaleChangedProp, children, ...props}: MultiGestureCanvasProps) { + + /** Range of zoom that can be applied to the content by pinching or double tapping. */ + zoomRange?: ZoomRange; +}>; + +function MultiGestureCanvas({canvasSize, contentSize: contentSizeProp, zoomRange: zoomRangeProp, isActive = true, onScaleChanged: onScaleChangedProp, children}: MultiGestureCanvasProps) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); - const {contentSize, zoomRange} = getDeepDefaultProps({contentSize: props.contentSize, zoomRange: props.zoomRange}); const pagerRefFallback = useRef(null); const shouldPagerScrollFallback = useSharedValue(false); @@ -64,6 +65,9 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged: onScal [attachmentCarouselPagerContext, isSwipingInPagerFallback, shouldPagerScrollFallback], ); + /** + * Calls the onScaleChanged callback from the both props and the pager context + */ const onScaleChanged = useCallback( (newScale: number) => { onScaleChangedProp(newScale); @@ -72,6 +76,22 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged: onScal [onScaleChangedContext, onScaleChangedProp], ); + const contentSize = useMemo( + () => ({ + width: contentSizeProp?.width ?? 1, + height: contentSizeProp?.height ?? 1, + }), + [contentSizeProp?.height, contentSizeProp?.width], + ); + + const zoomRange = useMemo( + () => ({ + min: zoomRangeProp?.min ?? defaultZoomRange.min, + max: zoomRangeProp?.max ?? defaultZoomRange.max, + }), + [zoomRangeProp?.max, zoomRangeProp?.min], + ); + // Based on the (original) content size and the canvas size, we calculate the horizontal and vertical scale factors // to fit the content inside the canvas // We later use the lower of the two scale factors to fit the content inside the canvas diff --git a/src/components/MultiGestureCanvas/types.ts b/src/components/MultiGestureCanvas/types.ts index 4ca0f5a1fe05..82fee9eb52a6 100644 --- a/src/components/MultiGestureCanvas/types.ts +++ b/src/components/MultiGestureCanvas/types.ts @@ -18,30 +18,6 @@ type ZoomRange = { type OnScaleChangedCallback = (zoomScale: number) => void; -type MultiGestureCanvasProps = React.PropsWithChildren<{ - /** - * Wheter the canvas is currently active (in the screen) or not. - * Disables certain gestures and functionality - */ - isActive: boolean; - - /** Handles scale changed event */ - onScaleChanged: OnScaleChangedCallback; - - /** The width and height of the canvas. - * This is needed in order to properly scale the content in the canvas - */ - canvasSize: CanvasSize; - - /** The width and height of the content. - * This is needed in order to properly scale the content in the canvas - */ - contentSize: ContentSize; - - /** Range of zoom that can be applied to the content by pinching or double tapping. */ - zoomRange?: ZoomRange; -}>; - type MultiGestureCanvasVariables = { minContentScale: number; maxContentScale: number; @@ -60,4 +36,4 @@ type MultiGestureCanvasVariables = { onTap: () => void; }; -export type {MultiGestureCanvasProps, CanvasSize, ContentSize, ZoomRange, OnScaleChangedCallback, MultiGestureCanvasVariables}; +export type {CanvasSize, ContentSize, ZoomRange, OnScaleChangedCallback, MultiGestureCanvasVariables}; diff --git a/src/components/MultiGestureCanvas/utils.ts b/src/components/MultiGestureCanvas/utils.ts index 0f081568462e..26e814313f8f 100644 --- a/src/components/MultiGestureCanvas/utils.ts +++ b/src/components/MultiGestureCanvas/utils.ts @@ -40,7 +40,7 @@ function clamp(value: number, lowerBound: number, upperBound: number) { // eslint-disable-next-line @typescript-eslint/ban-types function useWorkletCallback( callback: Parameters ReturnValue>>[0], - deps: Parameters>[1] = [], + deps: Parameters ReturnValue>>[1] = [], ): WorkletFunction { 'worklet'; From 369d61d25c0206a0a32e6832cd863fdeecbe3817 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Sun, 7 Jan 2024 13:50:03 +0100 Subject: [PATCH 162/580] migrate Lightbox --- src/components/{Lightbox.js => Lightbox.tsx} | 165 ++++++++++--------- 1 file changed, 86 insertions(+), 79 deletions(-) rename src/components/{Lightbox.js => Lightbox.tsx} (65%) diff --git a/src/components/Lightbox.js b/src/components/Lightbox.tsx similarity index 65% rename from src/components/Lightbox.js rename to src/components/Lightbox.tsx index 078cb13d42e5..bffffcf8a8cf 100644 --- a/src/components/Lightbox.js +++ b/src/components/Lightbox.tsx @@ -1,82 +1,93 @@ -/* eslint-disable es/no-optional-chaining */ -import PropTypes from 'prop-types'; import React, {useCallback, useEffect, useMemo, useState} from 'react'; +import type {ImageSourcePropType, LayoutChangeEvent, NativeSyntheticEvent, StyleProp, ViewStyle} from 'react-native'; import {ActivityIndicator, PixelRatio, StyleSheet, View} from 'react-native'; import useStyleUtils from '@hooks/useStyleUtils'; -import * as AttachmentsPropTypes from './Attachments/propTypes'; import Image from './Image'; -import MultiGestureCanvas from './MultiGestureCanvas'; -import getCanvasFitScale from './MultiGestureCanvas/utils'; +import MultiGestureCanvas, {defaultZoomRange} from './MultiGestureCanvas'; +import type {OnScaleChangedCallback, ZoomRange} from './MultiGestureCanvas/types'; +import * as MultiGestureCanvasUtils from './MultiGestureCanvas/utils'; // Increase/decrease this number to change the number of concurrent lightboxes // The more concurrent lighboxes, the worse performance gets (especially on low-end devices) // -1 means unlimited const NUMBER_OF_CONCURRENT_LIGHTBOXES = 3; +const DEFAULT_IMAGE_SIZE = 200; +const DEFAULT_IMAGE_DIMENSIONS = { + width: DEFAULT_IMAGE_SIZE, + height: DEFAULT_IMAGE_SIZE, +}; -const cachedDimensions = new Map(); - -/** - * On the native layer, we use a image library to handle zoom functionality - */ -const propTypes = { - // TODO: Add TS types for zoom range - // ...zoomRangePropTypes, +type LightboxImageDimension = { + lightboxSize?: { + width: number; + height: number; + }; + fallbackSize?: { + width: number; + height: number; + }; +}; - /** Triggers whenever the zoom scale changes */ - onScaleChanged: PropTypes.func, +const cachedDimensions = new Map(); - /** Function for handle on press */ - onPress: PropTypes.func, +type ImageOnLoadEvent = NativeSyntheticEvent<{width: number; height: number}>; - /** Handles errors while displaying the image */ - onError: PropTypes.func, +type LightboxProps = { + /** Whether source url requires authentication */ + isAuthTokenRequired: boolean; /** URL to full-sized attachment, SVG function, or numeric static image on native platforms */ - source: AttachmentsPropTypes.attachmentSourcePropType.isRequired, + source: ImageSourcePropType; - /** Whether source url requires authentication */ - isAuthTokenRequired: PropTypes.bool, + /** Triggers whenever the zoom scale changes */ + onScaleChanged: OnScaleChangedCallback; - /** Whether the Lightbox is used within a carousel component and there are other sibling elements */ - hasSiblingCarouselItems: PropTypes.bool, + /** Handles errors while displaying the image */ + onError: () => void; + + /** Additional styles to add to the component */ + style: StyleProp; /** The index of the carousel item */ - index: PropTypes.number, + index: number; /** The index of the currently active carousel item */ - activeIndex: PropTypes.number, + activeIndex: number; - /** Additional styles to add to the component */ - style: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.object), PropTypes.object]), -}; + /** Whether the Lightbox is used within a carousel component and there are other sibling elements */ + hasSiblingCarouselItems: boolean; -const defaultProps = { - // TODO: Add TS default values - // ...zoomRangeDefaultProps, - - isAuthTokenRequired: false, - index: 0, - activeIndex: 0, - hasSiblingCarouselItems: false, - onScaleChanged: () => {}, - onPress: () => {}, - onError: () => {}, - style: {}, + /** Range of zoom that can be applied to the content by pinching or double tapping. */ + zoomRange: ZoomRange; }; -const DEFAULT_IMAGE_SIZE = 200; - -function Lightbox({isAuthTokenRequired, source, onScaleChanged, onPress, onError, style, index, activeIndex, hasSiblingCarouselItems, zoomRange}) { +/** + * On the native layer, we use a image library to handle zoom functionality + */ +function Lightbox({ + isAuthTokenRequired = false, + source, + onScaleChanged, + onError, + style, + index = 0, + activeIndex = 0, + hasSiblingCarouselItems = false, + zoomRange = defaultZoomRange, +}: LightboxProps) { const StyleUtils = useStyleUtils(); const [containerSize, setContainerSize] = useState({width: 0, height: 0}); const isContainerLoaded = containerSize.width !== 0 && containerSize.height !== 0; - const [imageDimensions, _setImageDimensions] = useState(() => cachedDimensions.get(source)); - const setImageDimensions = (newDimensions) => { - _setImageDimensions(newDimensions); - cachedDimensions.set(source, newDimensions); - }; + const [imageDimensions, setInternalImageDimensions] = useState(() => cachedDimensions.get(source)); + const setImageDimensions = useCallback( + (newDimensions: LightboxImageDimension) => { + setInternalImageDimensions(newDimensions); + cachedDimensions.set(source, newDimensions); + }, + [source], + ); const isItemActive = index === activeIndex; const [isActive, setActive] = useState(isItemActive); @@ -86,8 +97,9 @@ function Lightbox({isAuthTokenRequired, source, onScaleChanged, onPress, onError const [isFallbackVisible, setFallbackVisible] = useState(isInactiveCarouselItem); const [isFallbackLoaded, setFallbackLoaded] = useState(false); - const isLightboxLoaded = imageDimensions?.lightboxSize != null; const isLightboxInRange = useMemo(() => { + // @ts-expect-error TS will throw an error here because -1 and the constantly set number have no overlap + // We can safely ignore this error, because we might change the value in the future if (NUMBER_OF_CONCURRENT_LIGHTBOXES === -1) { return true; } @@ -96,6 +108,7 @@ function Lightbox({isAuthTokenRequired, source, onScaleChanged, onPress, onError const indexOutOfRange = index > activeIndex + indexCanvasOffset || index < activeIndex - indexCanvasOffset; return !indexOutOfRange; }, [activeIndex, index]); + const [isLightboxLoaded, setLightboxLoaded] = useState(false); const isLightboxVisible = isLightboxInRange && (isActive || isLightboxLoaded || isFallbackLoaded); // If the fallback image is currently visible, we want to hide the Lightbox until the fallback gets hidden, @@ -107,10 +120,20 @@ function Lightbox({isAuthTokenRequired, source, onScaleChanged, onPress, onError const isLoading = isActive && (!isContainerLoaded || !isImageLoaded); const updateCanvasSize = useCallback( - ({nativeEvent}) => setContainerSize({width: PixelRatio.roundToNearestPixel(nativeEvent.layout.width), height: PixelRatio.roundToNearestPixel(nativeEvent.layout.height)}), + ({nativeEvent}: LayoutChangeEvent) => + setContainerSize({width: PixelRatio.roundToNearestPixel(nativeEvent.layout.width), height: PixelRatio.roundToNearestPixel(nativeEvent.layout.height)}), [], ); + const updateContentSize = useCallback( + (e: ImageOnLoadEvent) => { + const width = (e.nativeEvent?.width || 0) * PixelRatio.get(); + const height = (e.nativeEvent?.height || 0) * PixelRatio.get(); + setImageDimensions({...imageDimensions, lightboxSize: {width, height}}); + }, + [imageDimensions, setImageDimensions], + ); + // We delay setting a page to active state by a (few) millisecond(s), // to prevent the image transformer from flashing while still rendering // Instead, we show the fallback image while the image transformer is loading the image @@ -154,15 +177,15 @@ function Lightbox({isAuthTokenRequired, source, onScaleChanged, onPress, onError const fallbackSize = useMemo(() => { if (!hasSiblingCarouselItems || (imageDimensions?.lightboxSize == null && imageDimensions?.fallbackSize == null) || containerSize.width === 0 || containerSize.height === 0) { - return { - width: DEFAULT_IMAGE_SIZE, - height: DEFAULT_IMAGE_SIZE, - }; + return DEFAULT_IMAGE_DIMENSIONS; } - const imageSize = imageDimensions.lightboxSize || imageDimensions.fallbackSize; + // If the lightbox size is null, we know that fallback size must not be null, because otherwise we would have returned early + // TypeScript doesn't recognize that, so we need to use the non-null assertion operator + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const imageSize = imageDimensions?.lightboxSize ?? imageDimensions.fallbackSize!; - const {minScale} = getCanvasFitScale({canvasSize: containerSize, contentSize: imageSize}); + const {minScale} = MultiGestureCanvasUtils.getCanvasFitScale({canvasSize: containerSize, contentSize: imageSize}); return { width: PixelRatio.roundToNearestPixel(imageSize.width * minScale), @@ -172,32 +195,27 @@ function Lightbox({isAuthTokenRequired, source, onScaleChanged, onPress, onError return ( {isContainerLoaded && ( <> - {isLightboxVisible && ( + {isLightboxVisible && imageDimensions?.lightboxSize != null && ( setImageLoaded(true)} - onLoad={(e) => { - const width = (e.nativeEvent?.width || 0) * PixelRatio.get(); - const height = (e.nativeEvent?.height || 0) * PixelRatio.get(); - setImageDimensions({...imageDimensions, lightboxSize: {width, height}}); - }} + onLoad={updateContentSize} + onLoadEnd={() => setLightboxLoaded(true)} /> @@ -211,17 +229,8 @@ function Lightbox({isAuthTokenRequired, source, onScaleChanged, onPress, onError resizeMode="contain" style={fallbackSize} isAuthTokenRequired={isAuthTokenRequired} + onLoad={updateContentSize} onLoadEnd={() => setFallbackLoaded(true)} - onLoad={(e) => { - const width = (e.nativeEvent?.width || 0) * PixelRatio.get(); - const height = (e.nativeEvent?.height || 0) * PixelRatio.get(); - - if (imageDimensions?.lightboxSize != null) { - return; - } - - setImageDimensions({...imageDimensions, fallbackSize: {width, height}}); - }} /> )} @@ -239,8 +248,6 @@ function Lightbox({isAuthTokenRequired, source, onScaleChanged, onPress, onError ); } -Lightbox.propTypes = propTypes; -Lightbox.defaultProps = defaultProps; Lightbox.displayName = 'Lightbox'; export default Lightbox; From d209ac2dd8377626261856e980790edf0e7970ff Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Sun, 7 Jan 2024 13:52:44 +0100 Subject: [PATCH 163/580] restructure --- .../MultiGestureCanvas/getCanvasFitScale.ts | 15 --------------- src/components/MultiGestureCanvas/index.tsx | 3 +-- src/components/MultiGestureCanvas/types.ts | 5 +++++ src/components/MultiGestureCanvas/utils.ts | 15 ++++++++++++++- 4 files changed, 20 insertions(+), 18 deletions(-) delete mode 100644 src/components/MultiGestureCanvas/getCanvasFitScale.ts diff --git a/src/components/MultiGestureCanvas/getCanvasFitScale.ts b/src/components/MultiGestureCanvas/getCanvasFitScale.ts deleted file mode 100644 index 8fbb72e1f294..000000000000 --- a/src/components/MultiGestureCanvas/getCanvasFitScale.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type {CanvasSize, ContentSize} from './types'; - -type GetCanvasFitScale = (props: {canvasSize: CanvasSize; contentSize: ContentSize}) => {scaleX: number; scaleY: number; minScale: number; maxScale: number}; - -const getCanvasFitScale: GetCanvasFitScale = ({canvasSize, contentSize}) => { - const scaleX = canvasSize.width / contentSize.width; - const scaleY = canvasSize.height / contentSize.height; - - const minScale = Math.min(scaleX, scaleY); - const maxScale = Math.max(scaleX, scaleY); - - return {scaleX, scaleY, minScale, maxScale}; -}; - -export default getCanvasFitScale; diff --git a/src/components/MultiGestureCanvas/index.tsx b/src/components/MultiGestureCanvas/index.tsx index 8b86ca5660b5..778b680cb302 100644 --- a/src/components/MultiGestureCanvas/index.tsx +++ b/src/components/MultiGestureCanvas/index.tsx @@ -7,7 +7,6 @@ import AttachmentCarouselPagerContext from '@components/Attachments/AttachmentCa import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; import {defaultZoomRange} from './constants'; -import getCanvasFitScale from './getCanvasFitScale'; import type {CanvasSize, ContentSize, OnScaleChangedCallback, ZoomRange} from './types'; import usePanGesture from './usePanGesture'; import usePinchGesture from './usePinchGesture'; @@ -95,7 +94,7 @@ function MultiGestureCanvas({canvasSize, contentSize: contentSizeProp, zoomRange // Based on the (original) content size and the canvas size, we calculate the horizontal and vertical scale factors // to fit the content inside the canvas // We later use the lower of the two scale factors to fit the content inside the canvas - const {minScale: minContentScale, maxScale: maxContentScale} = useMemo(() => getCanvasFitScale({canvasSize, contentSize}), [canvasSize, contentSize]); + const {minScale: minContentScale, maxScale: maxContentScale} = useMemo(() => MultiGestureCanvasUtils.getCanvasFitScale({canvasSize, contentSize}), [canvasSize, contentSize]); const zoomScale = useSharedValue(1); diff --git a/src/components/MultiGestureCanvas/types.ts b/src/components/MultiGestureCanvas/types.ts index 82fee9eb52a6..0309a6cbcdfc 100644 --- a/src/components/MultiGestureCanvas/types.ts +++ b/src/components/MultiGestureCanvas/types.ts @@ -1,23 +1,28 @@ import type {SharedValue} from 'react-native-reanimated'; import type {WorkletFunction} from 'react-native-reanimated/lib/typescript/reanimated2/commonTypes'; +/** Dimensions of the canvas rendered by the MultiGestureCanvas */ type CanvasSize = { width: number; height: number; }; +/** Dimensions of the content passed to the MultiGestureCanvas */ type ContentSize = { width: number; height: number; }; +/** Range of zoom that can be applied to the content by pinching or double tapping. */ type ZoomRange = { min?: number; max?: number; }; +/** Triggered whenever the scale of the MultiGestureCanvas changes */ type OnScaleChangedCallback = (zoomScale: number) => void; +/** Types used of variables used within the MultiGestureCanvas component and it's hooks */ type MultiGestureCanvasVariables = { minContentScale: number; maxContentScale: number; diff --git a/src/components/MultiGestureCanvas/utils.ts b/src/components/MultiGestureCanvas/utils.ts index 26e814313f8f..4e377b3702d9 100644 --- a/src/components/MultiGestureCanvas/utils.ts +++ b/src/components/MultiGestureCanvas/utils.ts @@ -1,5 +1,6 @@ import {useCallback} from 'react'; import type {WorkletFunction} from 'react-native-reanimated/lib/typescript/reanimated2/commonTypes'; +import type {CanvasSize, ContentSize} from './types'; // The spring config is used to determine the physics of the spring animation // Details and a playground for testing different configs can be found at @@ -48,4 +49,16 @@ function useWorkletCallback( return useCallback<(...args: Args) => ReturnValue>(callback, deps) as WorkletFunction; } -export {SPRING_CONFIG, zoomScaleBounceFactors, clamp, useWorkletCallback}; +type GetCanvasFitScale = (props: {canvasSize: CanvasSize; contentSize: ContentSize}) => {scaleX: number; scaleY: number; minScale: number; maxScale: number}; + +const getCanvasFitScale: GetCanvasFitScale = ({canvasSize, contentSize}) => { + const scaleX = canvasSize.width / contentSize.width; + const scaleY = canvasSize.height / contentSize.height; + + const minScale = Math.min(scaleX, scaleY); + const maxScale = Math.max(scaleX, scaleY); + + return {scaleX, scaleY, minScale, maxScale}; +}; + +export {SPRING_CONFIG, zoomScaleBounceFactors, clamp, useWorkletCallback, getCanvasFitScale}; From 24d34a57cb1c8a6277fa21755d1de4553c381fbe Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Sun, 7 Jan 2024 16:20:19 +0100 Subject: [PATCH 164/580] add back propTypes --- .../MultiGestureCanvas/propTypes.js | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 src/components/MultiGestureCanvas/propTypes.js diff --git a/src/components/MultiGestureCanvas/propTypes.js b/src/components/MultiGestureCanvas/propTypes.js new file mode 100644 index 000000000000..189e661a702c --- /dev/null +++ b/src/components/MultiGestureCanvas/propTypes.js @@ -0,0 +1,65 @@ +import PropTypes from 'prop-types'; + +const defaultZoomRange = { + min: 1, + max: 20, +}; + +const zoomRangePropTypes = { + /** Range of zoom that can be applied to the content by pinching or double tapping. */ + zoomRange: PropTypes.shape({ + min: PropTypes.number, + max: PropTypes.number, + }), +}; + +const zoomRangeDefaultProps = { + zoomRange: { + min: defaultZoomRange.min, + max: defaultZoomRange.max, + }, +}; + +const multiGestureCanvasPropTypes = { + ...zoomRangePropTypes, + + /** + * Wheter the canvas is currently active (in the screen) or not. + * Disables certain gestures and functionality + */ + isActive: PropTypes.bool, + + /** Handles scale changed event */ + onScaleChanged: PropTypes.func, + + /** + * The width and height of the canvas. + * This is needed in order to properly scale the content in the canvas + */ + canvasSize: PropTypes.shape({ + width: PropTypes.number.isRequired, + height: PropTypes.number.isRequired, + }).isRequired, + + /** + * The width and height of the content. + * This is needed in order to properly scale the content in the canvas + */ + contentSize: PropTypes.shape({ + width: PropTypes.number, + height: PropTypes.number, + }), + + /** Content that should be transformed inside the canvas (images, pdf, ...) */ + children: PropTypes.node.isRequired, +}; + +const multiGestureCanvasDefaultProps = { + isActive: true, + onScaleChanged: () => undefined, + contentSize: undefined, + contentScaling: undefined, + zoomRange: undefined, +}; + +export {defaultZoomRange, zoomRangePropTypes, zoomRangeDefaultProps, multiGestureCanvasPropTypes, multiGestureCanvasDefaultProps}; From 083f37ccea17b3e2cdc52336104c98d4fe2276eb Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Sun, 7 Jan 2024 16:20:37 +0100 Subject: [PATCH 165/580] simplify props --- src/components/MultiGestureCanvas/index.tsx | 24 ++++++++++----------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/components/MultiGestureCanvas/index.tsx b/src/components/MultiGestureCanvas/index.tsx index 778b680cb302..764927f4f390 100644 --- a/src/components/MultiGestureCanvas/index.tsx +++ b/src/components/MultiGestureCanvas/index.tsx @@ -6,6 +6,7 @@ import Animated, {cancelAnimation, runOnUI, useAnimatedReaction, useAnimatedStyl import AttachmentCarouselPagerContext from '@components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; +import type ChildrenProps from '@src/types/utils/ChildrenProps'; import {defaultZoomRange} from './constants'; import type {CanvasSize, ContentSize, OnScaleChangedCallback, ZoomRange} from './types'; import usePanGesture from './usePanGesture'; @@ -13,7 +14,7 @@ import usePinchGesture from './usePinchGesture'; import useTapGestures from './useTapGestures'; import * as MultiGestureCanvasUtils from './utils'; -type MultiGestureCanvasProps = React.PropsWithChildren<{ +type MultiGestureCanvasProps = ChildrenProps & { /** * Wheter the canvas is currently active (in the screen) or not. * Disables certain gestures and functionality @@ -35,9 +36,16 @@ type MultiGestureCanvasProps = React.PropsWithChildren<{ /** Range of zoom that can be applied to the content by pinching or double tapping. */ zoomRange?: ZoomRange; -}>; - -function MultiGestureCanvas({canvasSize, contentSize: contentSizeProp, zoomRange: zoomRangeProp, isActive = true, onScaleChanged: onScaleChangedProp, children}: MultiGestureCanvasProps) { +}; + +function MultiGestureCanvas({ + canvasSize, + contentSize = {width: 1, height: 1}, + zoomRange: zoomRangeProp, + isActive = true, + onScaleChanged: onScaleChangedProp, + children, +}: MultiGestureCanvasProps) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); @@ -75,14 +83,6 @@ function MultiGestureCanvas({canvasSize, contentSize: contentSizeProp, zoomRange [onScaleChangedContext, onScaleChangedProp], ); - const contentSize = useMemo( - () => ({ - width: contentSizeProp?.width ?? 1, - height: contentSizeProp?.height ?? 1, - }), - [contentSizeProp?.height, contentSizeProp?.width], - ); - const zoomRange = useMemo( () => ({ min: zoomRangeProp?.min ?? defaultZoomRange.min, From 595337073b9c342770f599f496c0fcfeb1e38bce Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Sun, 7 Jan 2024 17:14:24 +0100 Subject: [PATCH 166/580] improve Lightbox --- src/components/ImageView/index.native.js | 2 +- src/components/Lightbox.tsx | 140 +++++++++----------- src/components/MultiGestureCanvas/index.tsx | 2 +- 3 files changed, 63 insertions(+), 81 deletions(-) diff --git a/src/components/ImageView/index.native.js b/src/components/ImageView/index.native.js index 98349b213aa5..a94842b35219 100644 --- a/src/components/ImageView/index.native.js +++ b/src/components/ImageView/index.native.js @@ -31,7 +31,7 @@ function ImageView({isAuthTokenRequired, url, onScaleChanged, onPress, style, zo return ( (); +const cachedImageDimensions = new Map(); -type ImageOnLoadEvent = NativeSyntheticEvent<{width: number; height: number}>; +type ImageOnLoadEvent = NativeSyntheticEvent; type LightboxProps = { /** Whether source url requires authentication */ isAuthTokenRequired: boolean; - /** URL to full-sized attachment, SVG function, or numeric static image on native platforms */ - source: ImageSourcePropType; + /** URI to full-sized attachment, SVG function, or numeric static image on native platforms */ + uri: string; /** Triggers whenever the zoom scale changes */ onScaleChanged: OnScaleChangedCallback; @@ -66,7 +55,7 @@ type LightboxProps = { */ function Lightbox({ isAuthTokenRequired = false, - source, + uri, onScaleChanged, onError, style, @@ -77,25 +66,49 @@ function Lightbox({ }: LightboxProps) { const StyleUtils = useStyleUtils(); - const [containerSize, setContainerSize] = useState({width: 0, height: 0}); - const isContainerLoaded = containerSize.width !== 0 && containerSize.height !== 0; + const [canvasSize, setCanvasSize] = useState({width: 0, height: 0}); + const isCanvasLoaded = canvasSize.width !== 0 && canvasSize.height !== 0; + const updateCanvasSize = useCallback( + ({ + nativeEvent: { + layout: {width, height}, + }, + }: LayoutChangeEvent) => setCanvasSize({width: PixelRatio.roundToNearestPixel(width), height: PixelRatio.roundToNearestPixel(height)}), + [], + ); - const [imageDimensions, setInternalImageDimensions] = useState(() => cachedDimensions.get(source)); - const setImageDimensions = useCallback( - (newDimensions: LightboxImageDimension) => { - setInternalImageDimensions(newDimensions); - cachedDimensions.set(source, newDimensions); + const [contentSize, setInternalContentSize] = useState(() => cachedImageDimensions.get(uri)); + const setContentSize = useCallback( + (newContentSize: ContentSize | undefined) => { + setInternalContentSize(newContentSize); + cachedImageDimensions.set(uri, newContentSize); }, - [source], + [uri], + ); + const updateContentSize = useCallback( + ({nativeEvent: {width, height}}: ImageOnLoadEvent) => setContentSize({width: width * PixelRatio.get(), height: height * PixelRatio.get()}), + [setContentSize], ); + const contentLoaded = contentSize != null; const isItemActive = index === activeIndex; const [isActive, setActive] = useState(isItemActive); - const [isImageLoaded, setImageLoaded] = useState(false); const isInactiveCarouselItem = hasSiblingCarouselItems && !isActive; const [isFallbackVisible, setFallbackVisible] = useState(isInactiveCarouselItem); - const [isFallbackLoaded, setFallbackLoaded] = useState(false); + const [isFallbackImageLoaded, setFallbackImageLoaded] = useState(false); + const fallbackSize = useMemo(() => { + if (!hasSiblingCarouselItems || contentSize == null || canvasSize.width === 0 || canvasSize.height === 0) { + return DEFAULT_IMAGE_DIMENSIONS; + } + + const {minScale} = MultiGestureCanvasUtils.getCanvasFitScale({canvasSize, contentSize}); + + return { + width: PixelRatio.roundToNearestPixel(contentSize.width * minScale), + height: PixelRatio.roundToNearestPixel(contentSize.height * minScale), + }; + }, [canvasSize, hasSiblingCarouselItems, contentSize]); const isLightboxInRange = useMemo(() => { // @ts-expect-error TS will throw an error here because -1 and the constantly set number have no overlap @@ -108,31 +121,17 @@ function Lightbox({ const indexOutOfRange = index > activeIndex + indexCanvasOffset || index < activeIndex - indexCanvasOffset; return !indexOutOfRange; }, [activeIndex, index]); - const [isLightboxLoaded, setLightboxLoaded] = useState(false); - const isLightboxVisible = isLightboxInRange && (isActive || isLightboxLoaded || isFallbackLoaded); + const [isLightboxImageLoaded, setLightboxImageLoaded] = useState(false); + const isLightboxVisible = isLightboxInRange && (isActive || isLightboxImageLoaded || isFallbackImageLoaded); // If the fallback image is currently visible, we want to hide the Lightbox until the fallback gets hidden, // so that we don't see two overlapping images at the same time. + // We cannot NOT render it, because we need to render the Lightbox to get the correct dimensions for the fallback image. // If there the Lightbox is not used within a carousel, we don't need to hide the Lightbox, // because it's only going to be rendered after the fallback image is hidden. const shouldHideLightbox = hasSiblingCarouselItems && isFallbackVisible; - const isLoading = isActive && (!isContainerLoaded || !isImageLoaded); - - const updateCanvasSize = useCallback( - ({nativeEvent}: LayoutChangeEvent) => - setContainerSize({width: PixelRatio.roundToNearestPixel(nativeEvent.layout.width), height: PixelRatio.roundToNearestPixel(nativeEvent.layout.height)}), - [], - ); - - const updateContentSize = useCallback( - (e: ImageOnLoadEvent) => { - const width = (e.nativeEvent?.width || 0) * PixelRatio.get(); - const height = (e.nativeEvent?.height || 0) * PixelRatio.get(); - setImageDimensions({...imageDimensions, lightboxSize: {width, height}}); - }, - [imageDimensions, setImageDimensions], - ); + const isLoading = isActive && (!isCanvasLoaded || !contentLoaded); // We delay setting a page to active state by a (few) millisecond(s), // to prevent the image transformer from flashing while still rendering @@ -149,8 +148,8 @@ function Lightbox({ if (isLightboxVisible) { return; } - setImageLoaded(false); - }, [isLightboxVisible]); + setContentSize(undefined); + }, [isLightboxVisible, setContentSize]); useEffect(() => { if (!hasSiblingCarouselItems) { @@ -158,64 +157,47 @@ function Lightbox({ } if (isActive) { - if (isImageLoaded && isFallbackVisible) { + if (contentLoaded && isFallbackVisible) { // We delay hiding the fallback image while image transformer is still rendering setTimeout(() => { setFallbackVisible(false); - setFallbackLoaded(false); + setFallbackImageLoaded(false); }, 100); } } else { - if (isLightboxVisible && isLightboxLoaded) { + if (isLightboxVisible && isLightboxImageLoaded) { return; } // Show fallback when the image goes out of focus or when the image is loading setFallbackVisible(true); } - }, [hasSiblingCarouselItems, isActive, isImageLoaded, isFallbackVisible, isLightboxLoaded, isLightboxVisible]); - - const fallbackSize = useMemo(() => { - if (!hasSiblingCarouselItems || (imageDimensions?.lightboxSize == null && imageDimensions?.fallbackSize == null) || containerSize.width === 0 || containerSize.height === 0) { - return DEFAULT_IMAGE_DIMENSIONS; - } - - // If the lightbox size is null, we know that fallback size must not be null, because otherwise we would have returned early - // TypeScript doesn't recognize that, so we need to use the non-null assertion operator - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const imageSize = imageDimensions?.lightboxSize ?? imageDimensions.fallbackSize!; - - const {minScale} = MultiGestureCanvasUtils.getCanvasFitScale({canvasSize: containerSize, contentSize: imageSize}); - - return { - width: PixelRatio.roundToNearestPixel(imageSize.width * minScale), - height: PixelRatio.roundToNearestPixel(imageSize.height * minScale), - }; - }, [containerSize, hasSiblingCarouselItems, imageDimensions]); + }, [hasSiblingCarouselItems, isActive, isFallbackVisible, isLightboxImageLoaded, isLightboxVisible, contentLoaded]); return ( - {isContainerLoaded && ( + {isCanvasLoaded && ( <> - {isLightboxVisible && imageDimensions?.lightboxSize != null && ( + {isLightboxVisible && ( setLightboxLoaded(true)} + onLoadEnd={() => setLightboxImageLoaded(true)} /> @@ -225,12 +207,12 @@ function Lightbox({ {isFallbackVisible && ( setFallbackLoaded(true)} + onLoadEnd={() => setFallbackImageLoaded(true)} /> )} diff --git a/src/components/MultiGestureCanvas/index.tsx b/src/components/MultiGestureCanvas/index.tsx index 764927f4f390..65a14d25a492 100644 --- a/src/components/MultiGestureCanvas/index.tsx +++ b/src/components/MultiGestureCanvas/index.tsx @@ -32,7 +32,7 @@ type MultiGestureCanvasProps = ChildrenProps & { /** The width and height of the content. * This is needed in order to properly scale the content in the canvas */ - contentSize: ContentSize; + contentSize?: ContentSize; /** Range of zoom that can be applied to the content by pinching or double tapping. */ zoomRange?: ZoomRange; From 616e8cb3d4794d8f583402064b58250276951fcf Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Mon, 8 Jan 2024 09:19:52 +0100 Subject: [PATCH 167/580] remove comment --- .../AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts b/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts index 981cdb797f9f..e595b8c5c4d1 100644 --- a/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts +++ b/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts @@ -4,8 +4,6 @@ import type {SharedValue} from 'react-native-reanimated'; type AttachmentCarouselPagerContextType = { onTap: () => void; - // onSwipe: (y: number) => void; - // onSwipeSuccess: () => void; onScaleChanged: (scale: number) => void; pagerRef: React.Ref; shouldPagerScroll: SharedValue; From 3055579c8bcc22807e2dd8e652c07f096c6935ce Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Mon, 8 Jan 2024 09:26:41 +0100 Subject: [PATCH 168/580] update error supression --- src/components/Lightbox.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/components/Lightbox.tsx b/src/components/Lightbox.tsx index a4d9203fba34..74ad03bf4678 100644 --- a/src/components/Lightbox.tsx +++ b/src/components/Lightbox.tsx @@ -10,7 +10,11 @@ import * as MultiGestureCanvasUtils from './MultiGestureCanvas/utils'; // Increase/decrease this number to change the number of concurrent lightboxes // The more concurrent lighboxes, the worse performance gets (especially on low-end devices) // -1 means unlimited -const NUMBER_OF_CONCURRENT_LIGHTBOXES = 3; +// We need to define a type for this constant and therefore ignore this ESLint error, although the type is inferable, +// because otherwise TS will throw an error later in the code since "-1" and this constant have no overlap. +// We can safely ignore this error, because we might change the value in the future +// eslint-disable-next-line @typescript-eslint/no-inferrable-types +const NUMBER_OF_CONCURRENT_LIGHTBOXES: number = 3; const DEFAULT_IMAGE_SIZE = 200; const DEFAULT_IMAGE_DIMENSIONS = { width: DEFAULT_IMAGE_SIZE, @@ -111,8 +115,6 @@ function Lightbox({ }, [canvasSize, hasSiblingCarouselItems, contentSize]); const isLightboxInRange = useMemo(() => { - // @ts-expect-error TS will throw an error here because -1 and the constantly set number have no overlap - // We can safely ignore this error, because we might change the value in the future if (NUMBER_OF_CONCURRENT_LIGHTBOXES === -1) { return true; } From 0b741dd040545f2d02cf7ff14176d936f240af4b Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Mon, 8 Jan 2024 11:03:59 +0100 Subject: [PATCH 169/580] revert Lightbox changes --- src/components/Lightbox.tsx | 146 ++++++++++-------- .../MultiGestureCanvas/getCanvasFitScale.ts | 15 ++ src/components/MultiGestureCanvas/utils.ts | 15 +- 3 files changed, 98 insertions(+), 78 deletions(-) create mode 100644 src/components/MultiGestureCanvas/getCanvasFitScale.ts diff --git a/src/components/Lightbox.tsx b/src/components/Lightbox.tsx index 74ad03bf4678..1684a489c0da 100644 --- a/src/components/Lightbox.tsx +++ b/src/components/Lightbox.tsx @@ -1,11 +1,12 @@ import React, {useCallback, useEffect, useMemo, useState} from 'react'; -import type {LayoutChangeEvent, NativeSyntheticEvent, StyleProp, ViewStyle} from 'react-native'; +import type {ImageSourcePropType, LayoutChangeEvent, NativeSyntheticEvent, StyleProp, ViewStyle} from 'react-native'; import {ActivityIndicator, PixelRatio, StyleSheet, View} from 'react-native'; import useStyleUtils from '@hooks/useStyleUtils'; import Image from './Image'; import MultiGestureCanvas, {defaultZoomRange} from './MultiGestureCanvas'; import type {ContentSize, OnScaleChangedCallback, ZoomRange} from './MultiGestureCanvas/types'; import * as MultiGestureCanvasUtils from './MultiGestureCanvas/utils'; +import getCanvasFitScale from '@components/MultiGestureCanvas/getCanvasFitScale'; // Increase/decrease this number to change the number of concurrent lightboxes // The more concurrent lighboxes, the worse performance gets (especially on low-end devices) @@ -16,12 +17,13 @@ import * as MultiGestureCanvasUtils from './MultiGestureCanvas/utils'; // eslint-disable-next-line @typescript-eslint/no-inferrable-types const NUMBER_OF_CONCURRENT_LIGHTBOXES: number = 3; const DEFAULT_IMAGE_SIZE = 200; -const DEFAULT_IMAGE_DIMENSIONS = { - width: DEFAULT_IMAGE_SIZE, - height: DEFAULT_IMAGE_SIZE, -}; -const cachedImageDimensions = new Map(); +type LightboxImageDimensions = { + lightboxSize?: ContentSize; + fallbackSize?: ContentSize; +}; +} +const cachedDimensions = new Map(); type ImageOnLoadEvent = NativeSyntheticEvent; @@ -30,7 +32,7 @@ type LightboxProps = { isAuthTokenRequired: boolean; /** URI to full-sized attachment, SVG function, or numeric static image on native platforms */ - uri: string; + source: ImageSourcePropType; /** Triggers whenever the zoom scale changes */ onScaleChanged: OnScaleChangedCallback; @@ -59,7 +61,7 @@ type LightboxProps = { */ function Lightbox({ isAuthTokenRequired = false, - uri, + source, onScaleChanged, onError, style, @@ -70,50 +72,26 @@ function Lightbox({ }: LightboxProps) { const StyleUtils = useStyleUtils(); - const [canvasSize, setCanvasSize] = useState({width: 0, height: 0}); - const isCanvasLoaded = canvasSize.width !== 0 && canvasSize.height !== 0; - const updateCanvasSize = useCallback( - ({ - nativeEvent: { - layout: {width, height}, - }, - }: LayoutChangeEvent) => setCanvasSize({width: PixelRatio.roundToNearestPixel(width), height: PixelRatio.roundToNearestPixel(height)}), - [], - ); + const [containerSize, setContainerSize] = useState({width: 0, height: 0}); + const isContainerLoaded = containerSize.width !== 0 && containerSize.height !== 0; - const [contentSize, setInternalContentSize] = useState(() => cachedImageDimensions.get(uri)); - const setContentSize = useCallback( - (newContentSize: ContentSize | undefined) => { - setInternalContentSize(newContentSize); - cachedImageDimensions.set(uri, newContentSize); + const [imageDimensions, setInternalImageDimensions] = useState(() => cachedDimensions.get(source)); + const setImageDimensions = useCallback( + (newDimensions: LightboxImageDimensions) => { + setInternalImageDimensions(newDimensions); + cachedDimensions.set(source, newDimensions); }, - [uri], - ); - const updateContentSize = useCallback( - ({nativeEvent: {width, height}}: ImageOnLoadEvent) => setContentSize({width: width * PixelRatio.get(), height: height * PixelRatio.get()}), - [setContentSize], + [source], ); - const contentLoaded = contentSize != null; - const isItemActive = index === activeIndex; const [isActive, setActive] = useState(isItemActive); + const [isImageLoaded, setImageLoaded] = useState(false); const isInactiveCarouselItem = hasSiblingCarouselItems && !isActive; const [isFallbackVisible, setFallbackVisible] = useState(isInactiveCarouselItem); - const [isFallbackImageLoaded, setFallbackImageLoaded] = useState(false); - const fallbackSize = useMemo(() => { - if (!hasSiblingCarouselItems || contentSize == null || canvasSize.width === 0 || canvasSize.height === 0) { - return DEFAULT_IMAGE_DIMENSIONS; - } - - const {minScale} = MultiGestureCanvasUtils.getCanvasFitScale({canvasSize, contentSize}); - - return { - width: PixelRatio.roundToNearestPixel(contentSize.width * minScale), - height: PixelRatio.roundToNearestPixel(contentSize.height * minScale), - }; - }, [canvasSize, hasSiblingCarouselItems, contentSize]); + const [isFallbackLoaded, setFallbackLoaded] = useState(false); + const [isLightboxLoaded, setLightboxLoaded] = useState(false); const isLightboxInRange = useMemo(() => { if (NUMBER_OF_CONCURRENT_LIGHTBOXES === -1) { return true; @@ -123,17 +101,24 @@ function Lightbox({ const indexOutOfRange = index > activeIndex + indexCanvasOffset || index < activeIndex - indexCanvasOffset; return !indexOutOfRange; }, [activeIndex, index]); - const [isLightboxImageLoaded, setLightboxImageLoaded] = useState(false); - const isLightboxVisible = isLightboxInRange && (isActive || isLightboxImageLoaded || isFallbackImageLoaded); + const isLightboxVisible = isLightboxInRange && (isActive || isLightboxLoaded || isFallbackLoaded); - // If the fallback image is currently visible, we want to hide the Lightbox until the fallback gets hidden, + // If the fallback image is currently visible, we want to hide the Lightbox until the fallback gets hidden, // so that we don't see two overlapping images at the same time. - // We cannot NOT render it, because we need to render the Lightbox to get the correct dimensions for the fallback image. // If there the Lightbox is not used within a carousel, we don't need to hide the Lightbox, // because it's only going to be rendered after the fallback image is hidden. const shouldHideLightbox = hasSiblingCarouselItems && isFallbackVisible; - const isLoading = isActive && (!isCanvasLoaded || !contentLoaded); + const isLoading = isActive && (!isContainerLoaded || !isImageLoaded); + + const updateCanvasSize = useCallback( + ({ + nativeEvent: { + layout: {width, height}, + }, + }: LayoutChangeEvent) => setContainerSize({width: PixelRatio.roundToNearestPixel(width), height: PixelRatio.roundToNearestPixel(height)}), + [], + ); // We delay setting a page to active state by a (few) millisecond(s), // to prevent the image transformer from flashing while still rendering @@ -150,8 +135,8 @@ function Lightbox({ if (isLightboxVisible) { return; } - setContentSize(undefined); - }, [isLightboxVisible, setContentSize]); + setImageLoaded(false); + }, [isLightboxVisible, setImageDimensions]); useEffect(() => { if (!hasSiblingCarouselItems) { @@ -159,47 +144,71 @@ function Lightbox({ } if (isActive) { - if (contentLoaded && isFallbackVisible) { + if (isImageLoaded && isFallbackVisible) { // We delay hiding the fallback image while image transformer is still rendering setTimeout(() => { setFallbackVisible(false); - setFallbackImageLoaded(false); + setFallbackLoaded(false); }, 100); } } else { - if (isLightboxVisible && isLightboxImageLoaded) { + if (isLightboxVisible && isLightboxLoaded) { return; } // Show fallback when the image goes out of focus or when the image is loading setFallbackVisible(true); } - }, [hasSiblingCarouselItems, isActive, isFallbackVisible, isLightboxImageLoaded, isLightboxVisible, contentLoaded]); + }, [hasSiblingCarouselItems, isActive, isFallbackVisible, isLightboxLoaded, isLightboxVisible, isImageLoaded]); + + const fallbackSize = useMemo(() => { + if (!hasSiblingCarouselItems || (imageDimensions?.lightboxSize == null && imageDimensions?.fallbackSize == null) || containerSize.width === 0 || containerSize.height === 0) { + return { + width: DEFAULT_IMAGE_SIZE, + height: DEFAULT_IMAGE_SIZE, + }; + } + + // If the lightbox size is undefined, th fallback size cannot be undefined, + // because we already checked for that before and would have returned early. + const imageSize = imageDimensions.lightboxSize || imageDimensions.fallbackSize!; + + const {minScale} = getCanvasFitScale({canvasSize: containerSize, contentSize: imageSize}); + + return { + width: PixelRatio.roundToNearestPixel(imageSize.width * minScale), + height: PixelRatio.roundToNearestPixel(imageSize.height * minScale), + }; + }, [containerSize, hasSiblingCarouselItems, imageDimensions]); return ( - {isCanvasLoaded && ( + {isContainerLoaded && ( <> {isLightboxVisible && ( setLightboxImageLoaded(true)} + onLoadEnd={() => setImageLoaded(true)} + onLoad={(e: ImageOnLoadEvent) => { + const width = (e.nativeEvent?.width || 0) * PixelRatio.get(); + const height = (e.nativeEvent?.height || 0) * PixelRatio.get(); + setImageDimensions({...imageDimensions, lightboxSize: {width, height}}); + }} /> @@ -209,12 +218,21 @@ function Lightbox({ {isFallbackVisible && ( setFallbackImageLoaded(true)} + onLoadEnd={() => setFallbackLoaded(true)} + onLoad={(e: ImageOnLoadEvent) => { + const width = (e.nativeEvent?.width || 0) * PixelRatio.get(); + const height = (e.nativeEvent?.height || 0) * PixelRatio.get(); + + if (imageDimensions?.lightboxSize != null) { + return; + } + + setImageDimensions({...imageDimensions, fallbackSize: {width, height}}); + }} /> )} diff --git a/src/components/MultiGestureCanvas/getCanvasFitScale.ts b/src/components/MultiGestureCanvas/getCanvasFitScale.ts new file mode 100644 index 000000000000..8fbb72e1f294 --- /dev/null +++ b/src/components/MultiGestureCanvas/getCanvasFitScale.ts @@ -0,0 +1,15 @@ +import type {CanvasSize, ContentSize} from './types'; + +type GetCanvasFitScale = (props: {canvasSize: CanvasSize; contentSize: ContentSize}) => {scaleX: number; scaleY: number; minScale: number; maxScale: number}; + +const getCanvasFitScale: GetCanvasFitScale = ({canvasSize, contentSize}) => { + const scaleX = canvasSize.width / contentSize.width; + const scaleY = canvasSize.height / contentSize.height; + + const minScale = Math.min(scaleX, scaleY); + const maxScale = Math.max(scaleX, scaleY); + + return {scaleX, scaleY, minScale, maxScale}; +}; + +export default getCanvasFitScale; diff --git a/src/components/MultiGestureCanvas/utils.ts b/src/components/MultiGestureCanvas/utils.ts index 4e377b3702d9..26e814313f8f 100644 --- a/src/components/MultiGestureCanvas/utils.ts +++ b/src/components/MultiGestureCanvas/utils.ts @@ -1,6 +1,5 @@ import {useCallback} from 'react'; import type {WorkletFunction} from 'react-native-reanimated/lib/typescript/reanimated2/commonTypes'; -import type {CanvasSize, ContentSize} from './types'; // The spring config is used to determine the physics of the spring animation // Details and a playground for testing different configs can be found at @@ -49,16 +48,4 @@ function useWorkletCallback( return useCallback<(...args: Args) => ReturnValue>(callback, deps) as WorkletFunction; } -type GetCanvasFitScale = (props: {canvasSize: CanvasSize; contentSize: ContentSize}) => {scaleX: number; scaleY: number; minScale: number; maxScale: number}; - -const getCanvasFitScale: GetCanvasFitScale = ({canvasSize, contentSize}) => { - const scaleX = canvasSize.width / contentSize.width; - const scaleY = canvasSize.height / contentSize.height; - - const minScale = Math.min(scaleX, scaleY); - const maxScale = Math.max(scaleX, scaleY); - - return {scaleX, scaleY, minScale, maxScale}; -}; - -export {SPRING_CONFIG, zoomScaleBounceFactors, clamp, useWorkletCallback, getCanvasFitScale}; +export {SPRING_CONFIG, zoomScaleBounceFactors, clamp, useWorkletCallback}; From e1a546f1d68f4258753f09351532da81787278ee Mon Sep 17 00:00:00 2001 From: Ishpaul Singh Date: Mon, 8 Jan 2024 15:53:38 +0530 Subject: [PATCH 170/580] 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 171/580] 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 9429921fb69c00262b00c26350e4058fe6a1e40c Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Mon, 8 Jan 2024 11:35:25 +0100 Subject: [PATCH 172/580] fix: revert changes --- src/components/Lightbox.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/components/Lightbox.tsx b/src/components/Lightbox.tsx index 1684a489c0da..a673aa6c4b3e 100644 --- a/src/components/Lightbox.tsx +++ b/src/components/Lightbox.tsx @@ -4,9 +4,8 @@ import {ActivityIndicator, PixelRatio, StyleSheet, View} from 'react-native'; import useStyleUtils from '@hooks/useStyleUtils'; import Image from './Image'; import MultiGestureCanvas, {defaultZoomRange} from './MultiGestureCanvas'; +import getCanvasFitScale from './MultiGestureCanvas/getCanvasFitScale'; import type {ContentSize, OnScaleChangedCallback, ZoomRange} from './MultiGestureCanvas/types'; -import * as MultiGestureCanvasUtils from './MultiGestureCanvas/utils'; -import getCanvasFitScale from '@components/MultiGestureCanvas/getCanvasFitScale'; // Increase/decrease this number to change the number of concurrent lightboxes // The more concurrent lighboxes, the worse performance gets (especially on low-end devices) @@ -22,7 +21,7 @@ type LightboxImageDimensions = { lightboxSize?: ContentSize; fallbackSize?: ContentSize; }; -} + const cachedDimensions = new Map(); type ImageOnLoadEvent = NativeSyntheticEvent; @@ -91,7 +90,7 @@ function Lightbox({ const [isFallbackVisible, setFallbackVisible] = useState(isInactiveCarouselItem); const [isFallbackLoaded, setFallbackLoaded] = useState(false); - const [isLightboxLoaded, setLightboxLoaded] = useState(false); + const isLightboxLoaded = imageDimensions?.lightboxSize != null; const isLightboxInRange = useMemo(() => { if (NUMBER_OF_CONCURRENT_LIGHTBOXES === -1) { return true; @@ -103,7 +102,7 @@ function Lightbox({ }, [activeIndex, index]); const isLightboxVisible = isLightboxInRange && (isActive || isLightboxLoaded || isFallbackLoaded); - // If the fallback image is currently visible, we want to hide the Lightbox until the fallback gets hidden, + // If the fallback image is currently visible, we want to hide the Lightbox until the fallback gets hidden, // so that we don't see two overlapping images at the same time. // If there the Lightbox is not used within a carousel, we don't need to hide the Lightbox, // because it's only going to be rendered after the fallback image is hidden. @@ -171,7 +170,8 @@ function Lightbox({ // If the lightbox size is undefined, th fallback size cannot be undefined, // because we already checked for that before and would have returned early. - const imageSize = imageDimensions.lightboxSize || imageDimensions.fallbackSize!; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const imageSize = imageDimensions.lightboxSize ?? imageDimensions.fallbackSize!; const {minScale} = getCanvasFitScale({canvasSize: containerSize, contentSize: imageSize}); From a869e6336acad911496d0bd9268a8222783a26ba Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Mon, 8 Jan 2024 11:36:36 +0100 Subject: [PATCH 173/580] fix: hook deps --- src/components/Lightbox.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/Lightbox.tsx b/src/components/Lightbox.tsx index a673aa6c4b3e..a96afc9c741b 100644 --- a/src/components/Lightbox.tsx +++ b/src/components/Lightbox.tsx @@ -135,7 +135,7 @@ function Lightbox({ return; } setImageLoaded(false); - }, [isLightboxVisible, setImageDimensions]); + }, [isLightboxVisible]); useEffect(() => { if (!hasSiblingCarouselItems) { @@ -158,7 +158,7 @@ function Lightbox({ // Show fallback when the image goes out of focus or when the image is loading setFallbackVisible(true); } - }, [hasSiblingCarouselItems, isActive, isFallbackVisible, isLightboxLoaded, isLightboxVisible, isImageLoaded]); + }, [hasSiblingCarouselItems, isActive, isImageLoaded, isFallbackVisible, isLightboxLoaded, isLightboxVisible]); const fallbackSize = useMemo(() => { if (!hasSiblingCarouselItems || (imageDimensions?.lightboxSize == null && imageDimensions?.fallbackSize == null) || containerSize.width === 0 || containerSize.height === 0) { From b42422baf94f79c830b8f2dcfbe8a627d61016de Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Mon, 8 Jan 2024 11:37:26 +0100 Subject: [PATCH 174/580] more fixes --- src/components/Lightbox.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/Lightbox.tsx b/src/components/Lightbox.tsx index a96afc9c741b..db662e6e8776 100644 --- a/src/components/Lightbox.tsx +++ b/src/components/Lightbox.tsx @@ -198,9 +198,8 @@ function Lightbox({ zoomRange={zoomRange} > setImageLoaded(true)} From 15e2ecd4c705479166e7ebb74ffdc50a3bd85adb Mon Sep 17 00:00:00 2001 From: Ishpaul Singh Date: Mon, 8 Jan 2024 16:07:32 +0530 Subject: [PATCH 175/580] 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 b10e6008b7d41b7c5e0d3ed9ea0d786b92f5fd62 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Mon, 8 Jan 2024 11:51:52 +0100 Subject: [PATCH 176/580] update propTypes --- .../MultiGestureCanvas/propTypes.js | 26 ++++++------------- 1 file changed, 8 insertions(+), 18 deletions(-) diff --git a/src/components/MultiGestureCanvas/propTypes.js b/src/components/MultiGestureCanvas/propTypes.js index f1961ec0e156..4fba5123fee5 100644 --- a/src/components/MultiGestureCanvas/propTypes.js +++ b/src/components/MultiGestureCanvas/propTypes.js @@ -23,15 +23,6 @@ const zoomRangeDefaultProps = { const multiGestureCanvasPropTypes = { ...zoomRangePropTypes, - /** - * Wheter the canvas is currently active (in the screen) or not. - * Disables certain gestures and functionality - */ - isActive: PropTypes.bool, - - /** Handles scale changed event */ - onScaleChanged: PropTypes.func, - /** The width and height of the canvas. * This is needed in order to properly scale the content in the canvas */ @@ -48,15 +39,14 @@ const multiGestureCanvasPropTypes = { height: PropTypes.number, }), - /** The scale factors (scaleX, scaleY) that are used to scale the content (width/height) to the canvas size. - * `scaledWidth` and `scaledHeight` reflect the actual size of the content after scaling. + /** + * Wheter the canvas is currently active (in the screen) or not. + * Disables certain gestures and functionality */ - contentScaling: PropTypes.shape({ - scaleX: PropTypes.number, - scaleY: PropTypes.number, - scaledWidth: PropTypes.number, - scaledHeight: PropTypes.number, - }), + isActive: PropTypes.bool, + + /** Handles scale changed event */ + onScaleChanged: PropTypes.func, /** Content that should be transformed inside the canvas (images, pdf, ...) */ children: PropTypes.node.isRequired, @@ -67,7 +57,7 @@ const multiGestureCanvasDefaultProps = { onScaleChanged: () => undefined, contentSize: undefined, contentScaling: undefined, - zoomRange: undefined, + zoomRange: defaultZoomRange, }; export {defaultZoomRange, zoomRangePropTypes, zoomRangeDefaultProps, multiGestureCanvasPropTypes, multiGestureCanvasDefaultProps}; From f1a6f9bd1f0e7f0f36ffe1cc5b85af255ebb8b2e Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Mon, 8 Jan 2024 12:18:53 +0100 Subject: [PATCH 177/580] fix: import --- src/components/MultiGestureCanvas/index.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/MultiGestureCanvas/index.tsx b/src/components/MultiGestureCanvas/index.tsx index 65a14d25a492..60a138b44606 100644 --- a/src/components/MultiGestureCanvas/index.tsx +++ b/src/components/MultiGestureCanvas/index.tsx @@ -8,6 +8,7 @@ import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; import type ChildrenProps from '@src/types/utils/ChildrenProps'; import {defaultZoomRange} from './constants'; +import getCanvasFitScale from './getCanvasFitScale'; import type {CanvasSize, ContentSize, OnScaleChangedCallback, ZoomRange} from './types'; import usePanGesture from './usePanGesture'; import usePinchGesture from './usePinchGesture'; @@ -94,7 +95,7 @@ function MultiGestureCanvas({ // Based on the (original) content size and the canvas size, we calculate the horizontal and vertical scale factors // to fit the content inside the canvas // We later use the lower of the two scale factors to fit the content inside the canvas - const {minScale: minContentScale, maxScale: maxContentScale} = useMemo(() => MultiGestureCanvasUtils.getCanvasFitScale({canvasSize, contentSize}), [canvasSize, contentSize]); + const {minScale: minContentScale, maxScale: maxContentScale} = useMemo(() => getCanvasFitScale({canvasSize, contentSize}), [canvasSize, contentSize]); const zoomScale = useSharedValue(1); From 29d4061ee30807bf605ce52d6b64f8ae4dee2179 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Mon, 8 Jan 2024 13:20:55 +0100 Subject: [PATCH 178/580] 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 b72567531453def76f89c0ce90b52af8d911ed02 Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Mon, 8 Jan 2024 16:11:46 +0100 Subject: [PATCH 179/580] remove draft, resolve prop related errors --- src/components/MagicCodeInput-draft.tsx | 418 ------------------------ src/components/MagicCodeInput.tsx | 200 +++++------- 2 files changed, 79 insertions(+), 539 deletions(-) delete mode 100644 src/components/MagicCodeInput-draft.tsx diff --git a/src/components/MagicCodeInput-draft.tsx b/src/components/MagicCodeInput-draft.tsx deleted file mode 100644 index 6c1cb1851e18..000000000000 --- a/src/components/MagicCodeInput-draft.tsx +++ /dev/null @@ -1,418 +0,0 @@ -import React, {ForwardedRef, forwardRef, useEffect, useImperativeHandle, useRef, useState} from 'react'; -import {StyleSheet, View, TextInput as RNTextInput, NativeSyntheticEvent, TextInputFocusEventData} from 'react-native'; -import {HandlerStateChangeEvent, TapGestureHandler} from 'react-native-gesture-handler'; -import useNetwork from '@hooks/useNetwork'; -import useStyleUtils from '@hooks/useStyleUtils'; -import useThemeStyles from '@hooks/useThemeStyles'; -import * as Browser from '@libs/Browser'; -import * as ValidationUtils from '@libs/ValidationUtils'; -import CONST from '@src/CONST'; -import FormHelpMessage from './FormHelpMessage'; -import Text from './Text'; -import TextInput from './TextInput'; - -const TEXT_INPUT_EMPTY_STATE = ''; - -type MagicCodeInputProps = { - /** Name attribute for the input */ - name?: string, - - /** Input value */ - value?: string, - - /** Should the input auto focus */ - autoFocus?: boolean, - - /** Whether we should wait before focusing the TextInput, useful when using transitions */ - shouldDelayFocus?: boolean, - - /** Error text to display */ - errorText?: string, - - /** Specifies autocomplete hints for the system, so it can provide autofill */ - autoComplete: 'sms-otp' | 'one-time-code' | 'off', - - /* Should submit when the input is complete */ - shouldSubmitOnComplete?: boolean, - - /** Function to call when the input is changed */ - onChangeText?: (value: string) => void, - - /** Function to call when the input is submitted or fully complete */ - onFulfill?: (value: string) => void, - - /** Specifies if the input has a validation error */ - hasError?: boolean, - - /** Specifies the max length of the input */ - maxLength?: number, - - /** Specifies if the keyboard should be disabled */ - isDisableKeyboard?: boolean, - - /** Last pressed digit on BigDigitPad */ - lastPressedDigit?: string, -} - -/** - * Converts a given string into an array of numbers that must have the same - * number of elements as the number of inputs. - */ -const decomposeString = (value: string, length: number): string[] => { - let arr = value.split('').slice(0, length).map((v) => (ValidationUtils.isNumeric(v) ? v : CONST.MAGIC_CODE_EMPTY_CHAR)) - if (arr.length < length) { - arr = arr.concat(Array(length - arr.length).fill(CONST.MAGIC_CODE_EMPTY_CHAR)); - } - return arr; -}; - -/** - * Converts an array of strings into a single string. If there are undefined or - * empty values, it will replace them with a space. - */ -const composeToString = (value: string[]): string => value.map((v) => (v === undefined || v === '' ? CONST.MAGIC_CODE_EMPTY_CHAR : v)).join(''); - -const getInputPlaceholderSlots = (length: number): number[] => Array.from(Array(length).keys()); - -function MagicCodeInput({ - value = '', - name = '', - autoFocus = true, - shouldDelayFocus = false, - errorText = '', - shouldSubmitOnComplete = true, - onChangeText: onChangeTextProp = () => {}, - onFulfill = () => {}, - hasError = false, - maxLength = CONST.MAGIC_CODE_LENGTH, - isDisableKeyboard = false, - lastPressedDigit = '', - autoComplete, -}: MagicCodeInputProps, ref: ForwardedRef) { - const styles = useThemeStyles(); - const StyleUtils = useStyleUtils(); - const inputRefs = useRef(); - const [input, setInput] = useState(TEXT_INPUT_EMPTY_STATE); - const [focusedIndex, setFocusedIndex] = useState(0); - const [editIndex, setEditIndex] = useState(0); - const [wasSubmitted, setWasSubmitted] = useState(false); - const shouldFocusLast = useRef(false); - const inputWidth = useRef(0); - const lastFocusedIndex = useRef(0); - const lastValue = useRef(TEXT_INPUT_EMPTY_STATE); - - console.log("** I RENDER **") - - useEffect(() => { - lastValue.current = input.length; - }, [input]); - - const blurMagicCodeInput = () => { - inputRefs.current?.blur(); - setFocusedIndex(undefined); - }; - - const focusMagicCodeInput = () => { - setFocusedIndex(0); - lastFocusedIndex.current = 0; - setEditIndex(0); - inputRefs.current?.focus(); - }; - - const setInputAndIndex = (index: number | undefined) => { - setInput(TEXT_INPUT_EMPTY_STATE); - setFocusedIndex(index); - setEditIndex(index); - }; - - useImperativeHandle(ref, () => ({ - focus() { - focusMagicCodeInput(); - }, - focusLastSelected() { - inputRefs.current?.focus(); - }, - resetFocus() { - setInput(TEXT_INPUT_EMPTY_STATE); - focusMagicCodeInput(); - }, - clear() { - lastFocusedIndex.current = 0; - setInputAndIndex(0); - inputRefs.current?.focus(); - onChangeTextProp(''); - }, - blur() { - blurMagicCodeInput(); - }, - })); - - const validateAndSubmit = () => { - const numbers = decomposeString(value, maxLength); - if (wasSubmitted || !shouldSubmitOnComplete || numbers.filter((n) => ValidationUtils.isNumeric(n)).length !== maxLength || isOffline) { - return; - } - if (!wasSubmitted) { - setWasSubmitted(true); - } - // Blurs the input and removes focus from the last input and, if it should submit - // on complete, it will call the onFulfill callback. - blurMagicCodeInput(); - onFulfill(value); - lastValue.current = ''; - }; - - const {isOffline} = useNetwork({onReconnect: validateAndSubmit}); - - useEffect(() => { - validateAndSubmit(); - - // We have not added: - // + the editIndex as the dependency because we don't want to run this logic after focusing on an input to edit it after the user has completed the code. - // + the onFulfill as the dependency because onFulfill is changed when the preferred locale changed => avoid auto submit form when preferred locale changed. - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [value, shouldSubmitOnComplete]); - - /** - * Focuses on the input when it is pressed. - * - * @param event - * @param index - */ - const onFocus = (event: NativeSyntheticEvent) => { - if (shouldFocusLast.current) { - lastValue.current = TEXT_INPUT_EMPTY_STATE; - setInputAndIndex(lastFocusedIndex.current); - } - event.preventDefault(); - }; - - /** - * Callback for the onPress event, updates the indexes - * of the currently focused input. - * - * @param index - */ - const onPress = (index: number) => { - shouldFocusLast.current = false; - // TapGestureHandler works differently on mobile web and native app - // On web gesture handler doesn't block interactions with textInput below so there is no need to run `focus()` manually - if (!Browser.isMobileChrome() && !Browser.isMobileSafari()) { - inputRefs.current?.focus(); - } - setInputAndIndex(index); - lastFocusedIndex.current = index; - }; - - /** - * Updates the magic inputs with the contents written in the - * input. It spreads each number into each input and updates - * the focused input on the next empty one, if exists. - * It handles both fast typing and only one digit at a time - * in a specific position. - * - * @param value - */ - const onChangeText = (val: string) => { - console.log('ON CHANGE', val) - if (!val || !ValidationUtils.isNumeric(val)) { - return; - } - - // Checks if one new character was added, or if the content was replaced - const hasToSlice = val.length - 1 === lastValue.current.length && val.slice(0, val.length - 1) === lastValue.current; - - // Gets the new value added by the user - const addedValue = hasToSlice ? val.slice(lastValue.current.length, val.length) : val; - - lastValue.current = val; - // Updates the focused input taking into consideration the last input - // edited and the number of digits added by the user. - const numbersArr = addedValue - .trim() - .split('') - .slice(0, maxLength - editIndex); - const updatedFocusedIndex = Math.min(editIndex + (numbersArr.length - 1) + 1, maxLength - 1); - - let numbers = decomposeString(val, maxLength); - numbers = [...numbers.slice(0, editIndex), ...numbersArr, ...numbers.slice(numbersArr.length + editIndex, maxLength)]; - - setInputAndIndex(updatedFocusedIndex); - - const finalInput = composeToString(numbers); - onChangeTextProp(finalInput); - }; - - /** - * Handles logic related to certain key presses. - * - * NOTE: when using Android Emulator, this can only be tested using - * hardware keyboard inputs. - * - * @param event - */ - const onKeyPress = ({nativeEvent: {key: keyValue}}) => { - if (keyValue === 'Backspace' || keyValue === '<') { - let numbers = decomposeString(value, maxLength); - - // If keyboard is disabled and no input is focused we need to remove - // the last entered digit and focus on the correct input - if (isDisableKeyboard && focusedIndex === undefined) { - const indexBeforeLastEditIndex = editIndex === 0 ? editIndex : editIndex - 1; - - const indexToFocus = numbers[editIndex] === CONST.MAGIC_CODE_EMPTY_CHAR ? indexBeforeLastEditIndex : editIndex; - inputRefs.current[indexToFocus].focus(); - onChangeTextProp(value.substring(0, indexToFocus)); - - return; - } - - // If the currently focused index already has a value, it will delete - // that value but maintain the focus on the same input. - if (numbers[focusedIndex] !== CONST.MAGIC_CODE_EMPTY_CHAR) { - setInput(TEXT_INPUT_EMPTY_STATE); - numbers = [...numbers.slice(0, focusedIndex), CONST.MAGIC_CODE_EMPTY_CHAR, ...numbers.slice(focusedIndex + 1, maxLength)]; - setEditIndex(focusedIndex); - onChangeTextProp(composeToString(numbers)); - return; - } - - const hasInputs = numbers.filter((n) => ValidationUtils.isNumeric(n)).length !== 0 - - // Fill the array with empty characters if there are no inputs. - if (focusedIndex === 0 && !hasInputs) { - numbers = Array(maxLength).fill(CONST.MAGIC_CODE_EMPTY_CHAR); - - // Deletes the value of the previous input and focuses on it. - } else if (focusedIndex !== 0) { - numbers = [...numbers.slice(0, Math.max(0, focusedIndex - 1)), CONST.MAGIC_CODE_EMPTY_CHAR, ...numbers.slice(focusedIndex, maxLength)]; - } - - const newFocusedIndex = Math.max(0, focusedIndex - 1); - - // Saves the input string so that it can compare to the change text - // event that will be triggered, this is a workaround for mobile that - // triggers the change text on the event after the key press. - setInputAndIndex(newFocusedIndex); - onChangeTextProp(composeToString(numbers)); - - if (newFocusedIndex !== undefined) { - inputRefs.current?.focus(); - } - } - if (keyValue === 'ArrowLeft' && focusedIndex !== undefined) { - const newFocusedIndex = Math.max(0, focusedIndex - 1); - setInputAndIndex(newFocusedIndex); - inputRefs.current?.focus(); - } else if (keyValue === 'ArrowRight' && focusedIndex !== undefined) { - const newFocusedIndex = Math.min(focusedIndex + 1, maxLength - 1); - setInputAndIndex(newFocusedIndex); - inputRefs.current?.focus(); - } else if (keyValue === 'Enter') { - // We should prevent users from submitting when it's offline. - if (isOffline) { - return; - } - setInput(TEXT_INPUT_EMPTY_STATE); - onFulfill(value); - } - }; - - /** - * If isDisableKeyboard is true we will have to call onKeyPress and onChangeText manually - * as the press on digit pad will not trigger native events. We take lastPressedDigit from props - * as it stores the last pressed digit pressed on digit pad. We take only the first character - * as anything after that is added to differentiate between two same digits passed in a row. - */ - - useEffect(() => { - if (!isDisableKeyboard) { - return; - } - - const val = lastPressedDigit.charAt(0); - onKeyPress({nativeEvent: {key: val}}); - onChangeText(val); - - // We have not added: - // + the onChangeText and onKeyPress as the dependencies because we only want to run this when lastPressedDigit changes. - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [lastPressedDigit, isDisableKeyboard]); - - return ( - <> - - { - onPress(Math.floor(e.nativeEvent.x / (inputWidth.current / maxLength))); - }} - > - {/* Android does not handle touch on invisible Views so I created a wrapper around invisible TextInput just to handle taps */} - - { - inputWidth.current = e.nativeEvent.layout.width; - }} - ref={(inputRef) => (inputRefs.current = inputRef)} - autoFocus={autoFocus} - inputMode="numeric" - textContentType="oneTimeCode" - name={name} - maxLength={maxLength} - value={input} - hideFocusedState - autoComplete={input.length === 0 && autoComplete} - shouldDelayFocus={input.length === 0 && shouldDelayFocus} - keyboardType={CONST.KEYBOARD_TYPE.NUMBER_PAD} - onChangeText={(text: string) => { - onChangeText(text); - }} - onKeyPress={onKeyPress} - onFocus={onFocus} - onBlur={() => { - shouldFocusLast.current = true; - lastFocusedIndex.current = focusedIndex; - setFocusedIndex(undefined); - }} - selectionColor="transparent" - inputStyle={[styles.inputTransparent]} - role={CONST.ROLE.FORM} - style={[styles.inputTransparent]} - textInputContainerStyles={[styles.borderNone]} - /> - - - {getInputPlaceholderSlots(maxLength).map((index) => ( - - - {decomposeString(value, maxLength)[index] || ''} - - - ))} - - {errorText && ( - - )} - - ); -} - -MagicCodeInput.displayName = 'MagicCodeInput'; - -export default forwardRef(MagicCodeInput); diff --git a/src/components/MagicCodeInput.tsx b/src/components/MagicCodeInput.tsx index b238c774405c..38dd92e7a836 100644 --- a/src/components/MagicCodeInput.tsx +++ b/src/components/MagicCodeInput.tsx @@ -1,7 +1,7 @@ -import PropTypes from 'prop-types'; -import React, {forwardRef, useEffect, useImperativeHandle, useRef, useState} from 'react'; -import {NativeSyntheticEvent, StyleSheet, TextInputFocusEventData, View} from 'react-native'; +import React, {ForwardedRef, forwardRef, useEffect, useImperativeHandle, useRef, useState} from 'react'; +import {NativeSyntheticEvent, StyleSheet, TextInputFocusEventData, TextInputKeyPressEventData, TextInputProps, View} from 'react-native'; import {TapGestureHandler} from 'react-native-gesture-handler'; +import {AnimatedProps} from 'react-native-reanimated'; import useNetwork from '@hooks/useNetwork'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -14,106 +14,53 @@ import TextInput from './TextInput'; const TEXT_INPUT_EMPTY_STATE = ''; -const propTypes = { - /** Name attribute for the input */ - name: PropTypes.string, - - /** Input value */ - value: PropTypes.string, - - /** Should the input auto focus */ - autoFocus: PropTypes.bool, - - /** Whether we should wait before focusing the TextInput, useful when using transitions */ - shouldDelayFocus: PropTypes.bool, - - /** Error text to display */ - errorText: PropTypes.string, - - /** Specifies autocomplete hints for the system, so it can provide autofill */ - autoComplete: PropTypes.oneOf(['sms-otp', 'one-time-code', 'off']).isRequired, - - /* Should submit when the input is complete */ - shouldSubmitOnComplete: PropTypes.bool, - - innerRef: PropTypes.oneOfType([PropTypes.func, PropTypes.object]), - - /** Function to call when the input is changed */ - onChangeText: PropTypes.func, - - /** Function to call when the input is submitted or fully complete */ - onFulfill: PropTypes.func, - - /** Specifies if the input has a validation error */ - hasError: PropTypes.bool, - - /** Specifies the max length of the input */ - maxLength: PropTypes.number, - - /** Specifies if the keyboard should be disabled */ - isDisableKeyboard: PropTypes.bool, - - /** Last pressed digit on BigDigitPad */ - lastPressedDigit: PropTypes.string, -}; - type MagicCodeInputProps = { /** Name attribute for the input */ - name?: string, + name?: string; /** Input value */ - value?: string, + value?: string; /** Should the input auto focus */ - autoFocus?: boolean, + autoFocus?: boolean; /** Whether we should wait before focusing the TextInput, useful when using transitions */ - shouldDelayFocus?: boolean, + shouldDelayFocus?: boolean; /** Error text to display */ - errorText?: string, + errorText?: string; /** Specifies autocomplete hints for the system, so it can provide autofill */ - autoComplete: 'sms-otp' | 'one-time-code' | 'off', + autoComplete: 'sms-otp' | 'one-time-code' | 'off'; /* Should submit when the input is complete */ - shouldSubmitOnComplete?: boolean, + shouldSubmitOnComplete?: boolean; /** Function to call when the input is changed */ - onChangeText?: (value: string) => void, + onChangeText?: (value: string) => void; /** Function to call when the input is submitted or fully complete */ - onFulfill?: (value: string) => void, + onFulfill?: (value: string) => void; /** Specifies if the input has a validation error */ - hasError?: boolean, + hasError?: boolean; /** Specifies the max length of the input */ - maxLength?: number, + maxLength?: number; /** Specifies if the keyboard should be disabled */ - isDisableKeyboard?: boolean, + isDisableKeyboard?: boolean; /** Last pressed digit on BigDigitPad */ - lastPressedDigit?: string, - - innerRef: unknown; -} + lastPressedDigit?: string; +}; -const defaultProps = { - value: '', - name: '', - autoFocus: true, - shouldDelayFocus: false, - errorText: '', - shouldSubmitOnComplete: true, - innerRef: null, - onChangeText: () => {}, - onFulfill: () => {}, - hasError: false, - maxLength: CONST.MAGIC_CODE_LENGTH, - isDisableKeyboard: false, - lastPressedDigit: '', +type MagicCodeInputHandle = { + focus: () => void; + focusLastSelected: () => void; + resetFocus: () => void; + clear: () => void; + blur: () => void; }; /** @@ -121,7 +68,10 @@ const defaultProps = { * number of elements as the number of inputs. */ const decomposeString = (value: string, length: number): string[] => { - let arr = value.split('').slice(0, length).map((v) => (ValidationUtils.isNumeric(v) ? v : CONST.MAGIC_CODE_EMPTY_CHAR)) + let arr = value + .split('') + .slice(0, length) + .map((v) => (ValidationUtils.isNumeric(v) ? v : CONST.MAGIC_CODE_EMPTY_CHAR)); if (arr.length < length) { arr = arr.concat(Array(length - arr.length).fill(CONST.MAGIC_CODE_EMPTY_CHAR)); } @@ -136,10 +86,25 @@ const composeToString = (value: string[]): string => value.map((v) => (v === und const getInputPlaceholderSlots = (length: number): number[] => Array.from(Array(length).keys()); -function MagicCodeInput(props: MagicCodeInputProps) { - const {value = '', name = '', autoFocus = true, shouldDelayFocus = false, errorText = '', shouldSubmitOnComplete = true, onChangeText: onChangeTextProp = () => {}} = props +function MagicCodeInput(props: MagicCodeInputProps, ref: ForwardedRef) { + const { + value = '', + name = '', + autoFocus = true, + shouldDelayFocus = false, + errorText = '', + shouldSubmitOnComplete = true, + onChangeText: onChangeTextProp = () => {}, + maxLength = CONST.MAGIC_CODE_LENGTH, + onFulfill = () => {}, + isDisableKeyboard = false, + lastPressedDigit = '', + autoComplete, + hasError = false, + } = props; const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); + // const inputRefs = useRef> | null>(); const inputRefs = useRef(); const [input, setInput] = useState(TEXT_INPUT_EMPTY_STATE); const [focusedIndex, setFocusedIndex] = useState(0); @@ -172,7 +137,7 @@ function MagicCodeInput(props: MagicCodeInputProps) { setEditIndex(index); }; - useImperativeHandle(props.innerRef, () => ({ + useImperativeHandle(ref, () => ({ focus() { focusMagicCodeInput(); }, @@ -195,8 +160,8 @@ function MagicCodeInput(props: MagicCodeInputProps) { })); const validateAndSubmit = () => { - const numbers = decomposeString(value, props.maxLength); - if (wasSubmitted || !shouldSubmitOnComplete || numbers.filter((n) => ValidationUtils.isNumeric(n)).length !== props.maxLength || isOffline) { + const numbers = decomposeString(value, maxLength); + if (wasSubmitted || !shouldSubmitOnComplete || numbers.filter((n) => ValidationUtils.isNumeric(n)).length !== maxLength || isOffline) { return; } if (!wasSubmitted) { @@ -205,7 +170,7 @@ function MagicCodeInput(props: MagicCodeInputProps) { // Blurs the input and removes focus from the last input and, if it should submit // on complete, it will call the onFulfill callback. blurMagicCodeInput(); - props.onFulfill(value); + onFulfill(value); lastValue.current = ''; }; @@ -216,7 +181,7 @@ function MagicCodeInput(props: MagicCodeInputProps) { // We have not added: // + the editIndex as the dependency because we don't want to run this logic after focusing on an input to edit it after the user has completed the code. - // + the props.onFulfill as the dependency because props.onFulfill is changed when the preferred locale changed => avoid auto submit form when preferred locale changed. + // + the onFulfill as the dependency because onFulfill is changed when the preferred locale changed => avoid auto submit form when preferred locale changed. // eslint-disable-next-line react-hooks/exhaustive-deps }, [value, shouldSubmitOnComplete]); @@ -269,7 +234,7 @@ function MagicCodeInput(props: MagicCodeInputProps) { const hasToSlice = typeof lastValue.current === 'string' && textValue.length - 1 === lastValue.current.length && textValue.slice(0, textValue.length - 1) === lastValue.current; // Gets the new textValue added by the user - const addedValue = (hasToSlice && typeof lastValue.current === 'string') ? textValue.slice(lastValue.current.length, textValue.length) : textValue; + const addedValue = hasToSlice && typeof lastValue.current === 'string' ? textValue.slice(lastValue.current.length, textValue.length) : textValue; lastValue.current = textValue; // Updates the focused input taking into consideration the last input @@ -277,11 +242,11 @@ function MagicCodeInput(props: MagicCodeInputProps) { const numbersArr = addedValue .trim() .split('') - .slice(0, props.maxLength - editIndex); - const updatedFocusedIndex = Math.min(editIndex + (numbersArr.length - 1) + 1, props.maxLength - 1); + .slice(0, maxLength - editIndex); + const updatedFocusedIndex = Math.min(editIndex + (numbersArr.length - 1) + 1, maxLength - 1); - let numbers = decomposeString(value, props.maxLength); - numbers = [...numbers.slice(0, editIndex), ...numbersArr, ...numbers.slice(numbersArr.length + editIndex, props.maxLength)]; + let numbers = decomposeString(value, maxLength); + numbers = [...numbers.slice(0, editIndex), ...numbersArr, ...numbers.slice(numbersArr.length + editIndex, maxLength)]; setInputAndIndex(updatedFocusedIndex); @@ -297,13 +262,14 @@ function MagicCodeInput(props: MagicCodeInputProps) { * * @param event */ - const onKeyPress = ({nativeEvent: {key: keyValue}}) => { + const onKeyPress = (event: Partial>) => { + const keyValue = event?.nativeEvent?.key; if (keyValue === 'Backspace' || keyValue === '<') { - let numbers = decomposeString(value, props.maxLength); + let numbers = decomposeString(value, maxLength); // If keyboard is disabled and no input is focused we need to remove // the last entered digit and focus on the correct input - if (props.isDisableKeyboard && focusedIndex === undefined) { + if (isDisableKeyboard && focusedIndex === undefined) { const indexBeforeLastEditIndex = editIndex === 0 ? editIndex : editIndex - 1; const indexToFocus = numbers[editIndex] === CONST.MAGIC_CODE_EMPTY_CHAR ? indexBeforeLastEditIndex : editIndex; @@ -317,7 +283,7 @@ function MagicCodeInput(props: MagicCodeInputProps) { // that value but maintain the focus on the same input. if (focusedIndex && numbers[focusedIndex] !== CONST.MAGIC_CODE_EMPTY_CHAR) { setInput(TEXT_INPUT_EMPTY_STATE); - numbers = [...numbers.slice(0, focusedIndex), CONST.MAGIC_CODE_EMPTY_CHAR, ...numbers.slice(focusedIndex + 1, props.maxLength)]; + numbers = [...numbers.slice(0, focusedIndex), CONST.MAGIC_CODE_EMPTY_CHAR, ...numbers.slice(focusedIndex + 1, maxLength)]; setEditIndex(focusedIndex); onChangeTextProp(composeToString(numbers)); return; @@ -327,11 +293,11 @@ function MagicCodeInput(props: MagicCodeInputProps) { // Fill the array with empty characters if there are no inputs. if (focusedIndex === 0 && !hasInputs) { - numbers = Array(props.maxLength).fill(CONST.MAGIC_CODE_EMPTY_CHAR); + numbers = Array(maxLength).fill(CONST.MAGIC_CODE_EMPTY_CHAR); // Deletes the value of the previous input and focuses on it. } else if (focusedIndex && focusedIndex !== 0) { - numbers = [...numbers.slice(0, Math.max(0, focusedIndex - 1)), CONST.MAGIC_CODE_EMPTY_CHAR, ...numbers.slice(focusedIndex, props.maxLength)]; + numbers = [...numbers.slice(0, Math.max(0, focusedIndex - 1)), CONST.MAGIC_CODE_EMPTY_CHAR, ...numbers.slice(focusedIndex, maxLength)]; } const newFocusedIndex = Math.max(0, (focusedIndex ?? 0) - 1); @@ -351,7 +317,7 @@ function MagicCodeInput(props: MagicCodeInputProps) { setInputAndIndex(newFocusedIndex); inputRefs.current.focus(); } else if (keyValue === 'ArrowRight' && focusedIndex !== undefined) { - const newFocusedIndex = Math.min(focusedIndex + 1, props.maxLength - 1); + const newFocusedIndex = Math.min(focusedIndex + 1, maxLength - 1); setInputAndIndex(newFocusedIndex); inputRefs.current.focus(); } else if (keyValue === 'Enter') { @@ -360,7 +326,7 @@ function MagicCodeInput(props: MagicCodeInputProps) { return; } setInput(TEXT_INPUT_EMPTY_STATE); - props.onFulfill(value); + onFulfill(value); } }; @@ -372,25 +338,26 @@ function MagicCodeInput(props: MagicCodeInputProps) { */ useEffect(() => { - if (!props.isDisableKeyboard) { + if (!isDisableKeyboard) { return; } - const textValue = props.lastPressedDigit.charAt(0); + const textValue = lastPressedDigit.charAt(0); onKeyPress({nativeEvent: {key: textValue}}); onChangeText(textValue); // We have not added: // + the onChangeText and onKeyPress as the dependencies because we only want to run this when lastPressedDigit changes. // eslint-disable-next-line react-hooks/exhaustive-deps - }, [props.lastPressedDigit, props.isDisableKeyboard]); + }, [lastPressedDigit, isDisableKeyboard]); return ( <> { - onPress(Math.floor(e.nativeEvent.x / (inputWidth.current / props.maxLength))); + console.log('** EVENT **', e); + onPress(Math.floor(e.nativeEvent.x / (inputWidth.current / maxLength))); }} > {/* Android does not handle touch on invisible Views so I created a wrapper around invisible TextInput just to handle taps */} @@ -402,15 +369,15 @@ function MagicCodeInput(props: MagicCodeInputProps) { onLayout={(e) => { inputWidth.current = e.nativeEvent.layout.width; }} - ref={(ref) => (inputRefs.current = ref)} + ref={(inputRef) => (inputRefs.current = inputRef)} autoFocus={autoFocus} inputMode="numeric" textContentType="oneTimeCode" name={name} - maxLength={props.maxLength} + maxLength={maxLength} value={input} hideFocusedState - autoComplete={input.length === 0 && props.autoComplete} + autoComplete={input.length === 0 ? autoComplete : undefined} shouldDelayFocus={input.length === 0 && shouldDelayFocus} keyboardType={CONST.KEYBOARD_TYPE.NUMBER_PAD} onChangeText={(textValue) => { @@ -420,32 +387,33 @@ function MagicCodeInput(props: MagicCodeInputProps) { onFocus={onFocus} onBlur={() => { shouldFocusLast.current = true; - lastFocusedIndex.current = focusedIndex; + lastFocusedIndex.current = focusedIndex ?? 0; setFocusedIndex(undefined); }} selectionColor="transparent" inputStyle={[styles.inputTransparent]} // role={CONST.ACCESSIBILITY_ROLE.TEXT} - role='none' + role="none" style={[styles.inputTransparent]} textInputContainerStyles={[styles.borderNone]} + icon={null} /> - {getInputPlaceholderSlots(props.maxLength).map((index) => ( + {getInputPlaceholderSlots(maxLength).map((index) => ( - {decomposeString(value, props.maxLength)[index] || ''} + {decomposeString(value, maxLength)[index] || ''} ))} @@ -460,16 +428,6 @@ function MagicCodeInput(props: MagicCodeInputProps) { ); } -MagicCodeInput.propTypes = propTypes; -MagicCodeInput.defaultProps = defaultProps; MagicCodeInput.displayName = 'MagicCodeInput'; -const MagicCodeInputWithRef = forwardRef((props, ref) => ( - -)); - -export default MagicCodeInputWithRef; +export default forwardRef(MagicCodeInput); 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 180/580] 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 c96835fc377af7de7dbffdc3183c261fc9d29a00 Mon Sep 17 00:00:00 2001 From: mkhutornyi Date: Mon, 8 Jan 2024 18:47:04 +0100 Subject: [PATCH 181/580] fix extra padding below emoji skin ton selection when keyboard is up --- src/components/Modal/BaseModal.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/Modal/BaseModal.tsx b/src/components/Modal/BaseModal.tsx index 6e5b4eddae9e..ab65bc7cdea8 100644 --- a/src/components/Modal/BaseModal.tsx +++ b/src/components/Modal/BaseModal.tsx @@ -2,6 +2,7 @@ import React, {forwardRef, useCallback, useEffect, useMemo, useRef} from 'react' import {View} from 'react-native'; import ReactNativeModal from 'react-native-modal'; import ColorSchemeWrapper from '@components/ColorSchemeWrapper'; +import useKeyboardState from '@hooks/useKeyboardState'; import usePrevious from '@hooks/usePrevious'; import useSafeAreaInsets from '@hooks/useSafeAreaInsets'; import useStyleUtils from '@hooks/useStyleUtils'; @@ -45,6 +46,7 @@ function BaseModal( const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); const {windowWidth, windowHeight, isSmallScreenWidth} = useWindowDimensions(); + const keyboardStateContextValue = useKeyboardState(); const safeAreaInsets = useSafeAreaInsets(); @@ -161,7 +163,7 @@ function BaseModal( safeAreaPaddingRight, shouldAddBottomSafeAreaMargin, shouldAddTopSafeAreaMargin, - shouldAddBottomSafeAreaPadding, + shouldAddBottomSafeAreaPadding: !keyboardStateContextValue?.isKeyboardShown && shouldAddBottomSafeAreaPadding, shouldAddTopSafeAreaPadding, modalContainerStyleMarginTop: modalContainerStyle.marginTop, modalContainerStyleMarginBottom: modalContainerStyle.marginBottom, From 2b84e41c4a7a0f19857f371fb9bf6903a4163acd Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Tue, 9 Jan 2024 08:43:39 +0100 Subject: [PATCH 182/580] type inputRefs --- src/components/MagicCodeInput.tsx | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/components/MagicCodeInput.tsx b/src/components/MagicCodeInput.tsx index 38dd92e7a836..8d1d275d9bea 100644 --- a/src/components/MagicCodeInput.tsx +++ b/src/components/MagicCodeInput.tsx @@ -105,7 +105,7 @@ function MagicCodeInput(props: MagicCodeInputProps, ref: ForwardedRef> | null>(); - const inputRefs = useRef(); + const inputRefs = useRef> | null>(); const [input, setInput] = useState(TEXT_INPUT_EMPTY_STATE); const [focusedIndex, setFocusedIndex] = useState(0); const [editIndex, setEditIndex] = useState(0); @@ -120,7 +120,7 @@ function MagicCodeInput(props: MagicCodeInputProps, ref: ForwardedRef { - inputRefs.current.blur(); + inputRefs.current?.blur(); setFocusedIndex(undefined); }; @@ -128,7 +128,7 @@ function MagicCodeInput(props: MagicCodeInputProps, ref: ForwardedRef { @@ -142,7 +142,7 @@ function MagicCodeInput(props: MagicCodeInputProps, ref: ForwardedRef Date: Tue, 9 Jan 2024 10:04:18 +0100 Subject: [PATCH 183/580] fix onBegan typing --- src/components/MagicCodeInput.tsx | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/components/MagicCodeInput.tsx b/src/components/MagicCodeInput.tsx index 8d1d275d9bea..e81e66e87e30 100644 --- a/src/components/MagicCodeInput.tsx +++ b/src/components/MagicCodeInput.tsx @@ -1,6 +1,6 @@ import React, {ForwardedRef, forwardRef, useEffect, useImperativeHandle, useRef, useState} from 'react'; import {NativeSyntheticEvent, StyleSheet, TextInputFocusEventData, TextInputKeyPressEventData, TextInputProps, View} from 'react-native'; -import {TapGestureHandler} from 'react-native-gesture-handler'; +import {HandlerStateChangeEvent, TapGestureHandler, TouchData} from 'react-native-gesture-handler'; import {AnimatedProps} from 'react-native-reanimated'; import useNetwork from '@hooks/useNetwork'; import useStyleUtils from '@hooks/useStyleUtils'; @@ -355,9 +355,8 @@ function MagicCodeInput(props: MagicCodeInputProps, ref: ForwardedRef { - console.log('** EVENT **', e); - onPress(Math.floor(e.nativeEvent.x / (inputWidth.current / maxLength))); + onBegan={(e: HandlerStateChangeEvent>) => { + onPress(Math.floor((e.nativeEvent?.x ?? 0) / (inputWidth.current / maxLength))); }} > {/* Android does not handle touch on invisible Views so I created a wrapper around invisible TextInput just to handle taps */} From 7bf4ad6ad103f455687e420bebd1a7be4c6c2ce0 Mon Sep 17 00:00:00 2001 From: tienifr Date: Tue, 9 Jan 2024 17:14:11 +0700 Subject: [PATCH 184/580] 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 185/580] 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 186/580] 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 187/580] 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 188/580] 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 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 189/580] 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 670a3e1501d27447e702e8a5835f209907b342ff Mon Sep 17 00:00:00 2001 From: Pedro Guerreiro Date: Tue, 9 Jan 2024 19:57:28 +0000 Subject: [PATCH 190/580] refactor(typescript): extract types to avoid repetition --- src/pages/ValidateLoginPage/index.tsx | 15 +++----------- src/pages/ValidateLoginPage/index.website.tsx | 20 ++----------------- src/pages/ValidateLoginPage/types.ts | 20 +++++++++++++++++++ 3 files changed, 25 insertions(+), 30 deletions(-) create mode 100644 src/pages/ValidateLoginPage/types.ts diff --git a/src/pages/ValidateLoginPage/index.tsx b/src/pages/ValidateLoginPage/index.tsx index 597a025cf602..c228e86a7928 100644 --- a/src/pages/ValidateLoginPage/index.tsx +++ b/src/pages/ValidateLoginPage/index.tsx @@ -1,19 +1,10 @@ -import type {StackScreenProps} from '@react-navigation/stack'; import React, {useEffect} from 'react'; -import {OnyxEntry, withOnyx} from 'react-native-onyx'; +import {withOnyx} from 'react-native-onyx'; import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import Navigation from '@libs/Navigation/Navigation'; -import type {AuthScreensParamList} from '@libs/Navigation/types'; import * as Session from '@userActions/Session'; import ONYXKEYS from '@src/ONYXKEYS'; -import SCREENS from '@src/SCREENS'; -import type {Session as SessionType} from '@src/types/onyx'; - -type ValidateLoginPageOnyxProps = { - session: OnyxEntry; -}; - -type ValidateLoginPageProps = ValidateLoginPageOnyxProps & StackScreenProps; +import type {ValidateLoginPageOnyxProps, ValidateLoginPageProps} from './types'; function ValidateLoginPage({ route: { @@ -37,6 +28,6 @@ function ValidateLoginPage({ ValidateLoginPage.displayName = 'ValidateLoginPage'; -export default withOnyx({ +export default withOnyx>({ session: {key: ONYXKEYS.SESSION}, })(ValidateLoginPage); diff --git a/src/pages/ValidateLoginPage/index.website.tsx b/src/pages/ValidateLoginPage/index.website.tsx index 12e680172198..699da83bae2d 100644 --- a/src/pages/ValidateLoginPage/index.website.tsx +++ b/src/pages/ValidateLoginPage/index.website.tsx @@ -1,30 +1,14 @@ -import type {StackScreenProps} from '@react-navigation/stack'; import React, {useEffect} from 'react'; -import {OnyxEntry, withOnyx} from 'react-native-onyx'; +import {withOnyx} from 'react-native-onyx'; import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import ExpiredValidateCodeModal from '@components/ValidateCode/ExpiredValidateCodeModal'; import JustSignedInModal from '@components/ValidateCode/JustSignedInModal'; import ValidateCodeModal from '@components/ValidateCode/ValidateCodeModal'; import Navigation from '@libs/Navigation/Navigation'; -import type {AuthScreensParamList} from '@libs/Navigation/types'; import * as Session from '@userActions/Session'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import SCREENS from '@src/SCREENS'; -import type {Account, Credentials, Session as SessionType} from '@src/types/onyx'; - -type ValidateLoginPageOnyxProps = { - /** The details about the account that the user is signing in with */ - account: OnyxEntry; - - /** The credentials of the person logging in */ - credentials: OnyxEntry; - - /** Session of currently logged in user */ - session: OnyxEntry; -}; - -type ValidateLoginPageProps = ValidateLoginPageOnyxProps & StackScreenProps; +import type {ValidateLoginPageOnyxProps, ValidateLoginPageProps} from './types'; function ValidateLoginPage({account, credentials, route, session}: ValidateLoginPageProps) { const login = credentials?.login; diff --git a/src/pages/ValidateLoginPage/types.ts b/src/pages/ValidateLoginPage/types.ts new file mode 100644 index 000000000000..3173c73796b6 --- /dev/null +++ b/src/pages/ValidateLoginPage/types.ts @@ -0,0 +1,20 @@ +import type {StackScreenProps} from '@react-navigation/stack'; +import type {OnyxEntry} from 'react-native-onyx'; +import type {AuthScreensParamList} from '@libs/Navigation/types'; +import type SCREENS from '@src/SCREENS'; +import type {Account, Credentials, Session} from '@src/types/onyx'; + +type ValidateLoginPageOnyxProps = { + /** The details about the account that the user is signing in with */ + account: OnyxEntry; + + /** The credentials of the person logging in */ + credentials: OnyxEntry; + + /** Session of currently logged in user */ + session: OnyxEntry; +}; + +type ValidateLoginPageProps = ValidateLoginPageOnyxProps & StackScreenProps; + +export type {ValidateLoginPageOnyxProps, ValidateLoginPageProps}; From 882b8c6e504d359246f6f5e30360b48136865228 Mon Sep 17 00:00:00 2001 From: hurali97 Date: Wed, 10 Jan 2024 14:29:29 +0500 Subject: [PATCH 191/580] 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 192/580] 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 193/580] 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 32d5cc9e822bd78606c1372b126183baedecf5c6 Mon Sep 17 00:00:00 2001 From: Kamil Owczarz Date: Wed, 10 Jan 2024 15:34:32 +0100 Subject: [PATCH 194/580] 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 029493228a4b120d40646c922f2cbf61008bcff2 Mon Sep 17 00:00:00 2001 From: Kamil Owczarz Date: Wed, 10 Jan 2024 16:05:16 +0100 Subject: [PATCH 195/580] 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 196/580] 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 197/580] 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 198/580] 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 0578bb6038c913d3b79949e178201aaa4828b8d1 Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Thu, 11 Jan 2024 10:34:43 +0100 Subject: [PATCH 199/580] remove comments, lint code --- src/components/MagicCodeInput.tsx | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/components/MagicCodeInput.tsx b/src/components/MagicCodeInput.tsx index e81e66e87e30..f22c56199f45 100644 --- a/src/components/MagicCodeInput.tsx +++ b/src/components/MagicCodeInput.tsx @@ -1,7 +1,10 @@ -import React, {ForwardedRef, forwardRef, useEffect, useImperativeHandle, useRef, useState} from 'react'; -import {NativeSyntheticEvent, StyleSheet, TextInputFocusEventData, TextInputKeyPressEventData, TextInputProps, View} from 'react-native'; -import {HandlerStateChangeEvent, TapGestureHandler, TouchData} from 'react-native-gesture-handler'; -import {AnimatedProps} from 'react-native-reanimated'; +import type {ForwardedRef} from 'react'; +import React, { forwardRef, useEffect, useImperativeHandle, useRef, useState} from 'react'; +import type {NativeSyntheticEvent, TextInputFocusEventData, TextInputKeyPressEventData, TextInputProps} from 'react-native'; +import { StyleSheet, View, TextInput} from 'react-native'; +import type {HandlerStateChangeEvent, TouchData} from 'react-native-gesture-handler'; +import { TapGestureHandler} from 'react-native-gesture-handler'; +import type {AnimatedProps} from 'react-native-reanimated'; import useNetwork from '@hooks/useNetwork'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -104,7 +107,6 @@ function MagicCodeInput(props: MagicCodeInputProps, ref: ForwardedRef> | null>(); const inputRefs = useRef> | null>(); const [input, setInput] = useState(TEXT_INPUT_EMPTY_STATE); const [focusedIndex, setFocusedIndex] = useState(0); From 35155fd1772c1c1d840cdaff9f1290b693b67507 Mon Sep 17 00:00:00 2001 From: hurali97 Date: Thu, 11 Jan 2024 16:50:28 +0500 Subject: [PATCH 200/580] 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 485de839d484adad36a9588afd4540d8ec23b179 Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Thu, 11 Jan 2024 13:45:25 +0100 Subject: [PATCH 201/580] wrap ref calls with if statements --- src/components/MagicCodeInput.tsx | 36 ++++++++++++++++++++++--------- 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/src/components/MagicCodeInput.tsx b/src/components/MagicCodeInput.tsx index f22c56199f45..906fba53447d 100644 --- a/src/components/MagicCodeInput.tsx +++ b/src/components/MagicCodeInput.tsx @@ -1,9 +1,9 @@ import type {ForwardedRef} from 'react'; -import React, { forwardRef, useEffect, useImperativeHandle, useRef, useState} from 'react'; +import React, {forwardRef, useEffect, useImperativeHandle, useRef, useState} from 'react'; import type {NativeSyntheticEvent, TextInputFocusEventData, TextInputKeyPressEventData, TextInputProps} from 'react-native'; -import { StyleSheet, View, TextInput} from 'react-native'; +import {StyleSheet, View} from 'react-native'; import type {HandlerStateChangeEvent, TouchData} from 'react-native-gesture-handler'; -import { TapGestureHandler} from 'react-native-gesture-handler'; +import {TapGestureHandler} from 'react-native-gesture-handler'; import type {AnimatedProps} from 'react-native-reanimated'; import useNetwork from '@hooks/useNetwork'; import useStyleUtils from '@hooks/useStyleUtils'; @@ -122,7 +122,9 @@ function MagicCodeInput(props: MagicCodeInputProps, ref: ForwardedRef { - inputRefs.current?.blur(); + if (inputRefs.current && 'blur' in inputRefs.current) { + inputRefs.current?.blur(); + } setFocusedIndex(undefined); }; @@ -130,7 +132,9 @@ function MagicCodeInput(props: MagicCodeInputProps, ref: ForwardedRef { @@ -144,6 +148,9 @@ function MagicCodeInput(props: MagicCodeInputProps, ref: ForwardedRef { const numbers = decomposeString(value, maxLength); + // eslint-disable-next-line @typescript-eslint/no-use-before-define if (wasSubmitted || !shouldSubmitOnComplete || numbers.filter((n) => ValidationUtils.isNumeric(n)).length !== maxLength || isOffline) { return; } @@ -212,7 +222,9 @@ function MagicCodeInput(props: MagicCodeInputProps, ref: ForwardedRef Date: Thu, 11 Jan 2024 15:45:41 +0100 Subject: [PATCH 202/580] [TS migration] Migrate 'ReportActionItemReportPreview.js' component --- .../{ReportPreview.js => ReportPreview.tsx} | 295 ++++++++---------- src/libs/ReportUtils.ts | 2 +- src/libs/onyxSubscribe.ts | 4 +- .../ContextMenu/ReportActionContextMenu.ts | 7 +- 4 files changed, 143 insertions(+), 165 deletions(-) rename src/components/ReportActionItem/{ReportPreview.js => ReportPreview.tsx} (61%) diff --git a/src/components/ReportActionItem/ReportPreview.js b/src/components/ReportActionItem/ReportPreview.tsx similarity index 61% rename from src/components/ReportActionItem/ReportPreview.js rename to src/components/ReportActionItem/ReportPreview.tsx index abc7e3954200..cd2c87a3a585 100644 --- a/src/components/ReportActionItem/ReportPreview.js +++ b/src/components/ReportActionItem/ReportPreview.tsx @@ -1,23 +1,19 @@ -import lodashGet from 'lodash/get'; -import PropTypes from 'prop-types'; import React, {useEffect, useMemo, useState} from 'react'; import {View} from 'react-native'; +import type {StyleProp, ViewStyle} from 'react-native'; import {withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; +import type {OnyxEntry} from 'react-native-onyx/lib/types'; import Button from '@components/Button'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; -import refPropTypes from '@components/refPropTypes'; import SettlementButton from '@components/SettlementButton'; import {showContextMenuForReport} from '@components/ShowContextMenuContext'; import Text from '@components/Text'; -import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; import useLocalize from '@hooks/useLocalize'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; -import compose from '@libs/compose'; import ControlSelection from '@libs/ControlSelection'; import * as CurrencyUtils from '@libs/CurrencyUtils'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; @@ -27,103 +23,78 @@ import * as ReceiptUtils from '@libs/ReceiptUtils'; import * as ReportActionUtils from '@libs/ReportActionsUtils'; import * as ReportUtils from '@libs/ReportUtils'; import * as TransactionUtils from '@libs/TransactionUtils'; -import reportActionPropTypes from '@pages/home/report/reportActionPropTypes'; -import reportPropTypes from '@pages/reportPropTypes'; +import type {ContextMenuAnchor} from '@pages/home/report/ContextMenu/ReportActionContextMenu'; import * as IOU from '@userActions/IOU'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import type {Policy, Report, ReportAction, Session} from '@src/types/onyx'; +import type DeepValueOf from '@src/types/utils/DeepValueOf'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; import ReportActionItemImages from './ReportActionItemImages'; -const propTypes = { - /** All the data of the action */ - action: PropTypes.shape(reportActionPropTypes).isRequired, - - /** The associated chatReport */ - chatReportID: PropTypes.string.isRequired, - - /** The active IOUReport, used for Onyx subscription */ - // eslint-disable-next-line react/no-unused-prop-types - iouReportID: PropTypes.string.isRequired, +type PaymentVerbTranslationPath = 'iou.payerSpent' | 'iou.payerOwes' | 'iou.payerPaid'; - /** The report's policyID, used for Onyx subscription */ - policyID: PropTypes.string.isRequired, +type PaymentMethodType = DeepValueOf; +type ReportPreviewOnyxProps = { /** The policy tied to the money request report */ - policy: PropTypes.shape({ - /** Name of the policy */ - name: PropTypes.string, - - /** Type of the policy */ - type: PropTypes.string, - - /** The role of the current user in the policy */ - role: PropTypes.string, + policy: OnyxEntry; - /** Whether Scheduled Submit is turned on for this policy */ - isHarvestingEnabled: PropTypes.bool, - }), - - /* Onyx Props */ - /** chatReport associated with iouReport */ - chatReport: reportPropTypes, - - /** Extra styles to pass to View wrapper */ - // eslint-disable-next-line react/forbid-prop-types - containerStyles: PropTypes.arrayOf(PropTypes.object), + /** ChatReport associated with iouReport */ + chatReport: OnyxEntry; /** Active IOU Report for current report */ - iouReport: PropTypes.shape({ - /** AccountID of the manager in this iou report */ - managerID: PropTypes.number, + iouReport: OnyxEntry; - /** AccountID of the creator of this iou report */ - ownerAccountID: PropTypes.number, + /** Session info for the currently logged in user. */ + session: OnyxEntry; +}; - /** Outstanding amount in cents of this transaction */ - total: PropTypes.number, +type ReportPreviewProps = ReportPreviewOnyxProps & { + /** All the data of the action */ + action: ReportAction; - /** Currency of outstanding amount of this transaction */ - currency: PropTypes.string, + /** The associated chatReport */ + chatReportID: string; - /** Is the iouReport waiting for the submitter to add a credit bank account? */ - isWaitingOnBankAccount: PropTypes.bool, - }), + /** The active IOUReport, used for Onyx subscription */ + iouReportID: string; - /** Session info for the currently logged in user. */ - session: PropTypes.shape({ - /** Currently logged in user accountID */ - accountID: PropTypes.number, - }), + /** The report's policyID, used for Onyx subscription */ + policyID: string; + + /** Extra styles to pass to View wrapper */ + containerStyles?: StyleProp; /** Popover context menu anchor, used for showing context menu */ - contextMenuAnchor: refPropTypes, + contextMenuAnchor?: ContextMenuAnchor; /** Callback for updating context menu active state, used for showing context menu */ - checkIfContextMenuActive: PropTypes.func, + checkIfContextMenuActive?: () => void; /** Whether a message is a whisper */ - isWhisper: PropTypes.bool, + isWhisper?: boolean; - ...withLocalizePropTypes, + /** Whether the corresponding report action item is hovered */ + isHovered?: boolean; }; -const defaultProps = { - contextMenuAnchor: null, - chatReport: {}, - containerStyles: [], - iouReport: {}, - checkIfContextMenuActive: () => {}, - session: { - accountID: null, - }, - isWhisper: false, - policy: { - isHarvestingEnabled: false, - }, -}; - -function ReportPreview(props) { +function ReportPreview({ + iouReport, + session, + policy, + iouReportID, + policyID, + chatReportID, + chatReport, + action, + containerStyles, + contextMenuAnchor, + isHovered = false, + isWhisper = false, + checkIfContextMenuActive = () => {}, +}: ReportPreviewProps) { const theme = useTheme(); const styles = useThemeStyles(); const {translate} = useLocalize(); @@ -133,35 +104,37 @@ function ReportPreview(props) { const [hasOnlyDistanceRequests, setHasOnlyDistanceRequests] = useState(false); const [hasNonReimbursableTransactions, setHasNonReimbursableTransactions] = useState(false); - const managerID = props.iouReport.managerID || 0; - const isCurrentUserManager = managerID === lodashGet(props.session, 'accountID'); - const {totalDisplaySpend, reimbursableSpend} = ReportUtils.getMoneyRequestSpendBreakdown(props.iouReport); - const policyType = lodashGet(props.policy, 'type'); - - const iouSettled = ReportUtils.isSettled(props.iouReportID); - const iouCanceled = ReportUtils.isArchivedRoom(props.chatReport); - const numberOfRequests = ReportActionUtils.getNumberOfMoneyRequests(props.action); - const moneyRequestComment = lodashGet(props.action, 'childLastMoneyRequestComment', ''); - const isPolicyExpenseChat = ReportUtils.isPolicyExpenseChat(props.chatReport); - const isDraftExpenseReport = isPolicyExpenseChat && ReportUtils.isDraftExpenseReport(props.iouReport); - - const isApproved = ReportUtils.isReportApproved(props.iouReport); - const isMoneyRequestReport = ReportUtils.isMoneyRequestReport(props.iouReport); - const transactionsWithReceipts = ReportUtils.getTransactionsWithReceipts(props.iouReportID); - const numberOfScanningReceipts = _.filter(transactionsWithReceipts, (transaction) => TransactionUtils.isReceiptBeingScanned(transaction)).length; + const managerID = iouReport?.managerID ?? 0; + const isCurrentUserManager = managerID === session?.accountID; + const {totalDisplaySpend, reimbursableSpend} = ReportUtils.getMoneyRequestSpendBreakdown(iouReport); + const policyType = policy?.type; + + const iouSettled = ReportUtils.isSettled(iouReportID); + const iouCanceled = ReportUtils.isArchivedRoom(chatReport); + const numberOfRequests = ReportActionUtils.getNumberOfMoneyRequests(action); + const moneyRequestComment = action?.childLastMoneyRequestComment ?? ''; + const isPolicyExpenseChat = ReportUtils.isPolicyExpenseChat(chatReport); + const isDraftExpenseReport = isPolicyExpenseChat && ReportUtils.isDraftExpenseReport(iouReport); + + const isApproved = ReportUtils.isReportApproved(iouReport); + const isMoneyRequestReport = ReportUtils.isMoneyRequestReport(iouReport); + const transactionsWithReceipts = ReportUtils.getTransactionsWithReceipts(iouReportID); + const numberOfScanningReceipts = transactionsWithReceipts.filter((transaction) => TransactionUtils.isReceiptBeingScanned(transaction)).length; const hasReceipts = transactionsWithReceipts.length > 0; const isScanning = hasReceipts && areAllRequestsBeingSmartScanned; const hasErrors = hasReceipts && hasMissingSmartscanFields; const lastThreeTransactionsWithReceipts = transactionsWithReceipts.slice(-3); - const lastThreeReceipts = _.map(lastThreeTransactionsWithReceipts, (transaction) => ReceiptUtils.getThumbnailAndImageURIs(transaction)); + const lastThreeReceipts = lastThreeTransactionsWithReceipts.map((transaction) => ReceiptUtils.getThumbnailAndImageURIs(transaction)); let formattedMerchant = numberOfRequests === 1 && hasReceipts ? TransactionUtils.getMerchant(transactionsWithReceipts[0]) : null; - const hasPendingWaypoints = formattedMerchant && hasOnlyDistanceRequests && _.every(transactionsWithReceipts, (transaction) => lodashGet(transaction, 'pendingFields.waypoints', null)); - if (hasPendingWaypoints) { - formattedMerchant = formattedMerchant.replace(CONST.REGEX.FIRST_SPACE, props.translate('common.tbd')); + const hasPendingWaypoints = formattedMerchant && hasOnlyDistanceRequests && transactionsWithReceipts.every((transaction) => transaction.pendingFields?.waypoints); + if (formattedMerchant && hasPendingWaypoints) { + formattedMerchant = formattedMerchant.replace(CONST.REGEX.FIRST_SPACE, translate('common.tbd')); } const previewSubtitle = + // Formatted merchant can be an empty string + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing formattedMerchant || - props.translate('iou.requestCount', { + translate('iou.requestCount', { count: numberOfRequests, scanningReceipts: numberOfScanningReceipts, }); @@ -170,67 +143,71 @@ function ReportPreview(props) { // The submit button should be success green colour only if the user is submitter and the policy does not have Scheduled Submit turned on const isWaitingForSubmissionFromCurrentUser = useMemo( - () => props.chatReport.isOwnPolicyExpenseChat && !props.policy.isHarvestingEnabled, - [props.chatReport.isOwnPolicyExpenseChat, props.policy.isHarvestingEnabled], + () => chatReport?.isOwnPolicyExpenseChat && !policy?.isHarvestingEnabled, + [chatReport?.isOwnPolicyExpenseChat, policy?.isHarvestingEnabled], ); - const getDisplayAmount = () => { + const getDisplayAmount = (): string => { if (hasPendingWaypoints) { - return props.translate('common.tbd'); + return translate('common.tbd'); } if (totalDisplaySpend) { - return CurrencyUtils.convertToDisplayString(totalDisplaySpend, props.iouReport.currency); + return CurrencyUtils.convertToDisplayString(totalDisplaySpend, iouReport?.currency); } if (isScanning) { - return props.translate('iou.receiptScanning'); + return translate('iou.receiptScanning'); } if (hasOnlyDistanceRequests) { - return props.translate('common.tbd'); + return translate('common.tbd'); } // If iouReport is not available, get amount from the action message (Ex: "Domain20821's Workspace owes $33.00" or "paid ₫60" or "paid -₫60 elsewhere") let displayAmount = ''; - const actionMessage = lodashGet(props.action, ['message', 0, 'text'], ''); + const actionMessage = action.message?.[0]?.text ?? ''; const splits = actionMessage.split(' '); - for (let i = 0; i < splits.length; i++) { - if (/\d/.test(splits[i])) { - displayAmount = splits[i]; + + splits.forEach((split) => { + if (!/\d/.test(split)) { + return; } - } + + displayAmount = split; + }); + return displayAmount; }; const getPreviewMessage = () => { if (isScanning) { - return props.translate('common.receipt'); + return translate('common.receipt'); } - const payerOrApproverName = isPolicyExpenseChat ? ReportUtils.getPolicyName(props.chatReport) : ReportUtils.getDisplayNameForParticipant(managerID, true); + const payerOrApproverName = isPolicyExpenseChat ? ReportUtils.getPolicyName(chatReport) : ReportUtils.getDisplayNameForParticipant(managerID, true); if (isApproved) { - return props.translate('iou.managerApproved', {manager: payerOrApproverName}); + return translate('iou.managerApproved', {manager: payerOrApproverName}); } - const managerName = isPolicyExpenseChat ? ReportUtils.getPolicyName(props.chatReport) : ReportUtils.getDisplayNameForParticipant(managerID, true); - let paymentVerb = hasNonReimbursableTransactions ? 'iou.payerSpent' : 'iou.payerOwes'; - if (iouSettled || props.iouReport.isWaitingOnBankAccount) { + const managerName = isPolicyExpenseChat ? ReportUtils.getPolicyName(chatReport) : ReportUtils.getDisplayNameForParticipant(managerID, true); + let paymentVerb: PaymentVerbTranslationPath = hasNonReimbursableTransactions ? 'iou.payerSpent' : 'iou.payerOwes'; + if (iouSettled || iouReport?.isWaitingOnBankAccount) { paymentVerb = 'iou.payerPaid'; } - return props.translate(paymentVerb, {payer: managerName}); + return translate(paymentVerb, {payer: managerName}); }; - const bankAccountRoute = ReportUtils.getBankAccountRoute(props.chatReport); + const bankAccountRoute = ReportUtils.getBankAccountRoute(chatReport); useEffect(() => { const unsubscribeOnyxTransaction = onyxSubscribe({ key: ONYXKEYS.COLLECTION.TRANSACTION, waitForCollectionCallback: true, callback: (allTransactions) => { - if (_.isEmpty(allTransactions)) { + if (isEmptyObject(allTransactions)) { return; } - sethasMissingSmartscanFields(ReportUtils.hasMissingSmartscanFields(props.iouReportID)); - setAreAllRequestsBeingSmartScanned(ReportUtils.areAllRequestsBeingSmartScanned(props.iouReportID, props.action)); - setHasOnlyDistanceRequests(ReportUtils.hasOnlyDistanceRequestTransactions(props.iouReportID)); - setHasNonReimbursableTransactions(ReportUtils.hasNonReimbursableTransactions(props.iouReportID)); + sethasMissingSmartscanFields(ReportUtils.hasMissingSmartscanFields(iouReportID)); + setAreAllRequestsBeingSmartScanned(ReportUtils.areAllRequestsBeingSmartScanned(iouReportID, action)); + setHasOnlyDistanceRequests(ReportUtils.hasOnlyDistanceRequestTransactions(iouReportID)); + setHasNonReimbursableTransactions(ReportUtils.hasNonReimbursableTransactions(iouReportID)); }, }); @@ -240,15 +217,15 @@ function ReportPreview(props) { // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - const isPaidGroupPolicy = ReportUtils.isPaidGroupPolicyExpenseChat(props.chatReport); - const isPolicyAdmin = policyType !== CONST.POLICY.TYPE.PERSONAL && lodashGet(props.policy, 'role') === CONST.POLICY.ROLE.ADMIN; + const isPaidGroupPolicy = ReportUtils.isPaidGroupPolicyExpenseChat(chatReport); + const isPolicyAdmin = policyType !== CONST.POLICY.TYPE.PERSONAL && policy?.role === CONST.POLICY.ROLE.ADMIN; const isPayer = isPaidGroupPolicy ? // In a paid group policy, the admin approver can pay the report directly by skipping the approval step isPolicyAdmin && (isApproved || isCurrentUserManager) : isPolicyAdmin || (isMoneyRequestReport && isCurrentUserManager); const shouldShowPayButton = useMemo( - () => isPayer && !isDraftExpenseReport && !iouSettled && !props.iouReport.isWaitingOnBankAccount && reimbursableSpend !== 0 && !iouCanceled, - [isPayer, isDraftExpenseReport, iouSettled, reimbursableSpend, iouCanceled, props.iouReport], + () => isPayer && !isDraftExpenseReport && !iouSettled && !iouReport?.isWaitingOnBankAccount && reimbursableSpend !== 0 && !iouCanceled, + [isPayer, isDraftExpenseReport, iouSettled, reimbursableSpend, iouCanceled, iouReport], ); const shouldShowApproveButton = useMemo(() => { if (!isPaidGroupPolicy) { @@ -258,25 +235,27 @@ function ReportPreview(props) { }, [isPaidGroupPolicy, isCurrentUserManager, isDraftExpenseReport, isApproved, iouSettled]); const shouldShowSettlementButton = shouldShowPayButton || shouldShowApproveButton; return ( - - + + { - Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(props.iouReportID)); + Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(iouReportID)); }} onPressIn={() => DeviceCapabilities.canUseTouchScreen() && ControlSelection.block()} onPressOut={() => ControlSelection.unblock()} - onLongPress={(event) => showContextMenuForReport(event, props.contextMenuAnchor, props.chatReportID, props.action, props.checkIfContextMenuActive)} + // @ts-expect-error TODO: Remove this once ShowContextMenuContext (https://github.com/Expensify/App/issues/24980) is migrated to TypeScript. + onLongPress={(event) => showContextMenuForReport(event, contextMenuAnchor, chatReportID, action, checkIfContextMenuActive)} style={[styles.flexRow, styles.justifyContentBetween, styles.reportPreviewBox]} role="button" - accessibilityLabel={props.translate('iou.viewDetails')} + accessibilityLabel={translate('iou.viewDetails')} > - + {hasReceipts && ( )} @@ -295,7 +274,7 @@ function ReportPreview(props) { {getDisplayAmount()} - {ReportUtils.isSettled(props.iouReportID) && ( + {ReportUtils.isSettled(iouReportID) && ( IOU.payMoneyRequest(paymentType, props.chatReport, props.iouReport)} + // @ts-expect-error TODO: Remove this once SettlementButton (https://github.com/Expensify/App/issues/25100) is migrated to TypeScript. + currency={iouReport?.currency} + policyID={policyID} + chatReportID={chatReportID} + iouReport={iouReport} + onPress={(paymentType: PaymentMethodType) => chatReport && iouReport && IOU.payMoneyRequest(paymentType, chatReport, iouReport)} enablePaymentsRoute={ROUTES.ENABLE_PAYMENTS} addBankAccountRoute={bankAccountRoute} shouldHidePaymentOptions={!shouldShowPayButton} @@ -340,7 +320,7 @@ function ReportPreview(props) { success={isWaitingForSubmissionFromCurrentUser} text={translate('common.submit')} style={styles.mt3} - onPress={() => IOU.submitReport(props.iouReport)} + onPress={() => iouReport && IOU.submitReport(iouReport)} /> )} @@ -351,24 +331,19 @@ function ReportPreview(props) { ); } -ReportPreview.propTypes = propTypes; -ReportPreview.defaultProps = defaultProps; ReportPreview.displayName = 'ReportPreview'; -export default compose( - withLocalize, - withOnyx({ - policy: { - key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, - }, - chatReport: { - key: ({chatReportID}) => `${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`, - }, - iouReport: { - key: ({iouReportID}) => `${ONYXKEYS.COLLECTION.REPORT}${iouReportID}`, - }, - session: { - key: ONYXKEYS.SESSION, - }, - }), -)(ReportPreview); +export default withOnyx({ + policy: { + key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + }, + chatReport: { + key: ({chatReportID}) => `${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`, + }, + iouReport: { + key: ({iouReportID}) => `${ONYXKEYS.COLLECTION.REPORT}${iouReportID}`, + }, + session: { + key: ONYXKEYS.SESSION, + }, +})(ReportPreview); diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index e619cb3c80dd..95c64f9b5e21 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -1488,7 +1488,7 @@ function getPersonalDetailsForAccountID(accountID: number): Partial(mapping: ConnectOptions) { +function onyxSubscribe(mapping: ConnectOptions) { const connectionId = Onyx.connect(mapping); return () => Onyx.disconnect(connectionId); } diff --git a/src/pages/home/report/ContextMenu/ReportActionContextMenu.ts b/src/pages/home/report/ContextMenu/ReportActionContextMenu.ts index 5b64d90da5da..9553d8207a2f 100644 --- a/src/pages/home/report/ContextMenu/ReportActionContextMenu.ts +++ b/src/pages/home/report/ContextMenu/ReportActionContextMenu.ts @@ -14,11 +14,13 @@ type OnCancel = () => void; type ContextMenuType = ValueOf; +type ContextMenuAnchor = View | RNText | null; + type ShowContextMenu = ( type: ContextMenuType, event: GestureResponderEvent | MouseEvent, selection: string, - contextMenuAnchor: View | RNText | null, + contextMenuAnchor: ContextMenuAnchor, reportID?: string, reportActionID?: string, originalReportID?: string, @@ -96,7 +98,7 @@ function showContextMenu( type: ContextMenuType, event: GestureResponderEvent | MouseEvent, selection: string, - contextMenuAnchor: View | RNText | null, + contextMenuAnchor: ContextMenuAnchor, reportID = '0', reportActionID = '0', originalReportID = '0', @@ -175,3 +177,4 @@ function clearActiveReportAction() { } export {contextMenuRef, showContextMenu, hideContextMenu, isActiveReportAction, clearActiveReportAction, showDeleteModal, hideDeleteModal}; +export type {ContextMenuAnchor}; From 7c61f38b1d74a27d31ed4d4c72d5f6975c28ea72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Thu, 11 Jan 2024 16:00:55 +0000 Subject: [PATCH 203/580] Fix typings --- src/components/Composer/index.tsx | 5 +-- src/components/MagicCodeInput.tsx | 37 ++++++------------- src/components/RNTextInput.tsx | 12 ++---- .../TextInput/BaseTextInput/index.native.tsx | 7 ++-- .../TextInput/BaseTextInput/index.tsx | 7 ++-- .../TextInput/BaseTextInput/types.ts | 9 ++--- 6 files changed, 27 insertions(+), 50 deletions(-) diff --git a/src/components/Composer/index.tsx b/src/components/Composer/index.tsx index 19b7bb6bb30a..33edd1f5d8cc 100755 --- a/src/components/Composer/index.tsx +++ b/src/components/Composer/index.tsx @@ -3,9 +3,8 @@ 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, TextInput, TextInputKeyPressEventData, TextInputSelectionChangeEventData} from 'react-native'; import {StyleSheet, View} from 'react-native'; -import type {AnimatedProps} from 'react-native-reanimated'; import RNTextInput from '@components/RNTextInput'; import Text from '@components/Text'; import useIsScrollBarVisible from '@hooks/useIsScrollBarVisible'; @@ -74,7 +73,7 @@ function Composer( shouldContainScroll = false, ...props }: ComposerProps, - ref: ForwardedRef>>, + ref: ForwardedRef, ) { const theme = useTheme(); const styles = useThemeStyles(); diff --git a/src/components/MagicCodeInput.tsx b/src/components/MagicCodeInput.tsx index 906fba53447d..727db31cb4a3 100644 --- a/src/components/MagicCodeInput.tsx +++ b/src/components/MagicCodeInput.tsx @@ -1,10 +1,9 @@ import type {ForwardedRef} from 'react'; import React, {forwardRef, useEffect, useImperativeHandle, useRef, useState} from 'react'; -import type {NativeSyntheticEvent, TextInputFocusEventData, TextInputKeyPressEventData, TextInputProps} from 'react-native'; +import type {NativeSyntheticEvent, TextInput as RNTextInput, TextInputFocusEventData, TextInputKeyPressEventData} from 'react-native'; import {StyleSheet, View} from 'react-native'; import type {HandlerStateChangeEvent, TouchData} from 'react-native-gesture-handler'; import {TapGestureHandler} from 'react-native-gesture-handler'; -import type {AnimatedProps} from 'react-native-reanimated'; import useNetwork from '@hooks/useNetwork'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -107,7 +106,7 @@ function MagicCodeInput(props: MagicCodeInputProps, ref: ForwardedRef> | null>(); + const inputRefs = useRef(); const [input, setInput] = useState(TEXT_INPUT_EMPTY_STATE); const [focusedIndex, setFocusedIndex] = useState(0); const [editIndex, setEditIndex] = useState(0); @@ -122,9 +121,7 @@ function MagicCodeInput(props: MagicCodeInputProps, ref: ForwardedRef { - if (inputRefs.current && 'blur' in inputRefs.current) { - inputRefs.current?.blur(); - } + inputRefs.current?.blur(); setFocusedIndex(undefined); }; @@ -132,9 +129,7 @@ function MagicCodeInput(props: MagicCodeInputProps, ref: ForwardedRef { @@ -148,9 +143,6 @@ function MagicCodeInput(props: MagicCodeInputProps, ref: ForwardedRef>; // 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>>) { +function RNTextInputWithRef(props: TextInputProps, ref: ForwardedRef) { const theme = useTheme(); return ( @@ -23,7 +21,7 @@ function RNTextInputWithRef(props: TextInputProps, ref: ForwardedRef(null); const labelScale = useRef(new Animated.Value(initialActiveLabel ? styleConst.ACTIVE_LABEL_SCALE : styleConst.INACTIVE_LABEL_SCALE)).current; const labelTranslateY = useRef(new Animated.Value(initialActiveLabel ? styleConst.ACTIVE_LABEL_TRANSLATE_Y : styleConst.INACTIVE_LABEL_TRANSLATE_Y)).current; - const input = useRef(null); + const input = useRef(null); const isLabelActive = useRef(initialActiveLabel); // AutoFocus which only works on mount: @@ -322,7 +321,7 @@ function BaseTextInput( ref.current = element; } - (input.current as AnimatedTextInputRef | null) = element; + input.current = element; }} // eslint-disable-next-line {...inputProps} diff --git a/src/components/TextInput/BaseTextInput/index.tsx b/src/components/TextInput/BaseTextInput/index.tsx index 9c3899979aaa..cd770b7a003c 100644 --- a/src/components/TextInput/BaseTextInput/index.tsx +++ b/src/components/TextInput/BaseTextInput/index.tsx @@ -1,13 +1,12 @@ import Str from 'expensify-common/lib/str'; import React, {forwardRef, useCallback, useEffect, useMemo, useRef, useState} from 'react'; +import type {GestureResponderEvent, LayoutChangeEvent, NativeSyntheticEvent, StyleProp, TextInputFocusEventData, ViewStyle} from 'react-native'; import {ActivityIndicator, Animated, StyleSheet, View} from 'react-native'; -import type {GestureResponderEvent, LayoutChangeEvent, NativeSyntheticEvent, StyleProp, TextInput, TextInputFocusEventData, ViewStyle} from 'react-native'; import Checkbox from '@components/Checkbox'; import FormHelpMessage from '@components/FormHelpMessage'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; -import type {AnimatedTextInputRef} from '@components/RNTextInput'; import RNTextInput from '@components/RNTextInput'; import SwipeInterceptPanResponder from '@components/SwipeInterceptPanResponder'; import Text from '@components/Text'; @@ -79,7 +78,7 @@ function BaseTextInput( const [width, setWidth] = useState(null); const labelScale = useRef(new Animated.Value(initialActiveLabel ? styleConst.ACTIVE_LABEL_SCALE : styleConst.INACTIVE_LABEL_SCALE)).current; const labelTranslateY = useRef(new Animated.Value(initialActiveLabel ? styleConst.ACTIVE_LABEL_TRANSLATE_Y : styleConst.INACTIVE_LABEL_TRANSLATE_Y)).current; - const input = useRef(null); + const input = useRef(null); const isLabelActive = useRef(initialActiveLabel); // AutoFocus which only works on mount: @@ -337,7 +336,7 @@ function BaseTextInput( ref.current = element; } - (input.current as AnimatedTextInputRef | null) = element; + input.current = element as HTMLInputElement | null; }} // eslint-disable-next-line {...inputProps} diff --git a/src/components/TextInput/BaseTextInput/types.ts b/src/components/TextInput/BaseTextInput/types.ts index 21875d4dcc64..79746ac9a55e 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 {ForwardedRef} from 'react'; +import type {GestureResponderEvent, StyleProp, TextInput, TextInputProps, TextStyle, ViewStyle} from 'react-native'; import type {MaybePhraseKey} from '@libs/Localize'; import type IconAsset from '@src/types/utils/IconAsset'; @@ -111,8 +110,8 @@ type CustomBaseTextInputProps = { autoCompleteType?: string; }; -type BaseTextInputRef = ForwardedRef>>; +type BaseTextInputRef = ForwardedRef; type BaseTextInputProps = CustomBaseTextInputProps & TextInputProps; -export type {CustomBaseTextInputProps, BaseTextInputRef, BaseTextInputProps}; +export type {BaseTextInputProps, BaseTextInputRef, CustomBaseTextInputProps}; From 51a21e516fcec45117b015c598e26026b9a9c6b5 Mon Sep 17 00:00:00 2001 From: Pedro Guerreiro Date: Thu, 11 Jan 2024 18:08:00 +0000 Subject: [PATCH 204/580] chore(typescript): fix wrong prop types --- src/pages/ValidateLoginPage/index.tsx | 6 +++--- src/pages/ValidateLoginPage/index.website.tsx | 4 ++-- src/pages/ValidateLoginPage/types.ts | 16 +++++++++------- 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/src/pages/ValidateLoginPage/index.tsx b/src/pages/ValidateLoginPage/index.tsx index c228e86a7928..1af5000ed801 100644 --- a/src/pages/ValidateLoginPage/index.tsx +++ b/src/pages/ValidateLoginPage/index.tsx @@ -4,14 +4,14 @@ import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import Navigation from '@libs/Navigation/Navigation'; import * as Session from '@userActions/Session'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {ValidateLoginPageOnyxProps, ValidateLoginPageProps} from './types'; +import type {ValidateLoginPageOnyxNativeProps, ValidateLoginPageProps} from './types'; function ValidateLoginPage({ route: { params: {accountID = '', validateCode = ''}, }, session, -}: ValidateLoginPageProps) { +}: ValidateLoginPageProps) { useEffect(() => { if (session?.authToken) { // If already signed in, do not show the validate code if not on web, @@ -28,6 +28,6 @@ function ValidateLoginPage({ ValidateLoginPage.displayName = 'ValidateLoginPage'; -export default withOnyx>({ +export default withOnyx, ValidateLoginPageOnyxNativeProps>({ session: {key: ONYXKEYS.SESSION}, })(ValidateLoginPage); diff --git a/src/pages/ValidateLoginPage/index.website.tsx b/src/pages/ValidateLoginPage/index.website.tsx index 699da83bae2d..3fe994e20644 100644 --- a/src/pages/ValidateLoginPage/index.website.tsx +++ b/src/pages/ValidateLoginPage/index.website.tsx @@ -10,7 +10,7 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {ValidateLoginPageOnyxProps, ValidateLoginPageProps} from './types'; -function ValidateLoginPage({account, credentials, route, session}: ValidateLoginPageProps) { +function ValidateLoginPage({account, credentials, route, session}: ValidateLoginPageProps) { const login = credentials?.login; const autoAuthState = session?.autoAuthState ?? CONST.AUTO_AUTH_STATE.NOT_STARTED; const accountID = Number(route?.params.accountID) ?? -1; @@ -63,7 +63,7 @@ function ValidateLoginPage({account, credentials, route, session}: ValidateLogin ValidateLoginPage.displayName = 'ValidateLoginPage'; -export default withOnyx({ +export default withOnyx, ValidateLoginPageOnyxProps>({ account: {key: ONYXKEYS.ACCOUNT}, credentials: {key: ONYXKEYS.CREDENTIALS}, session: {key: ONYXKEYS.SESSION}, diff --git a/src/pages/ValidateLoginPage/types.ts b/src/pages/ValidateLoginPage/types.ts index 3173c73796b6..d9d2873891cd 100644 --- a/src/pages/ValidateLoginPage/types.ts +++ b/src/pages/ValidateLoginPage/types.ts @@ -1,20 +1,22 @@ import type {StackScreenProps} from '@react-navigation/stack'; import type {OnyxEntry} from 'react-native-onyx'; -import type {AuthScreensParamList} from '@libs/Navigation/types'; +import type {PublicScreensParamList} from '@libs/Navigation/types'; import type SCREENS from '@src/SCREENS'; import type {Account, Credentials, Session} from '@src/types/onyx'; -type ValidateLoginPageOnyxProps = { +type ValidateLoginPageOnyxNativeProps = { + /** Session of currently logged in user */ + session: OnyxEntry; +}; + +type ValidateLoginPageOnyxProps = ValidateLoginPageOnyxNativeProps & { /** The details about the account that the user is signing in with */ account: OnyxEntry; /** The credentials of the person logging in */ credentials: OnyxEntry; - - /** Session of currently logged in user */ - session: OnyxEntry; }; -type ValidateLoginPageProps = ValidateLoginPageOnyxProps & StackScreenProps; +type ValidateLoginPageProps = OnyxProps & StackScreenProps; -export type {ValidateLoginPageOnyxProps, ValidateLoginPageProps}; +export type {ValidateLoginPageOnyxNativeProps, ValidateLoginPageOnyxProps, ValidateLoginPageProps}; From 273406e9e907101a6acc16a640433d776cadb02a Mon Sep 17 00:00:00 2001 From: VickyStash Date: Fri, 12 Jan 2024 09:27:48 +0100 Subject: [PATCH 205/580] Reuse PaymentMethodType in other places --- src/components/ReportActionItem/ReportPreview.tsx | 4 +--- src/libs/ReportUtils.ts | 5 ++--- src/types/onyx/OriginalMessage.ts | 5 ++++- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/components/ReportActionItem/ReportPreview.tsx b/src/components/ReportActionItem/ReportPreview.tsx index cd2c87a3a585..1a347c039826 100644 --- a/src/components/ReportActionItem/ReportPreview.tsx +++ b/src/components/ReportActionItem/ReportPreview.tsx @@ -29,14 +29,12 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type {Policy, Report, ReportAction, Session} from '@src/types/onyx'; -import type DeepValueOf from '@src/types/utils/DeepValueOf'; +import type {PaymentMethodType} from '@src/types/onyx/OriginalMessage'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import ReportActionItemImages from './ReportActionItemImages'; type PaymentVerbTranslationPath = 'iou.payerSpent' | 'iou.payerOwes' | 'iou.payerPaid'; -type PaymentMethodType = DeepValueOf; - type ReportPreviewOnyxProps = { /** The policy tied to the money request report */ policy: OnyxEntry; diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 95c64f9b5e21..a3331e5f500e 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -16,12 +16,11 @@ import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type {Beta, Login, PersonalDetails, PersonalDetailsList, Policy, Report, ReportAction, ReportMetadata, Session, Transaction} from '@src/types/onyx'; import type {Errors, Icon, PendingAction} from '@src/types/onyx/OnyxCommon'; -import type {IOUMessage, OriginalMessageActionName, OriginalMessageCreated} from '@src/types/onyx/OriginalMessage'; +import type {IOUMessage, OriginalMessageActionName, OriginalMessageCreated, PaymentMethodType} from '@src/types/onyx/OriginalMessage'; import type {Status} from '@src/types/onyx/PersonalDetails'; import type {NotificationPreference} from '@src/types/onyx/Report'; import type {Message, ReportActionBase, ReportActions} from '@src/types/onyx/ReportAction'; import type {Receipt, WaypointCollection} from '@src/types/onyx/Transaction'; -import type DeepValueOf from '@src/types/utils/DeepValueOf'; import type {EmptyObject} from '@src/types/utils/EmptyObject'; import {isEmptyObject, isNotEmptyObject} from '@src/types/utils/EmptyObject'; import type IconAsset from '@src/types/utils/IconAsset'; @@ -2697,7 +2696,7 @@ function buildOptimisticIOUReportAction( comment: string, participants: Participant[], transactionID: string, - paymentType: DeepValueOf, + paymentType: PaymentMethodType, iouReportID = '', isSettlingUp = false, isSendMoneyFlow = false, diff --git a/src/types/onyx/OriginalMessage.ts b/src/types/onyx/OriginalMessage.ts index c4e30157bf6f..527915cd0017 100644 --- a/src/types/onyx/OriginalMessage.ts +++ b/src/types/onyx/OriginalMessage.ts @@ -2,6 +2,8 @@ import type {ValueOf} from 'type-fest'; import type CONST from '@src/CONST'; import type DeepValueOf from '@src/types/utils/DeepValueOf'; +type PaymentMethodType = DeepValueOf; + type ActionName = DeepValueOf; type OriginalMessageActionName = | 'ADDCOMMENT' @@ -42,7 +44,7 @@ type IOUMessage = { lastModified?: string; participantAccountIDs?: number[]; type: ValueOf; - paymentType?: DeepValueOf; + paymentType?: PaymentMethodType; /** Only exists when we are sending money */ IOUDetails?: IOUDetails; }; @@ -266,4 +268,5 @@ export type { OriginalMessageIOU, OriginalMessageCreated, OriginalMessageAddComment, + PaymentMethodType, }; From 256b9ae9d06765d2ef882fc8440c37f3b7c2a3c0 Mon Sep 17 00:00:00 2001 From: Kamil Owczarz Date: Fri, 12 Jan 2024 12:05:36 +0100 Subject: [PATCH 206/580] 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 3e278ea6cba6d2f69b63579c67c5240855310c24 Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Fri, 12 Jan 2024 14:06:05 +0100 Subject: [PATCH 207/580] lint code --- src/components/MagicCodeInput.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/components/MagicCodeInput.tsx b/src/components/MagicCodeInput.tsx index 727db31cb4a3..22068cf12590 100644 --- a/src/components/MagicCodeInput.tsx +++ b/src/components/MagicCodeInput.tsx @@ -275,7 +275,7 @@ function MagicCodeInput(props: MagicCodeInputProps, ref: ForwardedRef Date: Fri, 12 Jan 2024 17:09:12 +0100 Subject: [PATCH 208/580] remove propTypes --- src/components/ImageView/index.native.js | 15 ++++- .../MultiGestureCanvas/propTypes.js | 67 ------------------- 2 files changed, 12 insertions(+), 70 deletions(-) delete mode 100644 src/components/MultiGestureCanvas/propTypes.js diff --git a/src/components/ImageView/index.native.js b/src/components/ImageView/index.native.js index a94842b35219..82a5a1bdb978 100644 --- a/src/components/ImageView/index.native.js +++ b/src/components/ImageView/index.native.js @@ -1,7 +1,7 @@ import PropTypes from 'prop-types'; import React from 'react'; import Lightbox from '@components/Lightbox'; -import {zoomRangeDefaultProps, zoomRangePropTypes} from '@components/MultiGestureCanvas/propTypes'; +import {defaultZoomRange} from '@components/MultiGestureCanvas'; import {imageViewDefaultProps, imageViewPropTypes} from './propTypes'; /** @@ -9,7 +9,12 @@ import {imageViewDefaultProps, imageViewPropTypes} from './propTypes'; */ const propTypes = { ...imageViewPropTypes, - ...zoomRangePropTypes, + + /** Range of zoom that can be applied to the content by pinching or double tapping. */ + zoomRange: PropTypes.shape({ + min: PropTypes.number, + max: PropTypes.number, + }), /** Function for handle on press */ onPress: PropTypes.func, @@ -20,7 +25,11 @@ const propTypes = { const defaultProps = { ...imageViewDefaultProps, - ...zoomRangeDefaultProps, + + zoomRange: { + min: defaultZoomRange.min, + max: defaultZoomRange.max, + }, onPress: () => {}, style: {}, diff --git a/src/components/MultiGestureCanvas/propTypes.js b/src/components/MultiGestureCanvas/propTypes.js deleted file mode 100644 index 392ea27a6533..000000000000 --- a/src/components/MultiGestureCanvas/propTypes.js +++ /dev/null @@ -1,67 +0,0 @@ -import PropTypes from 'prop-types'; - -const defaultZoomRange = { - min: 1, - max: 20, -}; - -const zoomRangePropTypes = { - /** Range of zoom that can be applied to the content by pinching or double tapping. */ - zoomRange: PropTypes.shape({ - min: PropTypes.number, - max: PropTypes.number, - }), -}; - -const zoomRangeDefaultProps = { - zoomRange: { - min: defaultZoomRange.min, - max: defaultZoomRange.max, - }, -}; - -const multiGestureCanvasPropTypes = { - ...zoomRangePropTypes, - - /** The width and height of the canvas. - * This is needed in order to properly scale the content in the canvas - /** - * Wheter the canvas is currently active (in the screen) or not. - * Disables certain gestures and functionality - */ - isActive: PropTypes.bool, - - /** Handles scale changed event */ - onScaleChanged: PropTypes.func, - - /** - * The width and height of the canvas. - * This is needed in order to properly scale the content in the canvas - */ - canvasSize: PropTypes.shape({ - width: PropTypes.number.isRequired, - height: PropTypes.number.isRequired, - }).isRequired, - - /** - * The width and height of the content. - * This is needed in order to properly scale the content in the canvas - */ - contentSize: PropTypes.shape({ - width: PropTypes.number, - height: PropTypes.number, - }), - - /** Content that should be transformed inside the canvas (images, pdf, ...) */ - children: PropTypes.node.isRequired, -}; - -const multiGestureCanvasDefaultProps = { - isActive: true, - onScaleChanged: () => undefined, - contentSize: undefined, - contentScaling: undefined, - zoomRange: defaultZoomRange, -}; - -export {defaultZoomRange, zoomRangePropTypes, zoomRangeDefaultProps, multiGestureCanvasPropTypes, multiGestureCanvasDefaultProps}; From d872300bcb879c4e9b447e116b74d539cd6c3e36 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Fri, 12 Jan 2024 17:18:14 +0100 Subject: [PATCH 209/580] 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 210/580] 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 211/580] 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 c1251f936afd5e5086895fcdf0935040d29d36df Mon Sep 17 00:00:00 2001 From: VH Date: Mon, 15 Jan 2024 00:08:17 +0700 Subject: [PATCH 212/580] Pass reportID to getForReportAction util --- src/libs/ModifiedExpenseMessage.ts | 4 ++-- .../Notification/LocalNotification/BrowserNotifications.ts | 2 +- src/libs/OptionsListUtils.js | 2 +- src/pages/home/report/ContextMenu/ContextMenuActions.js | 4 ++-- src/pages/home/report/ReportActionItem.js | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/libs/ModifiedExpenseMessage.ts b/src/libs/ModifiedExpenseMessage.ts index ff5ad9327191..5514b7094043 100644 --- a/src/libs/ModifiedExpenseMessage.ts +++ b/src/libs/ModifiedExpenseMessage.ts @@ -95,12 +95,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 { +function getForReportAction(reportID: string, reportAction: ReportAction): string { if (reportAction.actionName !== CONST.REPORT.ACTIONS.TYPE.MODIFIEDEXPENSE) { return ''; } const reportActionOriginalMessage = reportAction.originalMessage as ExpenseOriginalMessage | undefined; - const policyID = ReportUtils.getReportPolicyID(reportAction.reportID) ?? ''; + const policyID = ReportUtils.getReportPolicyID(reportID) ?? ''; const policyTags = allPolicyTags?.[`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`] ?? {}; const policyTagListName = PolicyUtils.getTagListName(policyTags) || Localize.translateLocal('common.tag'); diff --git a/src/libs/Notification/LocalNotification/BrowserNotifications.ts b/src/libs/Notification/LocalNotification/BrowserNotifications.ts index e65bd3d0021f..b0304e055fd5 100644 --- a/src/libs/Notification/LocalNotification/BrowserNotifications.ts +++ b/src/libs/Notification/LocalNotification/BrowserNotifications.ts @@ -109,7 +109,7 @@ export default { pushModifiedExpenseNotification(report: Report, reportAction: ReportAction, onClick: LocalNotificationClickHandler, usesIcon = false) { const title = reportAction.person?.map((f) => f.text).join(', ') ?? ''; - const body = ModifiedExpenseMessage.getForReportAction(reportAction); + const body = ModifiedExpenseMessage.getForReportAction(report.reportID, reportAction); const icon = usesIcon ? EXPENSIFY_ICON_URL : ''; const data = { reportID: report.reportID, diff --git a/src/libs/OptionsListUtils.js b/src/libs/OptionsListUtils.js index 988398009dd8..855aebfc1223 100644 --- a/src/libs/OptionsListUtils.js +++ b/src/libs/OptionsListUtils.js @@ -426,7 +426,7 @@ function getLastMessageTextForReport(report) { } 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 = ModifiedExpenseMessage.getForReportAction(lastReportAction); + const properSchemaForModifiedExpenseMessage = ModifiedExpenseMessage.getForReportAction(report.reportID, lastReportAction); lastMessageTextFromReport = ReportUtils.formatReportLastMessageText(properSchemaForModifiedExpenseMessage, true); } else if ( lastActionName === CONST.REPORT.ACTIONS.TYPE.TASKCOMPLETED || diff --git a/src/pages/home/report/ContextMenu/ContextMenuActions.js b/src/pages/home/report/ContextMenu/ContextMenuActions.js index f22eda58ce7f..aa31f5101e3e 100644 --- a/src/pages/home/report/ContextMenu/ContextMenuActions.js +++ b/src/pages/home/report/ContextMenu/ContextMenuActions.js @@ -269,7 +269,7 @@ export default [ // If return value is true, we switch the `text` and `icon` on // `ContextMenuItem` with `successText` and `successIcon` which will fall back to // the `text` and `icon` - onPress: (closePopover, {reportAction, selection}) => { + onPress: (closePopover, {reportAction, selection, reportID}) => { const isTaskAction = ReportActionsUtils.isTaskAction(reportAction); const isReportPreviewAction = ReportActionsUtils.isReportPreviewAction(reportAction); const message = _.last(lodashGet(reportAction, 'message', [{}])); @@ -283,7 +283,7 @@ export default [ const displayMessage = ReportUtils.getReportPreviewMessage(iouReport, reportAction); Clipboard.setString(displayMessage); } else if (ReportActionsUtils.isModifiedExpenseAction(reportAction)) { - const modifyExpenseMessage = ModifiedExpenseMessage.getForReportAction(reportAction); + const modifyExpenseMessage = ModifiedExpenseMessage.getForReportAction(reportID, reportAction); Clipboard.setString(modifyExpenseMessage); } else if (ReportActionsUtils.isMoneyRequestAction(reportAction)) { const displayMessage = ReportUtils.getIOUReportActionDisplayMessage(reportAction); diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.js index e490c4601d10..e92ee0aa916e 100644 --- a/src/pages/home/report/ReportActionItem.js +++ b/src/pages/home/report/ReportActionItem.js @@ -445,7 +445,7 @@ function ReportActionItem(props) { children = ; } else if (props.action.actionName === CONST.REPORT.ACTIONS.TYPE.MODIFIEDEXPENSE) { - children = ; + children = ; } else if (props.action.actionName === CONST.REPORT.ACTIONS.TYPE.MARKEDREIMBURSED) { children = ; } else { From 603551545a9e44938e12dc73ec497ef0b79642c6 Mon Sep 17 00:00:00 2001 From: VH Date: Mon, 15 Jan 2024 00:08:33 +0700 Subject: [PATCH 213/580] Fix unit tests --- .../ModifiedExpenseMessage.perf-test.ts | 3 +- tests/unit/ModifiedExpenseMessageTest.ts | 30 ++++++++++--------- 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/tests/perf-test/ModifiedExpenseMessage.perf-test.ts b/tests/perf-test/ModifiedExpenseMessage.perf-test.ts index 5aa842155cb5..718a9e025e1e 100644 --- a/tests/perf-test/ModifiedExpenseMessage.perf-test.ts +++ b/tests/perf-test/ModifiedExpenseMessage.perf-test.ts @@ -43,6 +43,7 @@ const mockedReportsMap = getMockedReports(5000) as Record<`${typeof ONYXKEYS.COL const mockedPoliciesMap = getMockedPolicies(5000) as Record<`${typeof ONYXKEYS.COLLECTION.POLICY}`, Policy>; test('[ModifiedExpenseMessage] getForReportAction on 5k reports and policies', async () => { + const report = createRandomReport(1); const reportAction = { ...createRandomReportAction(1), actionName: CONST.REPORT.ACTIONS.TYPE.MODIFIEDEXPENSE, @@ -60,5 +61,5 @@ test('[ModifiedExpenseMessage] getForReportAction on 5k reports and policies', a }); await waitForBatchedUpdates(); - await measureFunction(() => ModifiedExpenseMessage.getForReportAction(reportAction), {runs}); + await measureFunction(() => ModifiedExpenseMessage.getForReportAction(report.reportID, reportAction), {runs}); }); diff --git a/tests/unit/ModifiedExpenseMessageTest.ts b/tests/unit/ModifiedExpenseMessageTest.ts index aedc02cc628b..cf4c4136b8fa 100644 --- a/tests/unit/ModifiedExpenseMessageTest.ts +++ b/tests/unit/ModifiedExpenseMessageTest.ts @@ -1,9 +1,11 @@ import ModifiedExpenseMessage from '@libs/ModifiedExpenseMessage'; import CONST from '@src/CONST'; import createRandomReportAction from '../utils/collections/reportActions'; +import createRandomReport from '../utils/collections/reports'; describe('ModifiedExpenseMessage', () => { describe('getForAction', () => { + const report = createRandomReport(1); describe('when the amount is changed', () => { const reportAction = { ...createRandomReportAction(1), @@ -19,7 +21,7 @@ describe('ModifiedExpenseMessage', () => { it('returns the correct text message', () => { const expectedResult = `changed the amount to $18.00 (previously $12.55).`; - const result = ModifiedExpenseMessage.getForReportAction(reportAction); + const result = ModifiedExpenseMessage.getForReportAction(report.reportID, reportAction); expect(result).toEqual(expectedResult); }); @@ -42,7 +44,7 @@ describe('ModifiedExpenseMessage', () => { it('returns the correct text message', () => { const expectedResult = 'changed the amount to $18.00 (previously $12.55).\nremoved the description (previously "this is for the shuttle").'; - const result = ModifiedExpenseMessage.getForReportAction(reportAction); + const result = ModifiedExpenseMessage.getForReportAction(report.reportID, reportAction); expect(result).toEqual(expectedResult); }); @@ -67,7 +69,7 @@ describe('ModifiedExpenseMessage', () => { it('returns the correct text message', () => { const expectedResult = 'changed the amount to $18.00 (previously $12.55).\nset the category to "Benefits".\nremoved the description (previously "this is for the shuttle").'; - const result = ModifiedExpenseMessage.getForReportAction(reportAction); + const result = ModifiedExpenseMessage.getForReportAction(report.reportID, reportAction); expect(result).toEqual(expectedResult); }); @@ -90,7 +92,7 @@ describe('ModifiedExpenseMessage', () => { it('returns the correct text message', () => { const expectedResult = 'changed the amount to $18.00 (previously $12.55) and the merchant to "Taco Bell" (previously "Big Belly").'; - const result = ModifiedExpenseMessage.getForReportAction(reportAction); + const result = ModifiedExpenseMessage.getForReportAction(report.reportID, reportAction); expect(result).toEqual(expectedResult); }); @@ -118,7 +120,7 @@ describe('ModifiedExpenseMessage', () => { const expectedResult = 'changed the amount to $18.00 (previously $12.55) and the merchant to "Taco Bell" (previously "Big Belly").\nset the category to "Benefits".\nremoved the description (previously "this is for the shuttle").'; - const result = ModifiedExpenseMessage.getForReportAction(reportAction); + const result = ModifiedExpenseMessage.getForReportAction(report.reportID, reportAction); expect(result).toEqual(expectedResult); }); @@ -144,7 +146,7 @@ describe('ModifiedExpenseMessage', () => { const expectedResult = 'changed the amount to $18.00 (previously $12.55), the description to "I bought it on the way" (previously "from the business trip"), and the merchant to "Taco Bell" (previously "Big Belly").'; - const result = ModifiedExpenseMessage.getForReportAction(reportAction); + const result = ModifiedExpenseMessage.getForReportAction(report.reportID, reportAction); expect(result).toEqual(expectedResult); }); @@ -163,7 +165,7 @@ describe('ModifiedExpenseMessage', () => { it('returns the correct text message', () => { const expectedResult = `removed the merchant (previously "Big Belly").`; - const result = ModifiedExpenseMessage.getForReportAction(reportAction); + const result = ModifiedExpenseMessage.getForReportAction(report.reportID, reportAction); expect(result).toEqual(expectedResult); }); @@ -184,7 +186,7 @@ describe('ModifiedExpenseMessage', () => { it('returns the correct text message', () => { const expectedResult = `removed the description (previously "minishore") and the merchant (previously "Big Belly").`; - const result = ModifiedExpenseMessage.getForReportAction(reportAction); + const result = ModifiedExpenseMessage.getForReportAction(report.reportID, reportAction); expect(result).toEqual(expectedResult); }); @@ -207,7 +209,7 @@ describe('ModifiedExpenseMessage', () => { it('returns the correct text message', () => { const expectedResult = `removed the description (previously "minishore"), the merchant (previously "Big Belly"), and the category (previously "Benefits").`; - const result = ModifiedExpenseMessage.getForReportAction(reportAction); + const result = ModifiedExpenseMessage.getForReportAction(report.reportID, reportAction); expect(result).toEqual(expectedResult); }); @@ -226,7 +228,7 @@ describe('ModifiedExpenseMessage', () => { it('returns the correct text message', () => { const expectedResult = `set the merchant to "Big Belly".`; - const result = ModifiedExpenseMessage.getForReportAction(reportAction); + const result = ModifiedExpenseMessage.getForReportAction(report.reportID, reportAction); expect(result).toEqual(expectedResult); }); @@ -247,7 +249,7 @@ describe('ModifiedExpenseMessage', () => { it('returns the correct text message', () => { const expectedResult = `set the description to "minishore" and the merchant to "Big Belly".`; - const result = ModifiedExpenseMessage.getForReportAction(reportAction); + const result = ModifiedExpenseMessage.getForReportAction(report.reportID, reportAction); expect(result).toEqual(expectedResult); }); @@ -270,7 +272,7 @@ describe('ModifiedExpenseMessage', () => { it('returns the correct text message', () => { const expectedResult = `set the description to "minishore", the merchant to "Big Belly", and the category to "Benefits".`; - const result = ModifiedExpenseMessage.getForReportAction(reportAction); + const result = ModifiedExpenseMessage.getForReportAction(report.reportID, reportAction); expect(result).toEqual(expectedResult); }); @@ -289,7 +291,7 @@ describe('ModifiedExpenseMessage', () => { it('returns the correct text message', () => { const expectedResult = 'changed the date to 2023-12-27 (previously 2023-12-26).'; - const result = ModifiedExpenseMessage.getForReportAction(reportAction); + const result = ModifiedExpenseMessage.getForReportAction(report.reportID, reportAction); expect(result).toEqual(expectedResult); }); @@ -307,7 +309,7 @@ describe('ModifiedExpenseMessage', () => { it('returns the correct text message', () => { const expectedResult = 'changed the request'; - const result = ModifiedExpenseMessage.getForReportAction(reportAction); + const result = ModifiedExpenseMessage.getForReportAction(report.reportID, reportAction); expect(result).toEqual(expectedResult); }); From 37dc816e62294fadd10223f04b0124a61c0b87eb Mon Sep 17 00:00:00 2001 From: brunovjk Date: Sun, 14 Jan 2024 19:25:14 -0300 Subject: [PATCH 214/580] 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 215/580] 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 216/580] 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 217/580] 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 218/580] 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 26630b221955e5d48e3daf0fea50f9911b82e2c8 Mon Sep 17 00:00:00 2001 From: Someshwar Tripathi Date: Mon, 15 Jan 2024 05:40:42 +0530 Subject: [PATCH 219/580] Update useEffect to not scroll if multiple options cannot be selected --- src/components/SelectionList/BaseSelectionList.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/components/SelectionList/BaseSelectionList.js b/src/components/SelectionList/BaseSelectionList.js index 960618808fd9..48ee89a80192 100644 --- a/src/components/SelectionList/BaseSelectionList.js +++ b/src/components/SelectionList/BaseSelectionList.js @@ -372,6 +372,11 @@ function BaseSelectionList({ return; } + // scroll is unnecessary if multiple options cannot be selected + if(!canSelectMultiple) { + return; + } + // set the focus on the first item when the sections list is changed if (sections.length > 0) { updateAndScrollToFocusedIndex(0); From b0742dab36182273f1a28603f23881e53bd0bf80 Mon Sep 17 00:00:00 2001 From: Someshwar Tripathi Date: Mon, 15 Jan 2024 05:51:56 +0530 Subject: [PATCH 220/580] Define shouldScrollToTopOnSelect prop --- src/components/SelectionList/selectionListPropTypes.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/components/SelectionList/selectionListPropTypes.js b/src/components/SelectionList/selectionListPropTypes.js index f5178112a4c3..1790cf3aad6f 100644 --- a/src/components/SelectionList/selectionListPropTypes.js +++ b/src/components/SelectionList/selectionListPropTypes.js @@ -198,6 +198,9 @@ const propTypes = { /** Right hand side component to display in the list item. Function has list item passed as the param */ rightHandSideComponent: PropTypes.oneOfType([PropTypes.func, PropTypes.node]), + + /** Whether to scroll to top when an option is selected */ + shouldScrollToTopOnSelect: PropTypes.bool, }; export {propTypes, baseListItemPropTypes, radioListItemPropTypes, userListItemPropTypes}; From 102862bbcbfa26fb9e2af2c4162955bc54f0e071 Mon Sep 17 00:00:00 2001 From: Someshwar Tripathi Date: Mon, 15 Jan 2024 05:54:17 +0530 Subject: [PATCH 221/580] Add prop shouldScrollToTopOnSelect to BaseSelectionList --- src/components/SelectionList/BaseSelectionList.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/SelectionList/BaseSelectionList.js b/src/components/SelectionList/BaseSelectionList.js index 48ee89a80192..d073110a5e26 100644 --- a/src/components/SelectionList/BaseSelectionList.js +++ b/src/components/SelectionList/BaseSelectionList.js @@ -66,6 +66,7 @@ function BaseSelectionList({ shouldShowTooltips = true, shouldUseDynamicMaxToRenderPerBatch = false, rightHandSideComponent, + shouldScrollToTopOnSelect = true, }) { const theme = useTheme(); const styles = useThemeStyles(); @@ -378,7 +379,7 @@ function BaseSelectionList({ } // set the focus on the first item when the sections list is changed - if (sections.length > 0) { + if (sections.length > 0 && shouldScrollToTopOnSelect) { updateAndScrollToFocusedIndex(0); } // eslint-disable-next-line react-hooks/exhaustive-deps From 2543e1c57a2600e7b5a802e73c4951ad5df97bf9 Mon Sep 17 00:00:00 2001 From: Someshwar Tripathi Date: Mon, 15 Jan 2024 07:01:45 +0530 Subject: [PATCH 222/580] Prettier --- 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 d073110a5e26..dd8cba3602dc 100644 --- a/src/components/SelectionList/BaseSelectionList.js +++ b/src/components/SelectionList/BaseSelectionList.js @@ -374,7 +374,7 @@ function BaseSelectionList({ } // scroll is unnecessary if multiple options cannot be selected - if(!canSelectMultiple) { + if (!canSelectMultiple) { return; } From 4f170a18ff3ef16a4b7b47247f8d2154a1468f36 Mon Sep 17 00:00:00 2001 From: Tienifr <113963320+tienifr@users.noreply.github.com> Date: Mon, 15 Jan 2024 14:13:35 +0700 Subject: [PATCH 223/580] Noop function Co-authored-by: Aimane Chnaif <96077027+aimane-chnaif@users.noreply.github.com> --- src/libs/DomUtils/index.native.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/DomUtils/index.native.ts b/src/libs/DomUtils/index.native.ts index 42b19c1cad20..eac988e55f1b 100644 --- a/src/libs/DomUtils/index.native.ts +++ b/src/libs/DomUtils/index.native.ts @@ -2,7 +2,7 @@ import type GetActiveElement from './types'; const getActiveElement: GetActiveElement = () => null; -const addCSS = () => null; +const addCSS = () => {}; const getAutofilledInputStyle = () => null; From f470ff9d2461632d5ba3800fe1c854f6b2f85768 Mon Sep 17 00:00:00 2001 From: Kamil Owczarz Date: Mon, 15 Jan 2024 11:35:39 +0100 Subject: [PATCH 224/580] 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 b21c8a719bf8e2aeaec7864f114de244c7a871e8 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Mon, 15 Jan 2024 12:04:04 +0100 Subject: [PATCH 225/580] Update src/components/MultiGestureCanvas/useTapGestures.ts 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/components/MultiGestureCanvas/useTapGestures.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/MultiGestureCanvas/useTapGestures.ts b/src/components/MultiGestureCanvas/useTapGestures.ts index 217981a1238d..18439e07e626 100644 --- a/src/components/MultiGestureCanvas/useTapGestures.ts +++ b/src/components/MultiGestureCanvas/useTapGestures.ts @@ -123,7 +123,7 @@ const useTapGestures = ({ .maxDistance(20) .onEnd((evt) => { // If the content is already zoomed, we want to reset the zoom, - // otherwwise we want to zoom in + // otherwise we want to zoom in if (zoomScale.value > 1) { reset(true); } else { From e944dc212c5eb2080ad0e3d133cffd4d84df6641 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Mon, 15 Jan 2024 13:01:32 +0100 Subject: [PATCH 226/580] address GH comments --- .../Pager/AttachmentCarouselPagerContext.ts | 2 +- .../AttachmentCarousel/Pager/index.tsx | 36 +++------- .../attachmentCarouselPropTypes.js | 4 -- .../BaseAttachmentViewPdf.js | 2 +- src/components/ImageView/index.native.js | 12 +--- src/components/Lightbox.tsx | 65 ++++++++----------- .../MultiGestureCanvas/constants.ts | 21 +++++- src/components/MultiGestureCanvas/index.tsx | 26 ++++---- src/components/MultiGestureCanvas/types.ts | 7 +- .../MultiGestureCanvas/usePanGesture.ts | 5 +- .../MultiGestureCanvas/usePinchGesture.ts | 18 +++-- .../MultiGestureCanvas/useTapGestures.ts | 11 ++-- src/components/MultiGestureCanvas/utils.ts | 29 +-------- src/styles/utils/index.ts | 7 +- 14 files changed, 102 insertions(+), 143 deletions(-) diff --git a/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts b/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts index e595b8c5c4d1..aff8b4a0cae9 100644 --- a/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts +++ b/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts @@ -3,7 +3,7 @@ import type PagerView from 'react-native-pager-view'; import type {SharedValue} from 'react-native-reanimated'; type AttachmentCarouselPagerContextType = { - onTap: () => void; + onTap?: () => void; onScaleChanged: (scale: number) => void; pagerRef: React.Ref; shouldPagerScroll: SharedValue; diff --git a/src/components/Attachments/AttachmentCarousel/Pager/index.tsx b/src/components/Attachments/AttachmentCarousel/Pager/index.tsx index f61b3c160d67..0e41c4be56b3 100644 --- a/src/components/Attachments/AttachmentCarousel/Pager/index.tsx +++ b/src/components/Attachments/AttachmentCarousel/Pager/index.tsx @@ -1,24 +1,19 @@ +import type {Ref} from 'react'; import React, {useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react'; import {View} from 'react-native'; import type {NativeViewGestureHandlerProps} from 'react-native-gesture-handler'; import {createNativeWrapper} from 'react-native-gesture-handler'; import type {PagerViewProps} from 'react-native-pager-view'; import PagerView from 'react-native-pager-view'; -import type {AnimatedProps} from 'react-native-reanimated'; import Animated, {runOnJS, useAnimatedProps, useAnimatedReaction, useSharedValue} from 'react-native-reanimated'; import useThemeStyles from '@hooks/useThemeStyles'; import AttachmentCarouselPagerContext from './AttachmentCarouselPagerContext'; import usePageScrollHandler from './usePageScrollHandler'; -type PagerViewPropsObject = { - [K in keyof PagerViewProps]: PagerViewProps[K]; -}; - -type AnimatedNativeWrapperComponent

, C> = React.ForwardRefExoticComponent< - React.PropsWithoutRef & NativeViewGestureHandlerProps> & React.RefAttributes +const WrappedPagerView = createNativeWrapper(PagerView) as React.ForwardRefExoticComponent< + PagerViewProps & NativeViewGestureHandlerProps & React.RefAttributes> >; - -const AnimatedPagerView = Animated.createAnimatedComponent(createNativeWrapper(PagerView)) as AnimatedNativeWrapperComponent; +const AnimatedPagerView = Animated.createAnimatedComponent(WrappedPagerView); type AttachmentCarouselPagerHandle = { setPage: (selectedPage: number) => void; @@ -30,17 +25,15 @@ type PagerItem = { source: string; }; -type AttachmentCarouselPagerProps = React.PropsWithChildren<{ +type AttachmentCarouselPagerProps = { items: PagerItem[]; renderItem: (props: {item: PagerItem; index: number; isActive: boolean}) => React.ReactNode; initialIndex: number; onPageSelected: () => void; - onTap: () => void; onScaleChanged: (scale: number) => void; - forwardedRef: React.Ref; -}>; +}; -function AttachmentCarouselPager({items, renderItem, initialIndex, onPageSelected, onTap, onScaleChanged, forwardedRef}: AttachmentCarouselPagerProps) { +function AttachmentCarouselPager({items, renderItem, initialIndex, onPageSelected, onScaleChanged}: AttachmentCarouselPagerProps, ref: Ref) { const styles = useThemeStyles(); const shouldPagerScroll = useSharedValue(true); const pagerRef = useRef(null); @@ -81,7 +74,7 @@ function AttachmentCarouselPager({items, renderItem, initialIndex, onPageSelecte ); useImperativeHandle( - forwardedRef, + ref, () => ({ setPage: (selectedPage) => { pagerRef.current?.setPage(selectedPage); @@ -96,13 +89,12 @@ function AttachmentCarouselPager({items, renderItem, initialIndex, onPageSelecte const contextValue = useMemo( () => ({ - onTap, onScaleChanged, pagerRef, shouldPagerScroll, isSwipingInPager, }), - [isSwipingInPager, shouldPagerScroll, onScaleChanged, onTap], + [isSwipingInPager, shouldPagerScroll, onScaleChanged], ); return ( @@ -131,12 +123,4 @@ function AttachmentCarouselPager({items, renderItem, initialIndex, onPageSelecte } AttachmentCarouselPager.displayName = 'AttachmentCarouselPager'; -const AttachmentCarouselPagerWithRef = React.forwardRef>((props, ref) => ( - -)); - -export default AttachmentCarouselPagerWithRef; +export default React.forwardRef(AttachmentCarouselPager); diff --git a/src/components/Attachments/AttachmentCarousel/attachmentCarouselPropTypes.js b/src/components/Attachments/AttachmentCarousel/attachmentCarouselPropTypes.js index 72a554de68be..5aa665683162 100644 --- a/src/components/Attachments/AttachmentCarousel/attachmentCarouselPropTypes.js +++ b/src/components/Attachments/AttachmentCarousel/attachmentCarouselPropTypes.js @@ -10,9 +10,6 @@ const propTypes = { /** Callback to update the parent modal's state with a source and name from the attachments array */ onNavigate: PropTypes.func, - /** Callback to close carousel when user swipes down (on native) */ - onClose: PropTypes.func, - /** Function to change the download button Visibility */ setDownloadButtonVisibility: PropTypes.func, @@ -39,7 +36,6 @@ const defaultProps = { parentReportActions: {}, transaction: {}, onNavigate: () => {}, - onClose: () => {}, setDownloadButtonVisibility: () => {}, }; diff --git a/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.js b/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.js index de14f848c37e..022a89753476 100644 --- a/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.js +++ b/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.js @@ -21,7 +21,7 @@ function BaseAttachmentViewPdf({ if (!attachmentCarouselPagerContext) { return; } - attachmentCarouselPagerContext.onPinchGestureChange(false); + attachmentCarouselPagerContext.onScaleChanged(1); // eslint-disable-next-line react-hooks/exhaustive-deps -- we just want to call this function when component is mounted }, []); diff --git a/src/components/ImageView/index.native.js b/src/components/ImageView/index.native.js index 82a5a1bdb978..ba10162ec1e2 100644 --- a/src/components/ImageView/index.native.js +++ b/src/components/ImageView/index.native.js @@ -16,9 +16,6 @@ const propTypes = { max: PropTypes.number, }), - /** Function for handle on press */ - onPress: PropTypes.func, - /** Additional styles to add to the component */ style: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.object), PropTypes.object]), }; @@ -26,16 +23,12 @@ const propTypes = { const defaultProps = { ...imageViewDefaultProps, - zoomRange: { - min: defaultZoomRange.min, - max: defaultZoomRange.max, - }, + zoomRange: defaultZoomRange, - onPress: () => {}, style: {}, }; -function ImageView({isAuthTokenRequired, url, onScaleChanged, onPress, style, zoomRange, onError, isUsedInCarousel, isSingleCarouselItem, carouselItemIndex, carouselActiveItemIndex}) { +function ImageView({isAuthTokenRequired, url, onScaleChanged, style, zoomRange, onError, isUsedInCarousel, isSingleCarouselItem, carouselItemIndex, carouselActiveItemIndex}) { const hasSiblingCarouselItems = isUsedInCarousel && !isSingleCarouselItem; return ( @@ -44,7 +37,6 @@ function ImageView({isAuthTokenRequired, url, onScaleChanged, onPress, style, zo zoomRange={zoomRange} isAuthTokenRequired={isAuthTokenRequired} onScaleChanged={onScaleChanged} - onPress={onPress} onError={onError} index={carouselItemIndex} activeIndex={carouselActiveItemIndex} diff --git a/src/components/Lightbox.tsx b/src/components/Lightbox.tsx index db662e6e8776..f7eccdf3724f 100644 --- a/src/components/Lightbox.tsx +++ b/src/components/Lightbox.tsx @@ -1,5 +1,5 @@ import React, {useCallback, useEffect, useMemo, useState} from 'react'; -import type {ImageSourcePropType, LayoutChangeEvent, NativeSyntheticEvent, StyleProp, ViewStyle} from 'react-native'; +import type {LayoutChangeEvent, NativeSyntheticEvent, StyleProp, ViewStyle} from 'react-native'; import {ActivityIndicator, PixelRatio, StyleSheet, View} from 'react-native'; import useStyleUtils from '@hooks/useStyleUtils'; import Image from './Image'; @@ -9,20 +9,17 @@ import type {ContentSize, OnScaleChangedCallback, ZoomRange} from './MultiGestur // Increase/decrease this number to change the number of concurrent lightboxes // The more concurrent lighboxes, the worse performance gets (especially on low-end devices) -// -1 means unlimited -// We need to define a type for this constant and therefore ignore this ESLint error, although the type is inferable, -// because otherwise TS will throw an error later in the code since "-1" and this constant have no overlap. -// We can safely ignore this error, because we might change the value in the future -// eslint-disable-next-line @typescript-eslint/no-inferrable-types -const NUMBER_OF_CONCURRENT_LIGHTBOXES: number = 3; +type LightboxConcurrencyLimit = number | 'UNLIMITED'; +const NUMBER_OF_CONCURRENT_LIGHTBOXES: LightboxConcurrencyLimit = 3; const DEFAULT_IMAGE_SIZE = 200; +const DEFAULT_IMAGE_DIMENSION: ContentSize = {width: DEFAULT_IMAGE_SIZE, height: DEFAULT_IMAGE_SIZE}; type LightboxImageDimensions = { lightboxSize?: ContentSize; fallbackSize?: ContentSize; }; -const cachedDimensions = new Map(); +const cachedDimensions = new Map(); type ImageOnLoadEvent = NativeSyntheticEvent; @@ -31,10 +28,10 @@ type LightboxProps = { isAuthTokenRequired: boolean; /** URI to full-sized attachment, SVG function, or numeric static image on native platforms */ - source: ImageSourcePropType; + uri: string; /** Triggers whenever the zoom scale changes */ - onScaleChanged: OnScaleChangedCallback; + onScaleChanged?: OnScaleChangedCallback; /** Handles errors while displaying the image */ onError: () => void; @@ -60,7 +57,7 @@ type LightboxProps = { */ function Lightbox({ isAuthTokenRequired = false, - source, + uri, onScaleChanged, onError, style, @@ -71,16 +68,16 @@ function Lightbox({ }: LightboxProps) { const StyleUtils = useStyleUtils(); - const [containerSize, setContainerSize] = useState({width: 0, height: 0}); + const [containerSize, setContainerSize] = useState({width: 0, height: 0}); const isContainerLoaded = containerSize.width !== 0 && containerSize.height !== 0; - const [imageDimensions, setInternalImageDimensions] = useState(() => cachedDimensions.get(source)); + const [imageDimensions, setInternalImageDimensions] = useState(() => cachedDimensions.get(uri)); const setImageDimensions = useCallback( (newDimensions: LightboxImageDimensions) => { setInternalImageDimensions(newDimensions); - cachedDimensions.set(source, newDimensions); + cachedDimensions.set(uri, newDimensions); }, - [source], + [uri], ); const isItemActive = index === activeIndex; const [isActive, setActive] = useState(isItemActive); @@ -90,9 +87,9 @@ function Lightbox({ const [isFallbackVisible, setFallbackVisible] = useState(isInactiveCarouselItem); const [isFallbackLoaded, setFallbackLoaded] = useState(false); - const isLightboxLoaded = imageDimensions?.lightboxSize != null; + const isLightboxLoaded = imageDimensions?.lightboxSize !== undefined; const isLightboxInRange = useMemo(() => { - if (NUMBER_OF_CONCURRENT_LIGHTBOXES === -1) { + if (NUMBER_OF_CONCURRENT_LIGHTBOXES === 'UNLIMITED') { return true; } @@ -161,17 +158,11 @@ function Lightbox({ }, [hasSiblingCarouselItems, isActive, isImageLoaded, isFallbackVisible, isLightboxLoaded, isLightboxVisible]); const fallbackSize = useMemo(() => { - if (!hasSiblingCarouselItems || (imageDimensions?.lightboxSize == null && imageDimensions?.fallbackSize == null) || containerSize.width === 0 || containerSize.height === 0) { - return { - width: DEFAULT_IMAGE_SIZE, - height: DEFAULT_IMAGE_SIZE, - }; - } + const imageSize = imageDimensions?.lightboxSize ?? imageDimensions?.fallbackSize; - // If the lightbox size is undefined, th fallback size cannot be undefined, - // because we already checked for that before and would have returned early. - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const imageSize = imageDimensions.lightboxSize ?? imageDimensions.fallbackSize!; + if (!hasSiblingCarouselItems || !imageSize || !isContainerLoaded) { + return DEFAULT_IMAGE_DIMENSION; + } const {minScale} = getCanvasFitScale({canvasSize: containerSize, contentSize: imageSize}); @@ -179,7 +170,7 @@ function Lightbox({ width: PixelRatio.roundToNearestPixel(imageSize.width * minScale), height: PixelRatio.roundToNearestPixel(imageSize.height * minScale), }; - }, [containerSize, hasSiblingCarouselItems, imageDimensions]); + }, [containerSize, hasSiblingCarouselItems, imageDimensions?.fallbackSize, imageDimensions?.lightboxSize, isContainerLoaded]); return ( {isLightboxVisible && ( - + setImageLoaded(true)} onLoad={(e: ImageOnLoadEvent) => { - const width = (e.nativeEvent?.width || 0) * PixelRatio.get(); - const height = (e.nativeEvent?.height || 0) * PixelRatio.get(); + const width = e.nativeEvent.width * PixelRatio.get(); + const height = e.nativeEvent.height * PixelRatio.get(); setImageDimensions({...imageDimensions, lightboxSize: {width, height}}); }} /> @@ -217,16 +208,16 @@ function Lightbox({ {isFallbackVisible && ( setFallbackLoaded(true)} onLoad={(e: ImageOnLoadEvent) => { - const width = (e.nativeEvent?.width || 0) * PixelRatio.get(); - const height = (e.nativeEvent?.height || 0) * PixelRatio.get(); + const width = e.nativeEvent.width * PixelRatio.get(); + const height = e.nativeEvent.height * PixelRatio.get(); - if (imageDimensions?.lightboxSize != null) { + if (isLightboxLoaded) { return; } diff --git a/src/components/MultiGestureCanvas/constants.ts b/src/components/MultiGestureCanvas/constants.ts index 0103d07c55c2..1d3e143c970c 100644 --- a/src/components/MultiGestureCanvas/constants.ts +++ b/src/components/MultiGestureCanvas/constants.ts @@ -1,11 +1,26 @@ -const defaultZoomRange = { +import type {WithSpringConfig} from 'react-native-reanimated'; +import type {ZoomRange} from './types'; + +// The spring config is used to determine the physics of the spring animation +// Details and a playground for testing different configs can be found at +// https://docs.swmansion.com/react-native-reanimated/docs/animations/withSpring +const SPRING_CONFIG: WithSpringConfig = { + mass: 1, + stiffness: 1000, + damping: 500, +}; + +// The default zoom range within the user can pinch to zoom the content inside the canvas +const defaultZoomRange: Required = { min: 1, max: 20, }; -const zoomScaleBounceFactors = { +// The zoom scale bounce factors are used to determine the amount of bounce +// that is allowed when the user zooms more than the min or max zoom levels +const zoomScaleBounceFactors: Required = { min: 0.7, max: 1.5, }; -export {defaultZoomRange, zoomScaleBounceFactors}; +export {SPRING_CONFIG, defaultZoomRange, zoomScaleBounceFactors}; diff --git a/src/components/MultiGestureCanvas/index.tsx b/src/components/MultiGestureCanvas/index.tsx index 60a138b44606..7388d1942dad 100644 --- a/src/components/MultiGestureCanvas/index.tsx +++ b/src/components/MultiGestureCanvas/index.tsx @@ -7,7 +7,7 @@ import AttachmentCarouselPagerContext from '@components/Attachments/AttachmentCa import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; import type ChildrenProps from '@src/types/utils/ChildrenProps'; -import {defaultZoomRange} from './constants'; +import {defaultZoomRange, SPRING_CONFIG} from './constants'; import getCanvasFitScale from './getCanvasFitScale'; import type {CanvasSize, ContentSize, OnScaleChangedCallback, ZoomRange} from './types'; import usePanGesture from './usePanGesture'; @@ -22,9 +22,6 @@ type MultiGestureCanvasProps = ChildrenProps & { */ isActive: boolean; - /** Handles scale changed event */ - onScaleChanged: OnScaleChangedCallback; - /** The width and height of the canvas. * This is needed in order to properly scale the content in the canvas */ @@ -37,6 +34,9 @@ type MultiGestureCanvasProps = ChildrenProps & { /** Range of zoom that can be applied to the content by pinching or double tapping. */ zoomRange?: ZoomRange; + + /** Handles scale changed event */ + onScaleChanged?: OnScaleChangedCallback; }; function MultiGestureCanvas({ @@ -78,7 +78,7 @@ function MultiGestureCanvas({ */ const onScaleChanged = useCallback( (newScale: number) => { - onScaleChangedProp(newScale); + onScaleChangedProp?.(newScale); onScaleChangedContext(newScale); }, [onScaleChangedContext, onScaleChangedProp], @@ -135,13 +135,13 @@ function MultiGestureCanvas({ pinchScale.value = 1; if (animated) { - offsetX.value = withSpring(0, MultiGestureCanvasUtils.SPRING_CONFIG); - offsetY.value = withSpring(0, MultiGestureCanvasUtils.SPRING_CONFIG); - panTranslateX.value = withSpring(0, MultiGestureCanvasUtils.SPRING_CONFIG); - panTranslateY.value = withSpring(0, MultiGestureCanvasUtils.SPRING_CONFIG); - pinchTranslateX.value = withSpring(0, MultiGestureCanvasUtils.SPRING_CONFIG); - pinchTranslateY.value = withSpring(0, MultiGestureCanvasUtils.SPRING_CONFIG); - zoomScale.value = withSpring(1, MultiGestureCanvasUtils.SPRING_CONFIG); + offsetX.value = withSpring(0, SPRING_CONFIG); + offsetY.value = withSpring(0, SPRING_CONFIG); + panTranslateX.value = withSpring(0, SPRING_CONFIG); + panTranslateY.value = withSpring(0, SPRING_CONFIG); + pinchTranslateX.value = withSpring(0, SPRING_CONFIG); + pinchTranslateY.value = withSpring(0, SPRING_CONFIG); + zoomScale.value = withSpring(1, SPRING_CONFIG); return; } @@ -270,5 +270,5 @@ MultiGestureCanvas.displayName = 'MultiGestureCanvas'; export default MultiGestureCanvas; export {defaultZoomRange}; -export {zoomScaleBounceFactors} from './utils'; +export {zoomScaleBounceFactors} from './constants'; export type {MultiGestureCanvasProps}; diff --git a/src/components/MultiGestureCanvas/types.ts b/src/components/MultiGestureCanvas/types.ts index 0309a6cbcdfc..b4de586cd9da 100644 --- a/src/components/MultiGestureCanvas/types.ts +++ b/src/components/MultiGestureCanvas/types.ts @@ -20,7 +20,10 @@ type ZoomRange = { }; /** Triggered whenever the scale of the MultiGestureCanvas changes */ -type OnScaleChangedCallback = (zoomScale: number) => void; +type OnScaleChangedCallback = ((zoomScale: number) => void) | undefined; + +/** Triggered when the canvas is tapped (single tap) */ +type OnTapCallback = (() => void) | undefined; /** Types used of variables used within the MultiGestureCanvas component and it's hooks */ type MultiGestureCanvasVariables = { @@ -38,7 +41,7 @@ type MultiGestureCanvasVariables = { pinchTranslateY: SharedValue; stopAnimation: WorkletFunction<[], void>; reset: WorkletFunction<[boolean], void>; - onTap: () => void; + onTap: OnTapCallback; }; export type {CanvasSize, ContentSize, ZoomRange, OnScaleChangedCallback, MultiGestureCanvasVariables}; diff --git a/src/components/MultiGestureCanvas/usePanGesture.ts b/src/components/MultiGestureCanvas/usePanGesture.ts index 7e0aff08368f..7f7c2152d126 100644 --- a/src/components/MultiGestureCanvas/usePanGesture.ts +++ b/src/components/MultiGestureCanvas/usePanGesture.ts @@ -2,6 +2,7 @@ import type {PanGesture} from 'react-native-gesture-handler'; import {Gesture} from 'react-native-gesture-handler'; import {useDerivedValue, useSharedValue, withDecay, withSpring} from 'react-native-reanimated'; +import {SPRING_CONFIG} from './constants'; import type {CanvasSize, ContentSize, MultiGestureCanvasVariables} from './types'; import * as MultiGestureCanvasUtils from './utils'; @@ -95,7 +96,7 @@ const usePanGesture = ({canvasSize, contentSize, zoomScale, totalScale, offsetX, } } else { // Animated back to the boundary - offsetX.value = withSpring(clampedOffset.x, MultiGestureCanvasUtils.SPRING_CONFIG); + offsetX.value = withSpring(clampedOffset.x, SPRING_CONFIG); } if (isInVerticalBoundary) { @@ -110,7 +111,7 @@ const usePanGesture = ({canvasSize, contentSize, zoomScale, totalScale, offsetX, } } else { // Animated back to the boundary - offsetY.value = withSpring(clampedOffset.y, MultiGestureCanvasUtils.SPRING_CONFIG); + offsetY.value = withSpring(clampedOffset.y, SPRING_CONFIG); } // Reset velocity variables after we finished the pan gesture diff --git a/src/components/MultiGestureCanvas/usePinchGesture.ts b/src/components/MultiGestureCanvas/usePinchGesture.ts index 50c256933af5..d149316f6e85 100644 --- a/src/components/MultiGestureCanvas/usePinchGesture.ts +++ b/src/components/MultiGestureCanvas/usePinchGesture.ts @@ -3,6 +3,7 @@ import {useEffect, useState} from 'react'; import type {PinchGesture} from 'react-native-gesture-handler'; import {Gesture} from 'react-native-gesture-handler'; import {runOnJS, useAnimatedReaction, useSharedValue, withSpring} from 'react-native-reanimated'; +import {SPRING_CONFIG, zoomScaleBounceFactors} from './constants'; import type {CanvasSize, MultiGestureCanvasVariables, OnScaleChangedCallback, ZoomRange} from './types'; import * as MultiGestureCanvasUtils from './utils'; @@ -114,14 +115,11 @@ const usePinchGesture = ({ const newZoomScale = pinchScale.value * evt.scale; // Limit the zoom scale to zoom range including bounce range - if ( - zoomScale.value >= zoomRange.min * MultiGestureCanvasUtils.zoomScaleBounceFactors.min && - zoomScale.value <= zoomRange.max * MultiGestureCanvasUtils.zoomScaleBounceFactors.max - ) { + if (zoomScale.value >= zoomRange.min * zoomScaleBounceFactors.min && zoomScale.value <= zoomRange.max * zoomScaleBounceFactors.max) { zoomScale.value = newZoomScale; currentPinchScale.value = evt.scale; - if (onScaleChanged != null) { + if (onScaleChanged !== undefined) { runOnJS(onScaleChanged)(zoomScale.value); } } @@ -153,12 +151,12 @@ const usePinchGesture = ({ // If the content was "overzoomed" or "underzoomed", we need to bounce back with an animation if (pinchBounceTranslateX.value !== 0 || pinchBounceTranslateY.value !== 0) { - pinchBounceTranslateX.value = withSpring(0, MultiGestureCanvasUtils.SPRING_CONFIG); - pinchBounceTranslateY.value = withSpring(0, MultiGestureCanvasUtils.SPRING_CONFIG); + pinchBounceTranslateX.value = withSpring(0, SPRING_CONFIG); + pinchBounceTranslateY.value = withSpring(0, SPRING_CONFIG); } const triggerScaleChangeCallback = () => { - if (onScaleChanged == null) { + if (onScaleChanged === undefined) { return; } @@ -168,11 +166,11 @@ const usePinchGesture = ({ if (zoomScale.value < zoomRange.min) { // If the zoom scale is less than the minimum zoom scale, we need to set the zoom scale to the minimum pinchScale.value = zoomRange.min; - zoomScale.value = withSpring(zoomRange.min, MultiGestureCanvasUtils.SPRING_CONFIG, triggerScaleChangeCallback); + zoomScale.value = withSpring(zoomRange.min, SPRING_CONFIG, triggerScaleChangeCallback); } else if (zoomScale.value > zoomRange.max) { // If the zoom scale is higher than the maximum zoom scale, we need to set the zoom scale to the maximum pinchScale.value = zoomRange.max; - zoomScale.value = withSpring(zoomRange.max, MultiGestureCanvasUtils.SPRING_CONFIG, triggerScaleChangeCallback); + zoomScale.value = withSpring(zoomRange.max, SPRING_CONFIG, triggerScaleChangeCallback); } else { // Otherwise, we just update the pinch scale offset pinchScale.value = zoomScale.value; diff --git a/src/components/MultiGestureCanvas/useTapGestures.ts b/src/components/MultiGestureCanvas/useTapGestures.ts index 18439e07e626..ba928d08349c 100644 --- a/src/components/MultiGestureCanvas/useTapGestures.ts +++ b/src/components/MultiGestureCanvas/useTapGestures.ts @@ -3,6 +3,7 @@ import {useMemo} from 'react'; import type {TapGesture} from 'react-native-gesture-handler'; import {Gesture} from 'react-native-gesture-handler'; import {runOnJS, withSpring} from 'react-native-reanimated'; +import {SPRING_CONFIG} from './constants'; import type {CanvasSize, ContentSize, MultiGestureCanvasVariables, OnScaleChangedCallback} from './types'; import * as MultiGestureCanvasUtils from './utils'; @@ -109,9 +110,9 @@ const useTapGestures = ({ offsetAfterZooming.y = 0; } - offsetX.value = withSpring(offsetAfterZooming.x, MultiGestureCanvasUtils.SPRING_CONFIG); - offsetY.value = withSpring(offsetAfterZooming.y, MultiGestureCanvasUtils.SPRING_CONFIG); - zoomScale.value = withSpring(doubleTapScale, MultiGestureCanvasUtils.SPRING_CONFIG); + offsetX.value = withSpring(offsetAfterZooming.x, SPRING_CONFIG); + offsetY.value = withSpring(offsetAfterZooming.y, SPRING_CONFIG); + zoomScale.value = withSpring(doubleTapScale, SPRING_CONFIG); pinchScale.value = doubleTapScale; }, [scaledContentWidth, scaledContentHeight, canvasSize, doubleTapScale], @@ -130,7 +131,7 @@ const useTapGestures = ({ zoomToCoordinates(evt.x, evt.y); } - if (onScaleChanged != null) { + if (onScaleChanged !== undefined) { runOnJS(onScaleChanged)(zoomScale.value); } }); @@ -143,7 +144,7 @@ const useTapGestures = ({ }) // eslint-disable-next-line @typescript-eslint/naming-convention .onFinalize((_evt, success) => { - if (!success || !onTap) { + if (!success || onTap === undefined) { return; } diff --git a/src/components/MultiGestureCanvas/utils.ts b/src/components/MultiGestureCanvas/utils.ts index 26e814313f8f..d6d018f5be25 100644 --- a/src/components/MultiGestureCanvas/utils.ts +++ b/src/components/MultiGestureCanvas/utils.ts @@ -1,29 +1,7 @@ import {useCallback} from 'react'; import type {WorkletFunction} from 'react-native-reanimated/lib/typescript/reanimated2/commonTypes'; -// The spring config is used to determine the physics of the spring animation -// Details and a playground for testing different configs can be found at -// https://docs.swmansion.com/react-native-reanimated/docs/animations/withSpring -const SPRING_CONFIG = { - mass: 1, - stiffness: 1000, - damping: 500, -}; - -// The zoom scale bounce factors are used to determine the amount of bounce -// that is allowed when the user zooms more than the min or max zoom levels -const zoomScaleBounceFactors = { - min: 0.7, - max: 1.5, -}; - -/** - * Clamps a value between a lower and upper bound - * @param value - * @param lowerBound - * @param upperBound - * @returns - */ +/** Clamps a value between a lower and upper bound */ function clamp(value: number, lowerBound: number, upperBound: number) { 'worklet'; @@ -33,9 +11,6 @@ function clamp(value: number, lowerBound: number, upperBound: number) { /** * Creates a memoized callback on the UI thread * Same as `useWorkletCallback` from `react-native-reanimated` but without the deprecation warning - * @param callback - * @param deps - * @returns */ // eslint-disable-next-line @typescript-eslint/ban-types function useWorkletCallback( @@ -48,4 +23,4 @@ function useWorkletCallback( return useCallback<(...args: Args) => ReturnValue>(callback, deps) as WorkletFunction; } -export {SPRING_CONFIG, zoomScaleBounceFactors, clamp, useWorkletCallback}; +export {clamp, useWorkletCallback}; diff --git a/src/styles/utils/index.ts b/src/styles/utils/index.ts index 449489a33cce..059bc227393b 100644 --- a/src/styles/utils/index.ts +++ b/src/styles/utils/index.ts @@ -1008,6 +1008,10 @@ function getTransparentColor(color: string) { return `${color}00`; } +function getOpacityStyle(isHidden: boolean) { + return {opacity: isHidden ? 0 : 1}; +} + const staticStyleUtils = { positioning, combineStyles, @@ -1071,6 +1075,7 @@ const staticStyleUtils = { getEReceiptColorCode, getNavigationModalCardStyle, getCardStyles, + getOpacityStyle, }; const createStyleUtils = (theme: ThemeColors, styles: ThemeStyles) => ({ @@ -1432,8 +1437,6 @@ const createStyleUtils = (theme: ThemeColors, styles: ThemeStyles) => ({ }, getFullscreenCenteredContentStyles: () => [StyleSheet.absoluteFill, styles.justifyContentCenter, styles.alignItemsCenter], - - getLightboxVisibilityStyle: (isHidden: boolean) => ({opacity: isHidden ? 0 : 1}), }); type StyleUtilsType = ReturnType; From ef8050c96208e0bf4e2bba5f514fbf2f9ac38490 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Mon, 15 Jan 2024 14:02:57 +0100 Subject: [PATCH 227/580] Remove extra double negation, fix lint error --- src/components/ReportActionItem/ReportPreview.tsx | 4 ++-- src/libs/ReportUtils.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/ReportActionItem/ReportPreview.tsx b/src/components/ReportActionItem/ReportPreview.tsx index 1a347c039826..ecaa55ee0ef0 100644 --- a/src/components/ReportActionItem/ReportPreview.tsx +++ b/src/components/ReportActionItem/ReportPreview.tsx @@ -247,13 +247,13 @@ function ReportPreview({ role="button" accessibilityLabel={translate('iou.viewDetails')} > - + {hasReceipts && ( )} diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index da861d550566..b8e3d9c569ee 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -16,7 +16,7 @@ import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type {Beta, Login, PersonalDetails, PersonalDetailsList, Policy, Report, ReportAction, ReportMetadata, Session, Transaction} from '@src/types/onyx'; import type {Errors, Icon, PendingAction} from '@src/types/onyx/OnyxCommon'; -import type {IOUMessage, OriginalMessageActionName, OriginalMessageCreated, ReimbursementDeQueuedMessage, PaymentMethodType} from '@src/types/onyx/OriginalMessage'; +import type {IOUMessage, OriginalMessageActionName, OriginalMessageCreated, PaymentMethodType, ReimbursementDeQueuedMessage} from '@src/types/onyx/OriginalMessage'; import type {Status} from '@src/types/onyx/PersonalDetails'; import type {NotificationPreference} from '@src/types/onyx/Report'; import type {Message, ReportActionBase, ReportActions} from '@src/types/onyx/ReportAction'; From 999dfd5a2ffb40ed434f4a9ebcc34c30481e0bdc Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Mon, 15 Jan 2024 15:54:19 +0100 Subject: [PATCH 228/580] remove unused roles from CONST.ts --- src/CONST.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index 45cf7c8ea5c1..89ee5db8ba54 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -2841,16 +2841,12 @@ const CONST = { CHECKBOX: 'checkbox', /** Use for elements that allow a choice from multiple options. */ COMBOBOX: 'combobox', - /** Use for form elements. */ - FORM: 'form', /** Use with scrollable lists to represent a grid layout. */ GRID: 'grid', /** Use for section headers or titles. */ HEADING: 'heading', /** Use for image elements. */ IMG: 'img', - /** Use for input elements. */ - INPUT: 'input', /** Use for elements that navigate to other pages or content. */ LINK: 'link', /** Use to identify a list of items. */ From 82bd45387f3b67943b58b97917e61b3fb9c7ce7a Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Mon, 15 Jan 2024 16:14:44 +0100 Subject: [PATCH 229/580] address comment from review --- src/components/MagicCodeInput.tsx | 24 ++++++++---------------- 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/src/components/MagicCodeInput.tsx b/src/components/MagicCodeInput.tsx index 22068cf12590..f8b7106da3a6 100644 --- a/src/components/MagicCodeInput.tsx +++ b/src/components/MagicCodeInput.tsx @@ -88,8 +88,8 @@ const composeToString = (value: string[]): string => value.map((v) => (v === und const getInputPlaceholderSlots = (length: number): number[] => Array.from(Array(length).keys()); -function MagicCodeInput(props: MagicCodeInputProps, ref: ForwardedRef) { - const { +function MagicCodeInput( + { value = '', name = '', autoFocus = true, @@ -103,7 +103,9 @@ function MagicCodeInput(props: MagicCodeInputProps, ref: ForwardedRef, +) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); const inputRefs = useRef(); @@ -189,9 +191,6 @@ function MagicCodeInput(props: MagicCodeInputProps, ref: ForwardedRef) => { if (shouldFocusLast.current) { @@ -204,8 +203,6 @@ function MagicCodeInput(props: MagicCodeInputProps, ref: ForwardedRef { shouldFocusLast.current = false; @@ -224,8 +221,6 @@ function MagicCodeInput(props: MagicCodeInputProps, ref: ForwardedRef { if (!textValue?.length || !ValidationUtils.isNumeric(textValue)) { @@ -261,8 +256,6 @@ function MagicCodeInput(props: MagicCodeInputProps, ref: ForwardedRef>) => { const keyValue = event?.nativeEvent?.key; @@ -284,7 +277,7 @@ function MagicCodeInput(props: MagicCodeInputProps, ref: ForwardedRef >) => { - onPress(Math.floor((e.nativeEvent?.x ?? 0) / (inputWidth.current / maxLength))); + onBegan={(event: HandlerStateChangeEvent>) => { + onPress(Math.floor((event.nativeEvent?.x ?? 0) / (inputWidth.current / maxLength))); }} > {/* Android does not handle touch on invisible Views so I created a wrapper around invisible TextInput just to handle taps */} @@ -397,7 +390,6 @@ function MagicCodeInput(props: MagicCodeInputProps, ref: ForwardedRef From a2188fc7318da064f3fc78bf1485b52ea2257af2 Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Mon, 15 Jan 2024 16:19:07 +0100 Subject: [PATCH 230/580] remove unnecessary eslint disable --- src/components/RNTextInput.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/RNTextInput.tsx b/src/components/RNTextInput.tsx index 4016c5eafd9b..241573d336ae 100644 --- a/src/components/RNTextInput.tsx +++ b/src/components/RNTextInput.tsx @@ -3,7 +3,6 @@ import React from 'react'; import type {TextInputProps} from 'react-native'; import {TextInput} from 'react-native'; import Animated from 'react-native-reanimated'; -// eslint-disable-next-line no-restricted-imports 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 From 49c5c6be5f08216249f294f58a0084025cb8be8c Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Mon, 15 Jan 2024 17:25:38 +0100 Subject: [PATCH 231/580] 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 232/580] 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 233/580] 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 234/580] 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 235/580] 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 dce5acc386e4bf57341ed4eda6a319f2ef3ed404 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Mon, 15 Jan 2024 22:31:07 +0100 Subject: [PATCH 236/580] Prepare centralized setup for API --- src/libs/{API.ts => API/index.ts} | 36 ++++++---- .../parameters/AuthenticatePusherParams.ts | 10 +++ .../GetMissingOnyxMessagesParams.ts | 6 ++ .../parameters/HandleRestrictedEventParams.ts | 5 ++ src/libs/API/parameters/OpenAppParams.ts | 6 ++ .../API/parameters/OpenOldDotLinkParams.ts | 5 ++ src/libs/API/parameters/OpenProfileParams.ts | 5 ++ src/libs/API/parameters/OpenReportParams.ts | 12 ++++ src/libs/API/parameters/ReconnectAppParams.ts | 7 ++ .../RevealExpensifyCardDetailsParams.ts | 3 + .../parameters/UpdatePreferredLocaleParams.ts | 10 +++ src/libs/API/parameters/index.ts | 10 +++ src/libs/API/types.ts | 68 +++++++++++++++++++ src/libs/actions/App.ts | 53 ++++----------- src/libs/actions/Card.ts | 6 +- src/libs/actions/Link.ts | 3 +- src/libs/actions/Report.ts | 23 ++----- src/libs/actions/Session/index.ts | 13 +--- 18 files changed, 194 insertions(+), 87 deletions(-) rename src/libs/{API.ts => API/index.ts} (89%) create mode 100644 src/libs/API/parameters/AuthenticatePusherParams.ts create mode 100644 src/libs/API/parameters/GetMissingOnyxMessagesParams.ts create mode 100644 src/libs/API/parameters/HandleRestrictedEventParams.ts create mode 100644 src/libs/API/parameters/OpenAppParams.ts create mode 100644 src/libs/API/parameters/OpenOldDotLinkParams.ts create mode 100644 src/libs/API/parameters/OpenProfileParams.ts create mode 100644 src/libs/API/parameters/OpenReportParams.ts create mode 100644 src/libs/API/parameters/ReconnectAppParams.ts create mode 100644 src/libs/API/parameters/RevealExpensifyCardDetailsParams.ts create mode 100644 src/libs/API/parameters/UpdatePreferredLocaleParams.ts create mode 100644 src/libs/API/parameters/index.ts create mode 100644 src/libs/API/types.ts diff --git a/src/libs/API.ts b/src/libs/API/index.ts similarity index 89% rename from src/libs/API.ts rename to src/libs/API/index.ts index 4305469eafd5..d3751e0db8d2 100644 --- a/src/libs/API.ts +++ b/src/libs/API/index.ts @@ -1,15 +1,23 @@ import type {OnyxUpdate} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; -import type {ValueOf} from 'type-fest'; +import Log from '@libs/Log'; +import * as Middleware from '@libs/Middleware'; +import * as SequentialQueue from '@libs/Network/SequentialQueue'; +import * as Pusher from '@libs/Pusher/pusher'; +import * as Request from '@libs/Request'; import CONST from '@src/CONST'; import type OnyxRequest from '@src/types/onyx/Request'; import type Response from '@src/types/onyx/Response'; -import pkg from '../../package.json'; -import Log from './Log'; -import * as Middleware from './Middleware'; -import * as SequentialQueue from './Network/SequentialQueue'; -import * as Pusher from './Pusher/pusher'; -import * as Request from './Request'; +import pkg from '../../../package.json'; +import type { + ApiRequestWithSideEffects, + ReadCommand, + ReadCommandParameters, + SideEffectRequestCommand, + SideEffectRequestCommandParameters, + WriteCommand, + WriteCommandParameters, +} from './types'; // Setup API middlewares. Each request made will pass through a series of middleware functions that will get called in sequence (each one passing the result of the previous to the next). // Note: The ordering here is intentional as we want to Log, Recheck Connection, Reauthenticate, and Save the Response in Onyx. Errors thrown in one middleware will bubble to the next. @@ -38,8 +46,6 @@ type OnyxData = { finallyData?: OnyxUpdate[]; }; -type ApiRequestType = ValueOf; - /** * All calls to API.write() will be persisted to disk as JSON with the params, successData, and failureData (or finallyData, if included in place of the former two values). * This is so that if the network is unavailable or the app is closed, we can send the WRITE request later. @@ -54,7 +60,7 @@ type ApiRequestType = ValueOf; * @param [onyxData.failureData] - Onyx instructions that will be passed to Onyx.update() when the response has jsonCode !== 200. * @param [onyxData.finallyData] - Onyx instructions that will be passed to Onyx.update() when the response has jsonCode === 200 or jsonCode !== 200. */ -function write(command: string, apiCommandParameters: Record = {}, onyxData: OnyxData = {}) { +function write(command: TCommand, apiCommandParameters: WriteCommandParameters[TCommand], onyxData: OnyxData = {}) { Log.info('Called API write', false, {command, ...apiCommandParameters}); const {optimisticData, ...onyxDataWithoutOptimisticData} = onyxData; @@ -112,11 +118,11 @@ function write(command: string, apiCommandParameters: Record = * response back to the caller or to trigger reconnection callbacks when re-authentication is required. * @returns */ -function makeRequestWithSideEffects( - command: string, - apiCommandParameters = {}, +function makeRequestWithSideEffects( + command: TCommand, + apiCommandParameters: SideEffectRequestCommandParameters[TCommand], onyxData: OnyxData = {}, - apiRequestType: ApiRequestType = CONST.API_REQUEST_TYPE.MAKE_REQUEST_WITH_SIDE_EFFECTS, + apiRequestType: ApiRequestWithSideEffects = CONST.API_REQUEST_TYPE.MAKE_REQUEST_WITH_SIDE_EFFECTS, ): Promise { Log.info('Called API makeRequestWithSideEffects', false, {command, ...apiCommandParameters}); const {optimisticData, ...onyxDataWithoutOptimisticData} = onyxData; @@ -157,7 +163,7 @@ function makeRequestWithSideEffects( * @param [onyxData.failureData] - Onyx instructions that will be passed to Onyx.update() when the response has jsonCode !== 200. * @param [onyxData.finallyData] - Onyx instructions that will be passed to Onyx.update() when the response has jsonCode === 200 or jsonCode !== 200. */ -function read(command: string, apiCommandParameters: Record, onyxData: OnyxData = {}) { +function read(command: TCommand, apiCommandParameters: ReadCommandParameters[TCommand], onyxData: OnyxData = {}) { // Ensure all write requests on the sequential queue have finished responding before running read requests. // Responses from read requests can overwrite the optimistic data inserted by // write requests that use the same Onyx keys and haven't responded yet. diff --git a/src/libs/API/parameters/AuthenticatePusherParams.ts b/src/libs/API/parameters/AuthenticatePusherParams.ts new file mode 100644 index 000000000000..95e930431ccd --- /dev/null +++ b/src/libs/API/parameters/AuthenticatePusherParams.ts @@ -0,0 +1,10 @@ +type AuthenticatePusherParams = { + // eslint-disable-next-line @typescript-eslint/naming-convention + socket_id: string; + // eslint-disable-next-line @typescript-eslint/naming-convention + channel_name: string; + shouldRetry: boolean; + forceNetworkRequest: boolean; +}; + +export default AuthenticatePusherParams; diff --git a/src/libs/API/parameters/GetMissingOnyxMessagesParams.ts b/src/libs/API/parameters/GetMissingOnyxMessagesParams.ts new file mode 100644 index 000000000000..7f98d8c0116c --- /dev/null +++ b/src/libs/API/parameters/GetMissingOnyxMessagesParams.ts @@ -0,0 +1,6 @@ +type GetMissingOnyxMessagesParams = { + updateIDFrom: number; + updateIDTo: number | string; +}; + +export default GetMissingOnyxMessagesParams; \ No newline at end of file diff --git a/src/libs/API/parameters/HandleRestrictedEventParams.ts b/src/libs/API/parameters/HandleRestrictedEventParams.ts new file mode 100644 index 000000000000..808220f07747 --- /dev/null +++ b/src/libs/API/parameters/HandleRestrictedEventParams.ts @@ -0,0 +1,5 @@ +type HandleRestrictedEventParams = { + eventName: string; +}; + +export default HandleRestrictedEventParams; diff --git a/src/libs/API/parameters/OpenAppParams.ts b/src/libs/API/parameters/OpenAppParams.ts new file mode 100644 index 000000000000..ac0100109c51 --- /dev/null +++ b/src/libs/API/parameters/OpenAppParams.ts @@ -0,0 +1,6 @@ +type OpenAppParams = { + policyIDList: string[]; + enablePriorityModeFilter: boolean; +}; + +export default OpenAppParams; diff --git a/src/libs/API/parameters/OpenOldDotLinkParams.ts b/src/libs/API/parameters/OpenOldDotLinkParams.ts new file mode 100644 index 000000000000..873b1550368f --- /dev/null +++ b/src/libs/API/parameters/OpenOldDotLinkParams.ts @@ -0,0 +1,5 @@ +type OpenOldDotLinkParams = { + shouldRetry?: boolean; +}; + +export default OpenOldDotLinkParams; diff --git a/src/libs/API/parameters/OpenProfileParams.ts b/src/libs/API/parameters/OpenProfileParams.ts new file mode 100644 index 000000000000..f42ea8234fc8 --- /dev/null +++ b/src/libs/API/parameters/OpenProfileParams.ts @@ -0,0 +1,5 @@ +type OpenProfileParams = { + timezone: string; +}; + +export default OpenProfileParams; diff --git a/src/libs/API/parameters/OpenReportParams.ts b/src/libs/API/parameters/OpenReportParams.ts new file mode 100644 index 000000000000..477a002516de --- /dev/null +++ b/src/libs/API/parameters/OpenReportParams.ts @@ -0,0 +1,12 @@ +type OpenReportParams = { + reportID: string; + emailList?: string; + accountIDList?: string; + parentReportActionID?: string; + shouldRetry?: boolean; + createdReportActionID?: string; + clientLastReadTime?: string; + idempotencyKey?: string; +}; + +export default OpenReportParams; diff --git a/src/libs/API/parameters/ReconnectAppParams.ts b/src/libs/API/parameters/ReconnectAppParams.ts new file mode 100644 index 000000000000..8c5b7d6c0da9 --- /dev/null +++ b/src/libs/API/parameters/ReconnectAppParams.ts @@ -0,0 +1,7 @@ +type ReconnectAppParams = { + mostRecentReportActionLastModified?: string; + updateIDFrom?: number; + policyIDList: string[]; +}; + +export default ReconnectAppParams; diff --git a/src/libs/API/parameters/RevealExpensifyCardDetailsParams.ts b/src/libs/API/parameters/RevealExpensifyCardDetailsParams.ts new file mode 100644 index 000000000000..ec698fc85269 --- /dev/null +++ b/src/libs/API/parameters/RevealExpensifyCardDetailsParams.ts @@ -0,0 +1,3 @@ +type RevealExpensifyCardDetailsParams = {cardID: number}; + +export default RevealExpensifyCardDetailsParams; diff --git a/src/libs/API/parameters/UpdatePreferredLocaleParams.ts b/src/libs/API/parameters/UpdatePreferredLocaleParams.ts new file mode 100644 index 000000000000..5dd991dea3b5 --- /dev/null +++ b/src/libs/API/parameters/UpdatePreferredLocaleParams.ts @@ -0,0 +1,10 @@ +import type {ValueOf} from 'type-fest'; +import type CONST from '@src/CONST'; + +type Locale = ValueOf; + +type UpdatePreferredLocaleParams = { + value: Locale; +}; + +export default UpdatePreferredLocaleParams; diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts new file mode 100644 index 000000000000..f895a4e13485 --- /dev/null +++ b/src/libs/API/parameters/index.ts @@ -0,0 +1,10 @@ +export type {default as AuthenticatePusherParams} from './AuthenticatePusherParams'; +export type {default as HandleRestrictedEventParams} from './HandleRestrictedEventParams'; +export type {default as OpenOldDotLinkParams} from './OpenOldDotLinkParams'; +export type {default as OpenReportParams} from './OpenReportParams'; +export type {default as RevealExpensifyCardDetailsParams} from './RevealExpensifyCardDetailsParams'; +export type {default as GetMissingOnyxMessagesParams} from './GetMissingOnyxMessagesParams'; +export type {default as OpenAppParams} from './OpenAppParams'; +export type {default as OpenProfileParams} from './OpenProfileParams'; +export type {default as ReconnectAppParams} from './ReconnectAppParams'; +export type {default as UpdatePreferredLocaleParams} from './UpdatePreferredLocaleParams'; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts new file mode 100644 index 000000000000..3d511e9bbab7 --- /dev/null +++ b/src/libs/API/types.ts @@ -0,0 +1,68 @@ +import type {ValueOf} from 'type-fest'; +import type CONST from '@src/CONST'; +import type { + AuthenticatePusherParams, + GetMissingOnyxMessagesParams, + HandleRestrictedEventParams, + OpenAppParams, + OpenOldDotLinkParams, + OpenProfileParams, + OpenReportParams, + ReconnectAppParams, + RevealExpensifyCardDetailsParams, + UpdatePreferredLocaleParams, +} from './parameters'; + +type ApiRequestWithSideEffects = ValueOf; + +const WRITE_COMMANDS = { + UPDATE_PREFERRED_LOCALE: 'UpdatePreferredLocale', + RECONNECT_APP: 'ReconnectApp', + OPEN_PROFILE: 'OpenProfile', + HANDLE_RESTRICTED_EVENT: 'HandleRestrictedEvent', + OPEN_REPORT: 'OpenReport', +} as const; + +type WriteCommand = ValueOf; + +type WriteCommandParameters = { + [WRITE_COMMANDS.UPDATE_PREFERRED_LOCALE]: UpdatePreferredLocaleParams; + [WRITE_COMMANDS.RECONNECT_APP]: ReconnectAppParams; + [WRITE_COMMANDS.OPEN_PROFILE]: OpenProfileParams; + [WRITE_COMMANDS.HANDLE_RESTRICTED_EVENT]: HandleRestrictedEventParams; + [WRITE_COMMANDS.OPEN_REPORT]: HandleRestrictedEventParams; +}; + +const READ_COMMANDS = { + OPEN_APP: 'OpenApp', +} as const; + +type ReadCommand = ValueOf; + +type ReadCommandParameters = { + [READ_COMMANDS.OPEN_APP]: OpenAppParams; +}; + +const SIDE_EFFECT_REQUEST_COMMANDS = { + AUTHENTICATE_PUSHER: 'AuthenticatePusher', + OPEN_REPORT: 'OpenReport', + OPEN_OLD_DOT_LINK: 'OpenOldDotLink', + REVEAL_EXPENSIFY_CARD_DETAILS: 'RevealExpensifyCardDetails', + GET_MISSING_ONYX_MESSAGES: 'GetMissingOnyxMessages', + RECONNECT_APP: 'ReconnectApp', +} as const; + +type SideEffectRequestCommand = ReadCommand | ValueOf; + +type SideEffectRequestCommandParameters = ReadCommandParameters & { + [SIDE_EFFECT_REQUEST_COMMANDS.AUTHENTICATE_PUSHER]: AuthenticatePusherParams; + [SIDE_EFFECT_REQUEST_COMMANDS.OPEN_REPORT]: OpenReportParams; + [SIDE_EFFECT_REQUEST_COMMANDS.OPEN_OLD_DOT_LINK]: OpenOldDotLinkParams; + [SIDE_EFFECT_REQUEST_COMMANDS.REVEAL_EXPENSIFY_CARD_DETAILS]: RevealExpensifyCardDetailsParams; + [SIDE_EFFECT_REQUEST_COMMANDS.GET_MISSING_ONYX_MESSAGES]: GetMissingOnyxMessagesParams; + [SIDE_EFFECT_REQUEST_COMMANDS.RECONNECT_APP]: ReconnectAppParams; +}; + +export {WRITE_COMMANDS, READ_COMMANDS, SIDE_EFFECT_REQUEST_COMMANDS}; + +export type {ApiRequestWithSideEffects, WriteCommand, WriteCommandParameters, ReadCommand, ReadCommandParameters, SideEffectRequestCommand, SideEffectRequestCommandParameters}; diff --git a/src/libs/actions/App.ts b/src/libs/actions/App.ts index 768dc530cc51..595a1611fe1f 100644 --- a/src/libs/actions/App.ts +++ b/src/libs/actions/App.ts @@ -6,6 +6,10 @@ import type {OnyxCollection, OnyxEntry, OnyxUpdate} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import * as API from '@libs/API'; +import type {GetMissingOnyxMessagesParams, OpenOldDotLinkParams} from '@libs/API/parameters'; +import type {HandleRestrictedEventParams, OpenProfileParams, ReconnectAppParams, UpdatePreferredLocaleParams} from '@libs/API/parameters/HandleRestrictedEventParams'; +import type {OpenAppParams} from '@libs/API/parameters/OpenAppParams'; +import {READ_COMMANDS, SIDE_EFFECT_REQUEST_COMMANDS, WRITE_COMMANDS} from '@libs/API/types'; import * as Browser from '@libs/Browser'; import DateUtils from '@libs/DateUtils'; import Log from '@libs/Log'; @@ -103,15 +107,11 @@ function setLocale(locale: Locale) { }, ]; - type UpdatePreferredLocaleParams = { - value: Locale; - }; - const parameters: UpdatePreferredLocaleParams = { value: locale, }; - API.write('UpdatePreferredLocale', parameters, {optimisticData}); + API.write(WRITE_COMMANDS.UPDATE_PREFERRED_LOCALE, parameters, {optimisticData}); } function setLocaleAndNavigate(locale: Locale) { @@ -203,13 +203,9 @@ function getOnyxDataForOpenOrReconnect(isOpenApp = false): OnyxData { */ function openApp() { getPolicyParamsForOpenOrReconnect().then((policyParams: PolicyParamsForOpenOrReconnect) => { - type OpenAppParams = PolicyParamsForOpenOrReconnect & { - enablePriorityModeFilter: boolean; - }; - const params: OpenAppParams = {enablePriorityModeFilter: true, ...policyParams}; - API.read('OpenApp', params, getOnyxDataForOpenOrReconnect(true)); + API.read(READ_COMMANDS.OPEN_APP, params, getOnyxDataForOpenOrReconnect(true)); }); } @@ -220,12 +216,6 @@ function openApp() { function reconnectApp(updateIDFrom: OnyxEntry = 0) { console.debug(`[OnyxUpdates] App reconnecting with updateIDFrom: ${updateIDFrom}`); getPolicyParamsForOpenOrReconnect().then((policyParams) => { - type ReconnectParams = { - mostRecentReportActionLastModified?: string; - updateIDFrom?: number; - }; - type ReconnectAppParams = PolicyParamsForOpenOrReconnect & ReconnectParams; - const params: ReconnectAppParams = {...policyParams}; // When the app reconnects we do a fast "sync" of the LHN and only return chats that have new messages. We achieve this by sending the most recent reportActionID. @@ -243,7 +233,7 @@ function reconnectApp(updateIDFrom: OnyxEntry = 0) { params.updateIDFrom = updateIDFrom; } - API.write('ReconnectApp', params, getOnyxDataForOpenOrReconnect()); + API.write(WRITE_COMMANDS.RECONNECT_APP, params, getOnyxDataForOpenOrReconnect()); }); } @@ -255,8 +245,6 @@ function reconnectApp(updateIDFrom: OnyxEntry = 0) { function finalReconnectAppAfterActivatingReliableUpdates(): Promise { console.debug(`[OnyxUpdates] Executing last reconnect app with promise`); return getPolicyParamsForOpenOrReconnect().then((policyParams) => { - type ReconnectAppParams = PolicyParamsForOpenOrReconnect & {mostRecentReportActionLastModified?: string}; - const params: ReconnectAppParams = {...policyParams}; // When the app reconnects we do a fast "sync" of the LHN and only return chats that have new messages. We achieve this by sending the most recent reportActionID. @@ -273,7 +261,7 @@ function finalReconnectAppAfterActivatingReliableUpdates(): Promise { console.debug(`[OnyxUpdates] Fetching missing updates updateIDFrom: ${updateIDFrom} and updateIDTo: ${updateIDTo}`); - type GetMissingOnyxMessagesParams = { - updateIDFrom: number; - updateIDTo: number | string; - }; - const parameters: GetMissingOnyxMessagesParams = { updateIDFrom, updateIDTo, @@ -299,7 +282,7 @@ function getMissingOnyxUpdates(updateIDFrom = 0, updateIDTo: number | string = 0 // DO NOT FOLLOW THIS PATTERN!!!!! // It was absolutely necessary in order to block OnyxUpdates while fetching the missing updates from the server or else the udpates aren't applied in the proper order. // eslint-disable-next-line rulesdir/no-api-side-effects-method - return API.makeRequestWithSideEffects('GetMissingOnyxMessages', parameters, getOnyxDataForOpenOrReconnect()); + return API.makeRequestWithSideEffects(SIDE_EFFECT_REQUEST_COMMANDS.GET_MISSING_ONYX_MESSAGES, parameters, getOnyxDataForOpenOrReconnect()); } /** @@ -436,17 +419,13 @@ function openProfile(personalDetails: OnyxTypes.PersonalDetails) { newTimezoneData = DateUtils.formatToSupportedTimezone(newTimezoneData); - type OpenProfileParams = { - timezone: string; - }; - const parameters: OpenProfileParams = { timezone: JSON.stringify(newTimezoneData), }; // We expect currentUserAccountID to be a number because it doesn't make sense to open profile if currentUserAccountID is not set if (typeof currentUserAccountID === 'number') { - API.write('OpenProfile', parameters, { + API.write(WRITE_COMMANDS.OPEN_PROFILE, parameters, { optimisticData: [ { onyxMethod: Onyx.METHOD.MERGE, @@ -489,14 +468,10 @@ function beginDeepLinkRedirect(shouldAuthenticateWithCurrentAccount = true) { return; } - type OpenOldDotLinkParams = { - shouldRetry: boolean; - }; - const parameters: OpenOldDotLinkParams = {shouldRetry: false}; // eslint-disable-next-line rulesdir/no-api-side-effects-method - API.makeRequestWithSideEffects('OpenOldDotLink', parameters, {}).then((response) => { + API.makeRequestWithSideEffects(SIDE_EFFECT_REQUEST_COMMANDS.OPEN_OLD_DOT_LINK, parameters, {}).then((response) => { if (!response) { Log.alert( 'Trying to redirect via deep link, but the response is empty. User likely not authenticated.', @@ -518,13 +493,9 @@ function beginDeepLinkRedirectAfterTransition(shouldAuthenticateWithCurrentAccou } function handleRestrictedEvent(eventName: string) { - type HandleRestrictedEventParams = { - eventName: string; - }; - const parameters: HandleRestrictedEventParams = {eventName}; - API.write('HandleRestrictedEvent', parameters); + API.write(WRITE_COMMANDS.HANDLE_RESTRICTED_EVENT, parameters); } export { diff --git a/src/libs/actions/Card.ts b/src/libs/actions/Card.ts index 172b0ac73ca6..0d583001ddc9 100644 --- a/src/libs/actions/Card.ts +++ b/src/libs/actions/Card.ts @@ -1,6 +1,8 @@ import Onyx from 'react-native-onyx'; import type {OnyxUpdate} from 'react-native-onyx'; import * as API from '@libs/API'; +import type {RevealExpensifyCardDetailsParams} from '@libs/API/parameters'; +import {SIDE_EFFECT_REQUEST_COMMANDS} from '@libs/API/types'; import * as Localize from '@libs/Localize'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -173,12 +175,10 @@ function clearCardListErrors(cardID: number) { */ function revealVirtualCardDetails(cardID: number): Promise { return new Promise((resolve, reject) => { - type RevealExpensifyCardDetailsParams = {cardID: number}; - const parameters: RevealExpensifyCardDetailsParams = {cardID}; // eslint-disable-next-line rulesdir/no-api-side-effects-method - API.makeRequestWithSideEffects('RevealExpensifyCardDetails', parameters) + API.makeRequestWithSideEffects(SIDE_EFFECT_REQUEST_COMMANDS.REVEAL_EXPENSIFY_CARD_DETAILS, parameters) .then((response) => { if (response?.jsonCode !== CONST.JSON_CODE.SUCCESS) { reject(Localize.translateLocal('cardPage.cardDetailsLoadingFailure')); diff --git a/src/libs/actions/Link.ts b/src/libs/actions/Link.ts index 2fb863467e32..d41f9db89e97 100644 --- a/src/libs/actions/Link.ts +++ b/src/libs/actions/Link.ts @@ -1,5 +1,6 @@ import Onyx from 'react-native-onyx'; import * as API from '@libs/API'; +import {SIDE_EFFECT_REQUEST_COMMANDS} from '@libs/API/types'; import asyncOpenURL from '@libs/asyncOpenURL'; import * as Environment from '@libs/Environment/Environment'; import Navigation from '@libs/Navigation/Navigation'; @@ -55,7 +56,7 @@ function openOldDotLink(url: string) { // If shortLivedAuthToken is not accessible, fallback to opening the link without the token. asyncOpenURL( // eslint-disable-next-line rulesdir/no-api-side-effects-method - API.makeRequestWithSideEffects('OpenOldDotLink', {}, {}) + API.makeRequestWithSideEffects(SIDE_EFFECT_REQUEST_COMMANDS.OPEN_OLD_DOT_LINK, {}, {}) .then((response) => (response ? buildOldDotURL(url, response.shortLivedAuthToken) : buildOldDotURL(url))) .catch(() => buildOldDotURL(url)), (oldDotURL) => oldDotURL, diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index b182b7019846..5535b04064c6 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -10,6 +10,8 @@ import type {PartialDeep, ValueOf} from 'type-fest'; import type {Emoji} from '@assets/emojis/types'; import * as ActiveClientManager from '@libs/ActiveClientManager'; import * as API from '@libs/API'; +import {SIDE_EFFECT_REQUEST_COMMANDS} from '@libs/API/types'; +import type {OpenReportParams} from '@libs/API/parameters'; import * as CollectionUtils from '@libs/CollectionUtils'; import DateUtils from '@libs/DateUtils'; import * as EmojiUtils from '@libs/EmojiUtils'; @@ -490,8 +492,6 @@ function openReport( reportName: allReports?.[reportID]?.reportName ?? CONST.REPORT.DEFAULT_REPORT_NAME, }; - const commandName = 'OpenReport'; - const optimisticData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, @@ -543,23 +543,12 @@ function openReport( }, ]; - type OpenReportParameters = { - reportID: string; - emailList?: string; - accountIDList?: string; - parentReportActionID?: string; - shouldRetry?: boolean; - createdReportActionID?: string; - clientLastReadTime?: string; - idempotencyKey?: string; - }; - - const parameters: OpenReportParameters = { + const parameters: OpenReportParams = { reportID, emailList: participantLoginList ? participantLoginList.join(',') : '', accountIDList: participantAccountIDList ? participantAccountIDList.join(',') : '', parentReportActionID, - idempotencyKey: `${commandName}_${reportID}`, + idempotencyKey: `${SIDE_EFFECT_REQUEST_COMMANDS.OPEN_REPORT}_${reportID}`, }; if (isFromDeepLink) { @@ -664,12 +653,12 @@ function openReport( if (isFromDeepLink) { // eslint-disable-next-line rulesdir/no-api-side-effects-method - API.makeRequestWithSideEffects(commandName, parameters, {optimisticData, successData, failureData}).finally(() => { + API.makeRequestWithSideEffects(SIDE_EFFECT_REQUEST_COMMANDS.OPEN_REPORT, parameters, {optimisticData, successData, failureData}).finally(() => { Onyx.set(ONYXKEYS.IS_CHECKING_PUBLIC_ROOM, false); }); } else { // eslint-disable-next-line rulesdir/no-multiple-api-calls - API.write(commandName, parameters, {optimisticData, successData, failureData}); + API.write(SIDE_EFFECT_REQUEST_COMMANDS.OPEN_REPORT, parameters, {optimisticData, successData, failureData}); } } diff --git a/src/libs/actions/Session/index.ts b/src/libs/actions/Session/index.ts index bde2954e191a..eff52dbfa4fe 100644 --- a/src/libs/actions/Session/index.ts +++ b/src/libs/actions/Session/index.ts @@ -7,6 +7,8 @@ import Onyx from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import * as PersistedRequests from '@libs/actions/PersistedRequests'; import * as API from '@libs/API'; +import type {AuthenticatePusherParams} from '@libs/API/parameters/sideEffectRequest'; +import {SIDE_EFFECT_REQUEST_COMMANDS} from '@libs/API/types'; import * as Authentication from '@libs/Authentication'; import * as ErrorUtils from '@libs/ErrorUtils'; import HttpUtils from '@libs/HttpUtils'; @@ -665,15 +667,6 @@ const reauthenticatePusher = throttle( function authenticatePusher(socketID: string, channelName: string, callback: ChannelAuthorizationCallback) { Log.info('[PusherAuthorizer] Attempting to authorize Pusher', false, {channelName}); - type AuthenticatePusherParams = { - // eslint-disable-next-line @typescript-eslint/naming-convention - socket_id: string; - // eslint-disable-next-line @typescript-eslint/naming-convention - channel_name: string; - shouldRetry: boolean; - forceNetworkRequest: boolean; - }; - const params: AuthenticatePusherParams = { // eslint-disable-next-line @typescript-eslint/naming-convention socket_id: socketID, @@ -685,7 +678,7 @@ function authenticatePusher(socketID: string, channelName: string, callback: Cha // We use makeRequestWithSideEffects here because we need to authorize to Pusher (an external service) each time a user connects to any channel. // eslint-disable-next-line rulesdir/no-api-side-effects-method - API.makeRequestWithSideEffects('AuthenticatePusher', params) + API.makeRequestWithSideEffects(SIDE_EFFECT_REQUEST_COMMANDS.AUTHENTICATE_PUSHER, params) .then((response) => { if (response?.jsonCode === CONST.JSON_CODE.NOT_AUTHENTICATED) { Log.hmmm('[PusherAuthorizer] Unable to authenticate Pusher because authToken is expired'); From 44c85c5fb3cb4e6e2a38ea3333bbefa7cfc7e17e Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Mon, 15 Jan 2024 23:11:33 +0100 Subject: [PATCH 237/580] Add all read commands --- src/libs/API/parameters/BeginSignInParams.ts | 5 ++ .../API/parameters/ExpandURLPreviewParams.ts | 6 ++ .../API/parameters/GetNewerActionsParams.ts | 6 ++ .../API/parameters/GetOlderActionsParams.ts | 6 ++ .../parameters/GetReportPrivateNoteParams.ts | 5 ++ .../API/parameters/GetRouteForDraftParams.ts | 6 ++ src/libs/API/parameters/GetRouteParams.ts | 6 ++ .../API/parameters/GetStatementPDFParams.ts | 5 ++ .../OpenPlaidBankAccountSelectorParams.ts | 8 ++ .../parameters/OpenPlaidBankLoginParams.ts | 7 ++ .../parameters/OpenPublicProfilePageParams.ts | 5 ++ .../OpenReimbursementAccountPageParams.ts | 12 +++ .../parameters/OpenRoomMembersPageParams.ts | 5 ++ .../API/parameters/SearchForReportsParams.ts | 5 ++ .../parameters/SendPerformanceTimingParams.ts | 7 ++ .../SignInWithShortLivedAuthTokenParams.ts | 7 ++ src/libs/API/parameters/index.ts | 16 ++++ src/libs/API/types.ts | 63 ++++++++++++++ src/libs/actions/App.ts | 12 ++- src/libs/actions/BankAccounts.ts | 12 +-- src/libs/actions/MapboxToken.ts | 5 +- src/libs/actions/PaymentMethods.ts | 3 +- src/libs/actions/PersonalDetails.ts | 14 +--- src/libs/actions/Plaid.ts | 82 ++++++++++--------- src/libs/actions/Report.ts | 63 +++++--------- src/libs/actions/Session/index.ts | 18 +--- src/libs/actions/Timing.ts | 18 ++-- src/libs/actions/Transaction.ts | 30 ++++--- src/libs/actions/User.ts | 6 +- src/libs/actions/Wallet.ts | 14 +--- 30 files changed, 300 insertions(+), 157 deletions(-) create mode 100644 src/libs/API/parameters/BeginSignInParams.ts create mode 100644 src/libs/API/parameters/ExpandURLPreviewParams.ts create mode 100644 src/libs/API/parameters/GetNewerActionsParams.ts create mode 100644 src/libs/API/parameters/GetOlderActionsParams.ts create mode 100644 src/libs/API/parameters/GetReportPrivateNoteParams.ts create mode 100644 src/libs/API/parameters/GetRouteForDraftParams.ts create mode 100644 src/libs/API/parameters/GetRouteParams.ts create mode 100644 src/libs/API/parameters/GetStatementPDFParams.ts create mode 100644 src/libs/API/parameters/OpenPlaidBankAccountSelectorParams.ts create mode 100644 src/libs/API/parameters/OpenPlaidBankLoginParams.ts create mode 100644 src/libs/API/parameters/OpenPublicProfilePageParams.ts create mode 100644 src/libs/API/parameters/OpenReimbursementAccountPageParams.ts create mode 100644 src/libs/API/parameters/OpenRoomMembersPageParams.ts create mode 100644 src/libs/API/parameters/SearchForReportsParams.ts create mode 100644 src/libs/API/parameters/SendPerformanceTimingParams.ts create mode 100644 src/libs/API/parameters/SignInWithShortLivedAuthTokenParams.ts diff --git a/src/libs/API/parameters/BeginSignInParams.ts b/src/libs/API/parameters/BeginSignInParams.ts new file mode 100644 index 000000000000..2f85a3335c62 --- /dev/null +++ b/src/libs/API/parameters/BeginSignInParams.ts @@ -0,0 +1,5 @@ +type BeginSignInParams = { + email: string; +}; + +export default BeginSignInParams; diff --git a/src/libs/API/parameters/ExpandURLPreviewParams.ts b/src/libs/API/parameters/ExpandURLPreviewParams.ts new file mode 100644 index 000000000000..1b0e7e6fc78d --- /dev/null +++ b/src/libs/API/parameters/ExpandURLPreviewParams.ts @@ -0,0 +1,6 @@ +type ExpandURLPreviewParams = { + reportID: string; + reportActionID: string; +}; + +export default ExpandURLPreviewParams; diff --git a/src/libs/API/parameters/GetNewerActionsParams.ts b/src/libs/API/parameters/GetNewerActionsParams.ts new file mode 100644 index 000000000000..76ab1938b640 --- /dev/null +++ b/src/libs/API/parameters/GetNewerActionsParams.ts @@ -0,0 +1,6 @@ +type GetNewerActionsParams = { + reportID: string; + reportActionID: string; +}; + +export default GetNewerActionsParams; diff --git a/src/libs/API/parameters/GetOlderActionsParams.ts b/src/libs/API/parameters/GetOlderActionsParams.ts new file mode 100644 index 000000000000..4e585ba8afdd --- /dev/null +++ b/src/libs/API/parameters/GetOlderActionsParams.ts @@ -0,0 +1,6 @@ +type GetOlderActionsParams = { + reportID: string; + reportActionID: string; +}; + +export default GetOlderActionsParams; diff --git a/src/libs/API/parameters/GetReportPrivateNoteParams.ts b/src/libs/API/parameters/GetReportPrivateNoteParams.ts new file mode 100644 index 000000000000..3e52119c164f --- /dev/null +++ b/src/libs/API/parameters/GetReportPrivateNoteParams.ts @@ -0,0 +1,5 @@ +type GetReportPrivateNoteParams = { + reportID: string; +}; + +export default GetReportPrivateNoteParams; diff --git a/src/libs/API/parameters/GetRouteForDraftParams.ts b/src/libs/API/parameters/GetRouteForDraftParams.ts new file mode 100644 index 000000000000..5a213c3f2d49 --- /dev/null +++ b/src/libs/API/parameters/GetRouteForDraftParams.ts @@ -0,0 +1,6 @@ +type GetRouteForDraftParams = { + transactionID: string; + waypoints: string; +}; + +export default GetRouteForDraftParams; diff --git a/src/libs/API/parameters/GetRouteParams.ts b/src/libs/API/parameters/GetRouteParams.ts new file mode 100644 index 000000000000..d6ff7b972e0d --- /dev/null +++ b/src/libs/API/parameters/GetRouteParams.ts @@ -0,0 +1,6 @@ +type GetRouteParams = { + transactionID: string; + waypoints: string; +}; + +export default GetRouteParams; diff --git a/src/libs/API/parameters/GetStatementPDFParams.ts b/src/libs/API/parameters/GetStatementPDFParams.ts new file mode 100644 index 000000000000..3fa8bb732531 --- /dev/null +++ b/src/libs/API/parameters/GetStatementPDFParams.ts @@ -0,0 +1,5 @@ +type GetStatementPDFParams = { + period: string; +}; + +export default GetStatementPDFParams; diff --git a/src/libs/API/parameters/OpenPlaidBankAccountSelectorParams.ts b/src/libs/API/parameters/OpenPlaidBankAccountSelectorParams.ts new file mode 100644 index 000000000000..c92d72460fa9 --- /dev/null +++ b/src/libs/API/parameters/OpenPlaidBankAccountSelectorParams.ts @@ -0,0 +1,8 @@ +type OpenPlaidBankAccountSelectorParams = { + publicToken: string; + allowDebit: boolean; + bank: string; + bankAccountID: number; +}; + +export default OpenPlaidBankAccountSelectorParams; diff --git a/src/libs/API/parameters/OpenPlaidBankLoginParams.ts b/src/libs/API/parameters/OpenPlaidBankLoginParams.ts new file mode 100644 index 000000000000..f76e05423d03 --- /dev/null +++ b/src/libs/API/parameters/OpenPlaidBankLoginParams.ts @@ -0,0 +1,7 @@ +type OpenPlaidBankLoginParams = { + redirectURI: string | undefined; + allowDebit: boolean; + bankAccountID: number; +}; + +export default OpenPlaidBankLoginParams; diff --git a/src/libs/API/parameters/OpenPublicProfilePageParams.ts b/src/libs/API/parameters/OpenPublicProfilePageParams.ts new file mode 100644 index 000000000000..3bb50c563e28 --- /dev/null +++ b/src/libs/API/parameters/OpenPublicProfilePageParams.ts @@ -0,0 +1,5 @@ +type OpenPublicProfilePageParams = { + accountID: number; +}; + +export default OpenPublicProfilePageParams; diff --git a/src/libs/API/parameters/OpenReimbursementAccountPageParams.ts b/src/libs/API/parameters/OpenReimbursementAccountPageParams.ts new file mode 100644 index 000000000000..d831609b2e0a --- /dev/null +++ b/src/libs/API/parameters/OpenReimbursementAccountPageParams.ts @@ -0,0 +1,12 @@ +import type {BankAccountStep, BankAccountSubStep} from '@src/types/onyx/ReimbursementAccount'; + +type ReimbursementAccountStep = BankAccountStep | ''; +type ReimbursementAccountSubStep = BankAccountSubStep | ''; + +type OpenReimbursementAccountPageParams = { + stepToOpen: ReimbursementAccountStep; + subStep: ReimbursementAccountSubStep; + localCurrentStep: ReimbursementAccountStep; +}; + +export default OpenReimbursementAccountPageParams; diff --git a/src/libs/API/parameters/OpenRoomMembersPageParams.ts b/src/libs/API/parameters/OpenRoomMembersPageParams.ts new file mode 100644 index 000000000000..7ea1afb9bdb9 --- /dev/null +++ b/src/libs/API/parameters/OpenRoomMembersPageParams.ts @@ -0,0 +1,5 @@ +type OpenRoomMembersPageParams = { + reportID: string; +}; + +export default OpenRoomMembersPageParams; diff --git a/src/libs/API/parameters/SearchForReportsParams.ts b/src/libs/API/parameters/SearchForReportsParams.ts new file mode 100644 index 000000000000..b6d1bbadb1dc --- /dev/null +++ b/src/libs/API/parameters/SearchForReportsParams.ts @@ -0,0 +1,5 @@ +type SearchForReportsParams = { + searchInput: string; +}; + +export default SearchForReportsParams; diff --git a/src/libs/API/parameters/SendPerformanceTimingParams.ts b/src/libs/API/parameters/SendPerformanceTimingParams.ts new file mode 100644 index 000000000000..aebdaa40c8bb --- /dev/null +++ b/src/libs/API/parameters/SendPerformanceTimingParams.ts @@ -0,0 +1,7 @@ +type SendPerformanceTimingParams = { + name: string; + value: number; + platform: string; +}; + +export default SendPerformanceTimingParams; diff --git a/src/libs/API/parameters/SignInWithShortLivedAuthTokenParams.ts b/src/libs/API/parameters/SignInWithShortLivedAuthTokenParams.ts new file mode 100644 index 000000000000..447c0ede0399 --- /dev/null +++ b/src/libs/API/parameters/SignInWithShortLivedAuthTokenParams.ts @@ -0,0 +1,7 @@ +type SignInWithShortLivedAuthTokenParams = { + authToken: string; + oldPartnerUserID: string; + skipReauthentication: boolean; +}; + +export default SignInWithShortLivedAuthTokenParams; diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index f895a4e13485..8c6733f1ca11 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -8,3 +8,19 @@ export type {default as OpenAppParams} from './OpenAppParams'; export type {default as OpenProfileParams} from './OpenProfileParams'; export type {default as ReconnectAppParams} from './ReconnectAppParams'; export type {default as UpdatePreferredLocaleParams} from './UpdatePreferredLocaleParams'; +export type {default as OpenReimbursementAccountPageParams} from './OpenReimbursementAccountPageParams'; +export type {default as OpenPublicProfilePageParams} from './OpenPublicProfilePageParams'; +export type {default as OpenPlaidBankLoginParams} from './OpenPlaidBankLoginParams'; +export type {default as OpenPlaidBankAccountSelectorParams} from './OpenPlaidBankAccountSelectorParams'; +export type {default as GetOlderActionsParams} from './GetOlderActionsParams'; +export type {default as GetNewerActionsParams} from './GetNewerActionsParams'; +export type {default as ExpandURLPreviewParams} from './ExpandURLPreviewParams'; +export type {default as GetReportPrivateNoteParams} from './GetReportPrivateNoteParams'; +export type {default as OpenRoomMembersPageParams} from './OpenRoomMembersPageParams'; +export type {default as SearchForReportsParams} from './SearchForReportsParams'; +export type {default as SendPerformanceTimingParams} from './SendPerformanceTimingParams'; +export type {default as GetRouteParams} from './GetRouteParams'; +export type {default as GetRouteForDraftParams} from './GetRouteForDraftParams'; +export type {default as GetStatementPDFParams} from './GetStatementPDFParams'; +export type {default as BeginSignInParams} from './BeginSignInParams'; +export type {default as SignInWithShortLivedAuthTokenParams} from './SignInWithShortLivedAuthTokenParams'; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index 3d511e9bbab7..11f2fc9e382a 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -1,15 +1,32 @@ import type {ValueOf} from 'type-fest'; import type CONST from '@src/CONST'; +import type {EmptyObject} from '@src/types/utils/EmptyObject'; import type { AuthenticatePusherParams, + BeginSignInParams, + ExpandURLPreviewParams, GetMissingOnyxMessagesParams, + GetNewerActionsParams, + GetOlderActionsParams, + GetReportPrivateNoteParams, + GetRouteForDraftParams, + GetRouteParams, + GetStatementPDFParams, HandleRestrictedEventParams, OpenAppParams, OpenOldDotLinkParams, + OpenPlaidBankAccountSelectorParams, + OpenPlaidBankLoginParams, OpenProfileParams, + OpenPublicProfilePageParams, + OpenReimbursementAccountPageParams, OpenReportParams, + OpenRoomMembersPageParams, ReconnectAppParams, RevealExpensifyCardDetailsParams, + SearchForReportsParams, + SendPerformanceTimingParams, + SignInWithShortLivedAuthTokenParams, UpdatePreferredLocaleParams, } from './parameters'; @@ -35,12 +52,58 @@ type WriteCommandParameters = { const READ_COMMANDS = { OPEN_APP: 'OpenApp', + OPEN_REIMBURSEMENT_ACCOUNT_PAGE: 'OpenReimbursementAccountPage', + OPEN_WORKSPACE_VIEW: 'OpenWorkspaceView', + GET_MAPBOX_ACCESS_TOKEN: 'GetMapboxAccessToken', + OPEN_PAYMENTS_PAGE: 'OpenPaymentsPage', + OPEN_PERSONAL_DETAILS_PAGE: 'OpenPersonalDetailsPage', + OPEN_PUBLIC_PROFILE_PAGE: 'OpenPublicProfilePage', + OPEN_PLAID_BANK_LOGIN: 'OpenPlaidBankLogin', + OPEN_PLAID_BANK_ACCOUNT_SELECTOR: 'OpenPlaidBankAccountSelector', + GET_OLDER_ACTIONS: 'GetOlderActions', + GET_NEWER_ACTIONS: 'GetNewerActions', + EXPAND_URL_PREVIEW: 'ExpandURLPreview', + GET_REPORT_PRIVATE_NOTE: 'GetReportPrivateNote', + OPEN_ROOM_MEMBERS_PAGE: 'OpenRoomMembersPage', + SEARCH_FOR_REPORTS: 'SearchForReports', + SEND_PERFORMANCE_TIMING: 'SendPerformanceTiming', + GET_ROUTE: 'GetRoute', + GET_ROUTE_FOR_DRAFT: 'GetRouteForDraft', + GET_STATEMENT_PDF: 'GetStatementPDF', + OPEN_ONFIDO_FLOW: 'OpenOnfidoFlow', + OPEN_INITIAL_SETTINGS_PAGE: 'OpenInitialSettingsPage', + OPEN_ENABLE_PAYMENTS_PAGE: 'OpenEnablePaymentsPage', + BEGIN_SIGNIN: 'BeginSignIn', + SIGN_IN_WITH_SHORT_LIVED_AUTH_TOKEN: 'SignInWithShortLivedAuthToken', } as const; type ReadCommand = ValueOf; type ReadCommandParameters = { [READ_COMMANDS.OPEN_APP]: OpenAppParams; + [READ_COMMANDS.OPEN_REIMBURSEMENT_ACCOUNT_PAGE]: OpenReimbursementAccountPageParams; + [READ_COMMANDS.OPEN_WORKSPACE_VIEW]: EmptyObject; + [READ_COMMANDS.GET_MAPBOX_ACCESS_TOKEN]: EmptyObject; + [READ_COMMANDS.OPEN_PAYMENTS_PAGE]: EmptyObject; + [READ_COMMANDS.OPEN_PERSONAL_DETAILS_PAGE]: EmptyObject; + [READ_COMMANDS.OPEN_PUBLIC_PROFILE_PAGE]: OpenPublicProfilePageParams; + [READ_COMMANDS.OPEN_PLAID_BANK_LOGIN]: OpenPlaidBankLoginParams; + [READ_COMMANDS.OPEN_PLAID_BANK_ACCOUNT_SELECTOR]: OpenPlaidBankAccountSelectorParams; + [READ_COMMANDS.GET_OLDER_ACTIONS]: GetOlderActionsParams; + [READ_COMMANDS.GET_NEWER_ACTIONS]: GetNewerActionsParams; + [READ_COMMANDS.EXPAND_URL_PREVIEW]: ExpandURLPreviewParams; + [READ_COMMANDS.GET_REPORT_PRIVATE_NOTE]: GetReportPrivateNoteParams; + [READ_COMMANDS.OPEN_ROOM_MEMBERS_PAGE]: OpenRoomMembersPageParams; + [READ_COMMANDS.SEARCH_FOR_REPORTS]: SearchForReportsParams; + [READ_COMMANDS.SEND_PERFORMANCE_TIMING]: SendPerformanceTimingParams; + [READ_COMMANDS.GET_ROUTE]: GetRouteParams; + [READ_COMMANDS.GET_ROUTE_FOR_DRAFT]: GetRouteForDraftParams; + [READ_COMMANDS.GET_STATEMENT_PDF]: GetStatementPDFParams; + [READ_COMMANDS.OPEN_ONFIDO_FLOW]: EmptyObject; + [READ_COMMANDS.OPEN_INITIAL_SETTINGS_PAGE]: EmptyObject; + [READ_COMMANDS.OPEN_ENABLE_PAYMENTS_PAGE]: EmptyObject; + [READ_COMMANDS.BEGIN_SIGNIN]: BeginSignInParams; + [READ_COMMANDS.SIGN_IN_WITH_SHORT_LIVED_AUTH_TOKEN]: SignInWithShortLivedAuthTokenParams; }; const SIDE_EFFECT_REQUEST_COMMANDS = { diff --git a/src/libs/actions/App.ts b/src/libs/actions/App.ts index 595a1611fe1f..930c31fde287 100644 --- a/src/libs/actions/App.ts +++ b/src/libs/actions/App.ts @@ -6,9 +6,15 @@ import type {OnyxCollection, OnyxEntry, OnyxUpdate} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import * as API from '@libs/API'; -import type {GetMissingOnyxMessagesParams, OpenOldDotLinkParams} from '@libs/API/parameters'; -import type {HandleRestrictedEventParams, OpenProfileParams, ReconnectAppParams, UpdatePreferredLocaleParams} from '@libs/API/parameters/HandleRestrictedEventParams'; -import type {OpenAppParams} from '@libs/API/parameters/OpenAppParams'; +import type { + GetMissingOnyxMessagesParams, + HandleRestrictedEventParams, + OpenAppParams, + OpenOldDotLinkParams, + OpenProfileParams, + ReconnectAppParams, + UpdatePreferredLocaleParams, +} from '@libs/API/parameters'; import {READ_COMMANDS, SIDE_EFFECT_REQUEST_COMMANDS, WRITE_COMMANDS} from '@libs/API/types'; import * as Browser from '@libs/Browser'; import DateUtils from '@libs/DateUtils'; diff --git a/src/libs/actions/BankAccounts.ts b/src/libs/actions/BankAccounts.ts index f7b7ec89c670..6a4a0dcdfe2a 100644 --- a/src/libs/actions/BankAccounts.ts +++ b/src/libs/actions/BankAccounts.ts @@ -1,5 +1,7 @@ import Onyx from 'react-native-onyx'; import * as API from '@libs/API'; +import type {OpenReimbursementAccountPageParams} from '@libs/API/parameters'; +import {READ_COMMANDS} from '@libs/API/types'; import * as ErrorUtils from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; import * as PlaidDataProps from '@pages/ReimbursementAccount/plaidDataPropTypes'; @@ -331,19 +333,13 @@ function openReimbursementAccountPage(stepToOpen: ReimbursementAccountStep, subS ], }; - type OpenReimbursementAccountPageParams = { - stepToOpen: ReimbursementAccountStep; - subStep: ReimbursementAccountSubStep; - localCurrentStep: ReimbursementAccountStep; - }; - const parameters: OpenReimbursementAccountPageParams = { stepToOpen, subStep, localCurrentStep, }; - return API.read('OpenReimbursementAccountPage', parameters, onyxData); + return API.read(READ_COMMANDS.OPEN_REIMBURSEMENT_ACCOUNT_PAGE, parameters, onyxData); } /** @@ -405,7 +401,7 @@ function verifyIdentityForBankAccount(bankAccountID: number, onfidoData: OnfidoD function openWorkspaceView() { API.read( - 'OpenWorkspaceView', + READ_COMMANDS.OPEN_WORKSPACE_VIEW, {}, { optimisticData: [ diff --git a/src/libs/actions/MapboxToken.ts b/src/libs/actions/MapboxToken.ts index 54f99b58fbeb..3b98f79698ba 100644 --- a/src/libs/actions/MapboxToken.ts +++ b/src/libs/actions/MapboxToken.ts @@ -4,6 +4,7 @@ import {AppState} from 'react-native'; import Onyx from 'react-native-onyx'; import * as ActiveClientManager from '@libs/ActiveClientManager'; import * as API from '@libs/API'; +import {READ_COMMANDS} from '@libs/API/types'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {MapboxAccessToken, Network} from '@src/types/onyx'; @@ -38,7 +39,7 @@ const setExpirationTimer = () => { return; } console.debug(`[MapboxToken] Fetching a new token after waiting ${REFRESH_INTERVAL / 1000 / 60} minutes`); - API.read('GetMapboxAccessToken', {}, {}); + API.read(READ_COMMANDS.GET_MAPBOX_ACCESS_TOKEN, {}, {}); }, REFRESH_INTERVAL); }; @@ -51,7 +52,7 @@ const clearToken = () => { }; const fetchToken = () => { - API.read('GetMapboxAccessToken', {}, {}); + API.read(READ_COMMANDS.GET_MAPBOX_ACCESS_TOKEN, {}, {}); isCurrentlyFetchingToken = true; }; diff --git a/src/libs/actions/PaymentMethods.ts b/src/libs/actions/PaymentMethods.ts index a7ae54f46416..c3a19073eb63 100644 --- a/src/libs/actions/PaymentMethods.ts +++ b/src/libs/actions/PaymentMethods.ts @@ -4,6 +4,7 @@ import Onyx from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx/lib/types'; import type {ValueOf} from 'type-fest'; import * as API from '@libs/API'; +import {READ_COMMANDS} from '@libs/API/types'; import * as CardUtils from '@libs/CardUtils'; import Navigation from '@libs/Navigation/Navigation'; import CONST from '@src/CONST'; @@ -60,7 +61,7 @@ function openWalletPage() { ]; return API.read( - 'OpenPaymentsPage', + READ_COMMANDS.OPEN_PAYMENTS_PAGE, {}, { optimisticData, diff --git a/src/libs/actions/PersonalDetails.ts b/src/libs/actions/PersonalDetails.ts index 508cca34fb88..730dd682ebf5 100644 --- a/src/libs/actions/PersonalDetails.ts +++ b/src/libs/actions/PersonalDetails.ts @@ -2,6 +2,8 @@ import Str from 'expensify-common/lib/str'; import type {OnyxEntry, OnyxUpdate} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import * as API from '@libs/API'; +import type {OpenPublicProfilePageParams} from '@libs/API/parameters'; +import {READ_COMMANDS} from '@libs/API/types'; import type {CustomRNImageManipulatorResult} from '@libs/cropOrRotateImage/types'; import DateUtils from '@libs/DateUtils'; import * as LocalePhoneNumber from '@libs/LocalePhoneNumber'; @@ -365,11 +367,7 @@ function openPersonalDetailsPage() { }, ]; - type OpenPersonalDetailsPageParams = Record; - - const parameters: OpenPersonalDetailsPageParams = {}; - - API.read('OpenPersonalDetailsPage', parameters, {optimisticData, successData, failureData}); + API.read(READ_COMMANDS.OPEN_PERSONAL_DETAILS_PAGE, {}, {optimisticData, successData, failureData}); } /** @@ -414,13 +412,9 @@ function openPublicProfilePage(accountID: number) { }, ]; - type OpenPublicProfilePageParams = { - accountID: number; - }; - const parameters: OpenPublicProfilePageParams = {accountID}; - API.read('OpenPublicProfilePage', parameters, {optimisticData, successData, failureData}); + API.read(READ_COMMANDS.OPEN_PUBLIC_PROFILE_PAGE, parameters, {optimisticData, successData, failureData}); } /** diff --git a/src/libs/actions/Plaid.ts b/src/libs/actions/Plaid.ts index ab828eefeece..28b06d9e42a5 100644 --- a/src/libs/actions/Plaid.ts +++ b/src/libs/actions/Plaid.ts @@ -1,5 +1,7 @@ import Onyx from 'react-native-onyx'; import * as API from '@libs/API'; +import type {OpenPlaidBankAccountSelectorParams, OpenPlaidBankLoginParams} from '@libs/API/parameters'; +import {READ_COMMANDS} from '@libs/API/types'; import getPlaidLinkTokenParameters from '@libs/getPlaidLinkTokenParameters'; import * as PlaidDataProps from '@pages/ReimbursementAccount/plaidDataPropTypes'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -10,11 +12,13 @@ import ONYXKEYS from '@src/ONYXKEYS'; function openPlaidBankLogin(allowDebit: boolean, bankAccountID: number) { // redirect_uri needs to be in kebab case convention because that's how it's passed to the backend const {redirectURI} = getPlaidLinkTokenParameters(); - const params = { + + const params: OpenPlaidBankLoginParams = { redirectURI, allowDebit, bankAccountID, }; + const optimisticData = [ { onyxMethod: Onyx.METHOD.SET, @@ -35,51 +39,49 @@ function openPlaidBankLogin(allowDebit: boolean, bankAccountID: number) { }, ]; - API.read('OpenPlaidBankLogin', params, {optimisticData}); + API.read(READ_COMMANDS.OPEN_PLAID_BANK_LOGIN, params, {optimisticData}); } function openPlaidBankAccountSelector(publicToken: string, bankName: string, allowDebit: boolean, bankAccountID: number) { - API.read( - 'OpenPlaidBankAccountSelector', - { - publicToken, - allowDebit, - bank: bankName, - bankAccountID, - }, - { - optimisticData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.PLAID_DATA, - value: { - isLoading: true, - errors: null, - bankName, - }, + const parameters: OpenPlaidBankAccountSelectorParams = { + publicToken, + allowDebit, + bank: bankName, + bankAccountID, + }; + + API.read(READ_COMMANDS.OPEN_PLAID_BANK_ACCOUNT_SELECTOR, parameters, { + optimisticData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.PLAID_DATA, + value: { + isLoading: true, + errors: null, + bankName, }, - ], - successData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.PLAID_DATA, - value: { - isLoading: false, - errors: null, - }, + }, + ], + successData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.PLAID_DATA, + value: { + isLoading: false, + errors: null, }, - ], - failureData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.PLAID_DATA, - value: { - isLoading: false, - }, + }, + ], + failureData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.PLAID_DATA, + value: { + isLoading: false, }, - ], - }, - ); + }, + ], + }); } export {openPlaidBankAccountSelector, openPlaidBankLogin}; diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 5535b04064c6..20e523d3e5e0 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -10,8 +10,16 @@ import type {PartialDeep, ValueOf} from 'type-fest'; import type {Emoji} from '@assets/emojis/types'; import * as ActiveClientManager from '@libs/ActiveClientManager'; import * as API from '@libs/API'; -import {SIDE_EFFECT_REQUEST_COMMANDS} from '@libs/API/types'; -import type {OpenReportParams} from '@libs/API/parameters'; +import type { + ExpandURLPreviewParams, + GetNewerActionsParams, + GetOlderActionsParams, + GetReportPrivateNoteParams, + OpenReportParams, + OpenRoomMembersPageParams, + SearchForReportsParams, +} from '@libs/API/parameters'; +import {READ_COMMANDS, SIDE_EFFECT_REQUEST_COMMANDS} from '@libs/API/types'; import * as CollectionUtils from '@libs/CollectionUtils'; import DateUtils from '@libs/DateUtils'; import * as EmojiUtils from '@libs/EmojiUtils'; @@ -828,17 +836,12 @@ function getOlderActions(reportID: string, reportActionID: string) { }, ]; - type GetOlderActionsParameters = { - reportID: string; - reportActionID: string; - }; - - const parameters: GetOlderActionsParameters = { + const parameters: GetOlderActionsParams = { reportID, reportActionID, }; - API.read('GetOlderActions', parameters, {optimisticData, successData, failureData}); + API.read(READ_COMMANDS.GET_OLDER_ACTIONS, parameters, {optimisticData, successData, failureData}); } /** @@ -876,34 +879,24 @@ function getNewerActions(reportID: string, reportActionID: string) { }, ]; - type GetNewerActionsParameters = { - reportID: string; - reportActionID: string; - }; - - const parameters: GetNewerActionsParameters = { + const parameters: GetNewerActionsParams = { reportID, reportActionID, }; - API.read('GetNewerActions', parameters, {optimisticData, successData, failureData}); + API.read(READ_COMMANDS.GET_NEWER_ACTIONS, parameters, {optimisticData, successData, failureData}); } /** * Gets metadata info about links in the provided report action */ function expandURLPreview(reportID: string, reportActionID: string) { - type ExpandURLPreviewParameters = { - reportID: string; - reportActionID: string; - }; - - const parameters: ExpandURLPreviewParameters = { + const parameters: ExpandURLPreviewParams = { reportID, reportActionID, }; - API.read('ExpandURLPreview', parameters); + API.read(READ_COMMANDS.EXPAND_URL_PREVIEW, parameters); } /** Marks the new report actions as read */ @@ -2470,24 +2463,16 @@ function getReportPrivateNote(reportID: string) { }, ]; - type GetReportPrivateNoteParameters = { - reportID: string; - }; - - const parameters: GetReportPrivateNoteParameters = {reportID}; + const parameters: GetReportPrivateNoteParams = {reportID}; - API.read('GetReportPrivateNote', parameters, {optimisticData, successData, failureData}); + API.read(READ_COMMANDS.GET_REPORT_PRIVATE_NOTE, parameters, {optimisticData, successData, failureData}); } /** Loads necessary data for rendering the RoomMembersPage */ function openRoomMembersPage(reportID: string) { - type OpenRoomMembersPageParameters = { - reportID: string; - }; - - const parameters: OpenRoomMembersPageParameters = {reportID}; + const parameters: OpenRoomMembersPageParams = {reportID}; - API.read('OpenRoomMembersPage', parameters); + API.read(READ_COMMANDS.OPEN_ROOM_MEMBERS_PAGE, parameters); } /** @@ -2540,13 +2525,9 @@ function searchForReports(searchInput: string) { }, ]; - type SearchForReportsParameters = { - searchInput: string; - }; - - const parameters: SearchForReportsParameters = {searchInput}; + const parameters: SearchForReportsParams = {searchInput}; - API.read('SearchForReports', parameters, {successData, failureData}); + API.read(READ_COMMANDS.SEARCH_FOR_REPORTS, parameters, {successData, failureData}); } function searchInServer(searchInput: string) { diff --git a/src/libs/actions/Session/index.ts b/src/libs/actions/Session/index.ts index eff52dbfa4fe..7685a242f42a 100644 --- a/src/libs/actions/Session/index.ts +++ b/src/libs/actions/Session/index.ts @@ -7,8 +7,8 @@ import Onyx from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import * as PersistedRequests from '@libs/actions/PersistedRequests'; import * as API from '@libs/API'; -import type {AuthenticatePusherParams} from '@libs/API/parameters/sideEffectRequest'; -import {SIDE_EFFECT_REQUEST_COMMANDS} from '@libs/API/types'; +import type {AuthenticatePusherParams, BeginSignInParams, SignInWithShortLivedAuthTokenParams} from '@libs/API/parameters'; +import {READ_COMMANDS, SIDE_EFFECT_REQUEST_COMMANDS} from '@libs/API/types'; import * as Authentication from '@libs/Authentication'; import * as ErrorUtils from '@libs/ErrorUtils'; import HttpUtils from '@libs/HttpUtils'; @@ -281,13 +281,9 @@ function signInAttemptState(): OnyxData { function beginSignIn(email: string) { const {optimisticData, successData, failureData} = signInAttemptState(); - type BeginSignInParams = { - email: string; - }; - const params: BeginSignInParams = {email}; - API.read('BeginSignIn', params, {optimisticData, successData, failureData}); + API.read(READ_COMMANDS.BEGIN_SIGNIN, params, {optimisticData, successData, failureData}); } /** @@ -375,15 +371,9 @@ function signInWithShortLivedAuthToken(email: string, authToken: string) { // scene 2: the user is transitioning to desktop app from a different account on web app. const oldPartnerUserID = credentials.login === email && credentials.autoGeneratedLogin ? credentials.autoGeneratedLogin : ''; - type SignInWithShortLivedAuthTokenParams = { - authToken: string; - oldPartnerUserID: string; - skipReauthentication: boolean; - }; - const params: SignInWithShortLivedAuthTokenParams = {authToken, oldPartnerUserID, skipReauthentication: true}; - API.read('SignInWithShortLivedAuthToken', params, {optimisticData, successData, failureData}); + API.read(READ_COMMANDS.SIGN_IN_WITH_SHORT_LIVED_AUTH_TOKEN, params, {optimisticData, successData, failureData}); } /** diff --git a/src/libs/actions/Timing.ts b/src/libs/actions/Timing.ts index 9e40f088f1c2..28ffdd92ffba 100644 --- a/src/libs/actions/Timing.ts +++ b/src/libs/actions/Timing.ts @@ -1,4 +1,6 @@ import * as API from '@libs/API'; +import type {SendPerformanceTimingParams} from '@libs/API/parameters'; +import {READ_COMMANDS} from '@libs/API/types'; import * as Environment from '@libs/Environment/Environment'; import Firebase from '@libs/Firebase'; import getPlatform from '@libs/getPlatform'; @@ -62,15 +64,13 @@ function end(eventName: string, secondaryName = '', maxExecutionTime = 0) { Log.warn(`${eventName} exceeded max execution time of ${maxExecutionTime}.`, {eventTime, eventName}); } - API.read( - 'SendPerformanceTiming', - { - name: grafanaEventName, - value: eventTime, - platform: `${getPlatform()}`, - }, - {}, - ); + const parameters: SendPerformanceTimingParams = { + name: grafanaEventName, + value: eventTime, + platform: `${getPlatform()}`, + }; + + API.read(READ_COMMANDS.SEND_PERFORMANCE_TIMING, parameters, {}); }); } diff --git a/src/libs/actions/Transaction.ts b/src/libs/actions/Transaction.ts index 430de0557674..8743da7abd06 100644 --- a/src/libs/actions/Transaction.ts +++ b/src/libs/actions/Transaction.ts @@ -3,6 +3,8 @@ import lodashClone from 'lodash/clone'; import lodashHas from 'lodash/has'; import Onyx from 'react-native-onyx'; import * as API from '@libs/API'; +import type {GetRouteForDraftParams, GetRouteParams} from '@libs/API/parameters'; +import {READ_COMMANDS} from '@libs/API/types'; import * as CollectionUtils from '@libs/CollectionUtils'; import * as TransactionUtils from '@libs/TransactionUtils'; import CONST from '@src/CONST'; @@ -209,14 +211,12 @@ function getOnyxDataForRouteRequest(transactionID: string, isDraft = false): Ony * Used so we can generate a map view of the provided waypoints */ function getRoute(transactionID: string, waypoints: WaypointCollection) { - API.read( - 'GetRoute', - { - transactionID, - waypoints: JSON.stringify(waypoints), - }, - getOnyxDataForRouteRequest(transactionID), - ); + const parameters: GetRouteParams = { + transactionID, + waypoints: JSON.stringify(waypoints), + }; + + API.read(READ_COMMANDS.GET_ROUTE, parameters, getOnyxDataForRouteRequest(transactionID)); } /** @@ -224,14 +224,12 @@ function getRoute(transactionID: string, waypoints: WaypointCollection) { * Used so we can generate a map view of the provided waypoints */ function getRouteForDraft(transactionID: string, waypoints: WaypointCollection) { - API.read( - 'GetRouteForDraft', - { - transactionID, - waypoints: JSON.stringify(waypoints), - }, - getOnyxDataForRouteRequest(transactionID, true), - ); + const parameters: GetRouteForDraftParams = { + transactionID, + waypoints: JSON.stringify(waypoints), + }; + + API.read(READ_COMMANDS.GET_ROUTE_FOR_DRAFT, parameters, getOnyxDataForRouteRequest(transactionID, true)); } /** diff --git a/src/libs/actions/User.ts b/src/libs/actions/User.ts index 8e3bd5f2c017..f8d0a407703f 100644 --- a/src/libs/actions/User.ts +++ b/src/libs/actions/User.ts @@ -4,6 +4,8 @@ import Onyx from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx/lib/types'; import type {ValueOf} from 'type-fest'; import * as API from '@libs/API'; +import type {GetStatementPDFParams} from '@libs/API/parameters'; +import {READ_COMMANDS} from '@libs/API/types'; import * as ErrorUtils from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; import * as SequentialQueue from '@libs/Network/SequentialQueue'; @@ -673,11 +675,9 @@ function generateStatementPDF(period: string) { }, ]; - type GetStatementPDFParams = {period: string}; - const parameters: GetStatementPDFParams = {period}; - API.read('GetStatementPDF', parameters, { + API.read(READ_COMMANDS.GET_STATEMENT_PDF, parameters, { optimisticData, successData, failureData, diff --git a/src/libs/actions/Wallet.ts b/src/libs/actions/Wallet.ts index bc2fb518d8e6..b61c1816eb7e 100644 --- a/src/libs/actions/Wallet.ts +++ b/src/libs/actions/Wallet.ts @@ -2,6 +2,7 @@ import type {OnyxUpdate} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import * as API from '@libs/API'; +import {READ_COMMANDS} from '@libs/API/types'; import type {PrivatePersonalDetails} from '@libs/GetPhysicalCardUtils'; import type CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -62,14 +63,7 @@ function openOnfidoFlow() { }, ]; - API.read( - 'OpenOnfidoFlow', - {}, - { - optimisticData, - finallyData, - }, - ); + API.read(READ_COMMANDS.OPEN_ONFIDO_FLOW, {}, {optimisticData, finallyData}); } function setAdditionalDetailsQuestions(questions: WalletAdditionalQuestionDetails[], idNumber: string) { @@ -232,14 +226,14 @@ function acceptWalletTerms(parameters: WalletTerms) { * Fetches data when the user opens the InitialSettingsPage */ function openInitialSettingsPage() { - API.read('OpenInitialSettingsPage', {}); + API.read(READ_COMMANDS.OPEN_INITIAL_SETTINGS_PAGE, {}); } /** * Fetches data when the user opens the EnablePaymentsPage */ function openEnablePaymentsPage() { - API.read('OpenEnablePaymentsPage', {}); + API.read(READ_COMMANDS.OPEN_ENABLE_PAYMENTS_PAGE, {}); } function updateCurrentStep(currentStep: ValueOf) { From d6bd5f2cba71b8c02318afee657d89d38d9da11b Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Mon, 15 Jan 2024 23:14:04 +0100 Subject: [PATCH 238/580] Fix lint --- src/libs/API/parameters/GetMissingOnyxMessagesParams.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/libs/API/parameters/GetMissingOnyxMessagesParams.ts b/src/libs/API/parameters/GetMissingOnyxMessagesParams.ts index 7f98d8c0116c..38df5a37ba97 100644 --- a/src/libs/API/parameters/GetMissingOnyxMessagesParams.ts +++ b/src/libs/API/parameters/GetMissingOnyxMessagesParams.ts @@ -1,6 +1,6 @@ type GetMissingOnyxMessagesParams = { - updateIDFrom: number; - updateIDTo: number | string; + updateIDFrom: number; + updateIDTo: number | string; }; -export default GetMissingOnyxMessagesParams; \ No newline at end of file +export default GetMissingOnyxMessagesParams; From aff34cc1dc4243cfe992fb30031290385d20e54f Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Tue, 16 Jan 2024 08:22:27 +0100 Subject: [PATCH 239/580] refactor composeToString method --- src/components/MagicCodeInput.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/MagicCodeInput.tsx b/src/components/MagicCodeInput.tsx index f8b7106da3a6..1b5bb8642d32 100644 --- a/src/components/MagicCodeInput.tsx +++ b/src/components/MagicCodeInput.tsx @@ -84,7 +84,7 @@ const decomposeString = (value: string, length: number): string[] => { * Converts an array of strings into a single string. If there are undefined or * empty values, it will replace them with a space. */ -const composeToString = (value: string[]): string => value.map((v) => (v === undefined || v === '' ? CONST.MAGIC_CODE_EMPTY_CHAR : v)).join(''); +const composeToString = (value: string[]): string => value.map((v) => (!v ? CONST.MAGIC_CODE_EMPTY_CHAR : v)).join(''); const getInputPlaceholderSlots = (length: number): number[] => Array.from(Array(length).keys()); From 3b5f6e58b37d67ad5a43bed3d14a36a5c5b837a0 Mon Sep 17 00:00:00 2001 From: Roji Philip Date: Tue, 16 Jan 2024 16:24:10 +0530 Subject: [PATCH 240/580] use announce and admin room report id from policy --- src/pages/workspace/WorkspaceInitialPage.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/pages/workspace/WorkspaceInitialPage.js b/src/pages/workspace/WorkspaceInitialPage.js index 80813c847239..73723b61fd34 100644 --- a/src/pages/workspace/WorkspaceInitialPage.js +++ b/src/pages/workspace/WorkspaceInitialPage.js @@ -206,22 +206,22 @@ function WorkspaceInitialPage(props) { onSelected: () => setIsDeleteModalOpen(true), }, ]; - if (adminsRoom) { + if (adminsRoom || policy.chatReportIDAdmins) { items.push({ icon: Expensicons.Hashtag, text: translate('workspace.common.goToRoom', {roomName: CONST.REPORT.WORKSPACE_CHAT_ROOMS.ADMINS}), - onSelected: () => Navigation.dismissModal(adminsRoom.reportID), + onSelected: () => Navigation.dismissModal(adminsRoom ? adminsRoom.reportID : policy.chatReportIDAdmins.toString()), }); } - if (announceRoom) { + if (announceRoom || policy.chatReportIDAnnounce) { items.push({ icon: Expensicons.Hashtag, text: translate('workspace.common.goToRoom', {roomName: CONST.REPORT.WORKSPACE_CHAT_ROOMS.ANNOUNCE}), - onSelected: () => Navigation.dismissModal(announceRoom.reportID), + onSelected: () => Navigation.dismissModal(announceRoom ? announceRoom.reportID : policy.chatReportIDAnnounce.toString()), }); } return items; - }, [adminsRoom, announceRoom, translate]); + }, [adminsRoom, announceRoom, translate, policy]); const prevPolicy = usePrevious(policy); From feb1d36561848d5cc4cee0d792470f5e586a5506 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Tue, 16 Jan 2024 13:08:15 +0100 Subject: [PATCH 241/580] Add remaining write commands --- src/libs/API/types.ts | 83 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index 11f2fc9e382a..1f5c58463e8a 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -38,6 +38,89 @@ const WRITE_COMMANDS = { OPEN_PROFILE: 'OpenProfile', HANDLE_RESTRICTED_EVENT: 'HandleRestrictedEvent', OPEN_REPORT: 'OpenReport', + DELETE_PAYMENT_BANK_ACCOUNT: 'DeletePaymentBankAccount', + UPDATE_PERSONAL_INFORMATION_FOR_BANK_ACCOUNT: 'UpdatePersonalInformationForBankAccount', + VALIDATE_BANK_ACCOUNT_WITH_TRANSACTIONS: 'ValidateBankAccountWithTransactions', + UPDATE_COMPANY_INFORMATION_FOR_BANK_ACCOUNT: 'UpdateCompanyInformationForBankAccount', + UPDATE_BENEFICIAL_OWNERS_FOR_BANK_ACCOUNT: 'UpdateBeneficialOwnersForBankAccount', + CONNECT_BANK_ACCOUNT_MANUALLY: 'ConnectBankAccountManually', + VERIFY_IDENTITY_FOR_BANK_ACCOUNT: 'VerifyIdentityForBankAccount', + BANK_ACCOUNT_HANDLE_PLAID_ERROR: 'BankAccount_HandlePlaidError', + REPORT_VIRTUAL_EXPENSIFY_CARD_FRAUD: 'ReportVirtualExpensifyCardFraud', + REQUEST_REPLACEMENT_EXPENSIFY_CARD: 'RequestReplacementExpensifyCard', + ACTIVATE_PHYSICAL_EXPENSIFY_CARD: 'ActivatePhysicalExpensifyCard', + CHRONOS_REMOVE_OOO_EVENT: 'Chronos_RemoveOOOEvent', + MAKE_DEFAULT_PAYMENT_METHOD: 'MakeDefaultPaymentMethod', + ADD_PAYMENT_CARD: 'AddPaymentCard', + TRANSFER_WALLET_BALANCE: 'TransferWalletBalance', + DELETE_PAYMENT_CARD: 'DeletePaymentCard', + UPDATE_PRONOUNS: 'UpdatePronouns', + UPDATE_DISPLAY_NAME: 'UpdateDisplayName', + UPDATE_LEGAL_NAME: 'UpdateLegalName', + UPDATE_DATE_OF_BIRTH: 'UpdateDateOfBirth', + UPDATE_HOME_ADDRESS: 'UpdateHomeAddress', + UPDATE_AUTOMATIC_TIMEZONE: 'UpdateAutomaticTimezone', + UPDATE_SELECTED_TIMEZONE: 'UpdateSelectedTimezone', + UPDATE_USER_AVATAR: 'UpdateUserAvatar', + DELETE_USER_AVATAR: 'DeleteUserAvatar', + REFER_TEACHERS_UNITE_VOLUNTEER: 'ReferTeachersUniteVolunteer', + ADD_SCHOOL_PRINCIPAL: 'AddSchoolPrincipal', + CLOSE_ACCOUNT: 'CloseAccount', + REQUEST_CONTACT_METHOD_VALIDATE_CODE: 'RequestContactMethodValidateCode', + UPDATE_NEWSLETTER_SUBSCRIPTION: 'UpdateNewsletterSubscription', + DELETE_CONTACT_METHOD: 'DeleteContactMethod', + ADD_NEW_CONTACT_METHOD: 'AddNewContactMethod', + VALIDATE_LOGIN: 'ValidateLogin', + VALIDATE_SECONDARY_LOGIN: 'ValidateSecondaryLogin', + UPDATE_PREFERRED_EMOJI_SKIN_TONE: 'UpdatePreferredEmojiSkinTone', + UPDATE_FREQUENTLY_USED_EMOJIS: 'UpdateFrequentlyUsedEmojis', + UPDATE_CHAT_PRIORITY_MODE: 'UpdateChatPriorityMode', + SET_CONTACT_METHOD_AS_DEFAULT: 'SetContactMethodAsDefault', + UPDATE_THEME: 'UpdateTheme', + UPDATE_STATUS: 'UpdateStatus', + CLEAR_STATUS: 'ClearStatus', + UPDATE_PERSONAL_DETAILS_FOR_WALLET: 'UpdatePersonalDetailsForWallet', + VERIFY_IDENTITY: 'VerifyIdentity', + ACCEPT_WALLET_TERMS: 'AcceptWalletTerms', + ANSWER_QUESTIONS_FOR_WALLET: 'AnswerQuestionsForWallet', + REQUEST_PHYSICAL_EXPENSIFY_CARD: 'RequestPhysicalExpensifyCard', + LOG_OUT: 'LogOut', + REQUEST_ACCOUNT_VALIDATION_LINK: 'RequestAccountValidationLink', + REQUEST_NEW_VALIDATE_CODE: 'RequestNewValidateCode', + SIGN_IN_WITH_APPLE: 'SignInWithApple', + SIGN_IN_WITH_GOOGLE: 'SignInWithGoogle', + SIGN_IN_USER: 'SigninUser', + SIGN_IN_USER_WITH_LINK: 'SigninUserWithLink', + REQUEST_UNLINK_VALIDATION_LINK: 'RequestUnlinkValidationLink', + UNLINK_LOGIN: 'UnlinkLogin', + ENABLE_TWO_FACTOR_AUTH: 'EnableTwoFactorAuth', + DISABLE_TWO_FACTOR_AUTH: 'DisableTwoFactorAuth', + TWO_FACTOR_AUTH_VALIDATE: 'TwoFactorAuth_Validate', + ADD_COMMENT: 'AddComment', + ADD_ATTACHMENT: 'AddAttachment', + CONNECT_BANK_ACCOUNT_WITH_PLAID: 'ConnectBankAccountWithPlaid', + ADD_PERSONAL_BANK_ACCOUNT: 'AddPersonalBankAccount', + OPT_IN_TO_PUSH_NOTIFICATIONS: 'OptInToPushNotifications', + OPT_OUT_OF_PUSH_NOTIFICATIONS: 'OptOutOfPushNotifications', + RECONNECT_TO_REPORT: 'ReconnectToReport', + READ_NEWEST_ACTION: 'ReadNewestAction', + MARK_AS_UNREAD: 'MarkAsUnread', + TOGGLE_PINNED_CHAT: 'TogglePinnedChat', + DELETE_COMMENT: 'DeleteComment', + UPDATE_COMMENT: 'UpdateComment', + UPDATE_REPORT_NOTIFICATION_PREFERENCE: 'UpdateReportNotificationPreference', + UPDATE_WELCOME_MESSAGE: 'UpdateWelcomeMessage', + UPDATE_REPORT_WRITE_CAPABILITY: 'UpdateReportWriteCapability', + ADD_WORKSPACE_ROOM: 'AddWorkspaceRoom', + UPDATE_POLICY_ROOM_NAME: 'UpdatePolicyRoomName', + ADD_EMOJI_REACTION: 'AddEmojiReaction', + REMOVE_EMOJI_REACTION: 'RemoveEmojiReaction', + LEAVE_ROOM: 'LeaveRoom', + INVITE_TO_ROOM: 'InviteToRoom', + REMOVE_FROM_ROOM: 'RemoveFromRoom', + FLAG_COMMENT: 'FlagComment', + UPDATE_REPORT_PRIVATE_NOTE: 'UpdateReportPrivateNote', + RESOLVE_ACTIONABLE_MENTION_WHISPER: 'ResolveActionableMentionWhisper', } as const; type WriteCommand = ValueOf; From 449e6915ed77bd79f240e088c2739217eaf4f8ea Mon Sep 17 00:00:00 2001 From: VickyStash Date: Tue, 16 Jan 2024 13:18:54 +0100 Subject: [PATCH 242/580] TS fixes after merging main --- .../ReportActionItem/ReportPreview.tsx | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/components/ReportActionItem/ReportPreview.tsx b/src/components/ReportActionItem/ReportPreview.tsx index 2c3cc4c13204..1378e3b169f0 100644 --- a/src/components/ReportActionItem/ReportPreview.tsx +++ b/src/components/ReportActionItem/ReportPreview.tsx @@ -2,7 +2,7 @@ import React, {useMemo} from 'react'; import {View} from 'react-native'; import type {StyleProp, ViewStyle} from 'react-native'; import {withOnyx} from 'react-native-onyx'; -import type {OnyxEntry} from 'react-native-onyx/lib/types'; +import type {OnyxCollection, OnyxEntry} from 'react-native-onyx/lib/types'; import Button from '@components/Button'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; @@ -27,7 +27,7 @@ import * as IOU from '@userActions/IOU'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import type {Policy, Report, ReportAction, Session} from '@src/types/onyx'; +import type {Policy, Report, ReportAction, Session, Transaction} from '@src/types/onyx'; import type {PaymentMethodType} from '@src/types/onyx/OriginalMessage'; import ReportActionItemImages from './ReportActionItemImages'; @@ -45,6 +45,9 @@ type ReportPreviewOnyxProps = { /** Session info for the currently logged in user. */ session: OnyxEntry; + + /** All the transactions, used to update ReportPreview label and status */ + transactions: OnyxCollection; }; type ReportPreviewProps = ReportPreviewOnyxProps & { @@ -87,6 +90,7 @@ function ReportPreview({ action, containerStyles, contextMenuAnchor, + transactions, isHovered = false, isWhisper = false, checkIfContextMenuActive = () => {}, @@ -97,14 +101,14 @@ function ReportPreview({ const {hasMissingSmartscanFields, areAllRequestsBeingSmartScanned, hasOnlyDistanceRequests, hasNonReimbursableTransactions} = useMemo( () => ({ - hasMissingSmartscanFields: ReportUtils.hasMissingSmartscanFields(props.iouReportID), - areAllRequestsBeingSmartScanned: ReportUtils.areAllRequestsBeingSmartScanned(props.iouReportID, props.action), - hasOnlyDistanceRequests: ReportUtils.hasOnlyDistanceRequestTransactions(props.iouReportID), - hasNonReimbursableTransactions: ReportUtils.hasNonReimbursableTransactions(props.iouReportID), + hasMissingSmartscanFields: ReportUtils.hasMissingSmartscanFields(iouReportID), + areAllRequestsBeingSmartScanned: ReportUtils.areAllRequestsBeingSmartScanned(iouReportID, action), + hasOnlyDistanceRequests: ReportUtils.hasOnlyDistanceRequestTransactions(iouReportID), + hasNonReimbursableTransactions: ReportUtils.hasNonReimbursableTransactions(iouReportID), }), // When transactions get updated these status may have changed, so that is a case where we also want to run this. // eslint-disable-next-line react-hooks/exhaustive-deps - [props.transactions, props.iouReportID, props.action], + [transactions, iouReportID, action], ); const managerID = iouReport?.managerID ?? 0; From 905ba178ec28a20fc29b80569a7f2d10a8759a55 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Tue, 16 Jan 2024 13:43:11 +0100 Subject: [PATCH 243/580] improve and clean up PR --- .../Pager/AttachmentCarouselPagerContext.ts | 5 +-- .../AttachmentCarousel/Pager/index.tsx | 4 +-- .../BaseAttachmentViewPdf.js | 2 +- src/components/Lightbox.tsx | 2 +- .../MultiGestureCanvas/constants.ts | 8 ++--- .../MultiGestureCanvas/getCanvasFitScale.ts | 15 --------- src/components/MultiGestureCanvas/index.tsx | 22 ++++++------- src/components/MultiGestureCanvas/types.ts | 15 +++++---- .../MultiGestureCanvas/usePanGesture.ts | 24 +++++--------- .../MultiGestureCanvas/usePinchGesture.ts | 30 ++++++----------- .../MultiGestureCanvas/useTapGestures.ts | 24 ++++---------- src/components/MultiGestureCanvas/utils.ts | 32 ++++++++----------- 12 files changed, 69 insertions(+), 114 deletions(-) delete mode 100644 src/components/MultiGestureCanvas/getCanvasFitScale.ts diff --git a/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts b/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts index aff8b4a0cae9..ae318a8c7eef 100644 --- a/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts +++ b/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts @@ -1,11 +1,12 @@ +import type {ForwardedRef} from 'react'; import {createContext} from 'react'; import type PagerView from 'react-native-pager-view'; import type {SharedValue} from 'react-native-reanimated'; type AttachmentCarouselPagerContextType = { - onTap?: () => void; + onTap: () => void; onScaleChanged: (scale: number) => void; - pagerRef: React.Ref; + pagerRef: ForwardedRef; shouldPagerScroll: SharedValue; isSwipingInPager: SharedValue; }; diff --git a/src/components/Attachments/AttachmentCarousel/Pager/index.tsx b/src/components/Attachments/AttachmentCarousel/Pager/index.tsx index 0e41c4be56b3..687ed64d9ca3 100644 --- a/src/components/Attachments/AttachmentCarousel/Pager/index.tsx +++ b/src/components/Attachments/AttachmentCarousel/Pager/index.tsx @@ -1,4 +1,4 @@ -import type {Ref} from 'react'; +import type {ForwardedRef} from 'react'; import React, {useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react'; import {View} from 'react-native'; import type {NativeViewGestureHandlerProps} from 'react-native-gesture-handler'; @@ -33,7 +33,7 @@ type AttachmentCarouselPagerProps = { onScaleChanged: (scale: number) => void; }; -function AttachmentCarouselPager({items, renderItem, initialIndex, onPageSelected, onScaleChanged}: AttachmentCarouselPagerProps, ref: Ref) { +function AttachmentCarouselPager({items, renderItem, initialIndex, onPageSelected, onScaleChanged}: AttachmentCarouselPagerProps, ref: ForwardedRef) { const styles = useThemeStyles(); const shouldPagerScroll = useSharedValue(true); const pagerRef = useRef(null); diff --git a/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.js b/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.js index 022a89753476..0c3b8835186b 100644 --- a/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.js +++ b/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.js @@ -33,7 +33,7 @@ function BaseAttachmentViewPdf({ if (isUsedInCarousel && attachmentCarouselPagerContext) { const shouldPagerScroll = scale === 1; - attachmentCarouselPagerContext.onPinchGestureChange(!shouldPagerScroll); + attachmentCarouselPagerContext.onScaleChanged(1); if (attachmentCarouselPagerContext.shouldPagerScroll.value === shouldPagerScroll) { return; diff --git a/src/components/Lightbox.tsx b/src/components/Lightbox.tsx index f7eccdf3724f..6bf57ab4d9a8 100644 --- a/src/components/Lightbox.tsx +++ b/src/components/Lightbox.tsx @@ -27,7 +27,7 @@ type LightboxProps = { /** Whether source url requires authentication */ isAuthTokenRequired: boolean; - /** URI to full-sized attachment, SVG function, or numeric static image on native platforms */ + /** URI to full-sized attachment */ uri: string; /** Triggers whenever the zoom scale changes */ diff --git a/src/components/MultiGestureCanvas/constants.ts b/src/components/MultiGestureCanvas/constants.ts index 1d3e143c970c..7dba3e568ea4 100644 --- a/src/components/MultiGestureCanvas/constants.ts +++ b/src/components/MultiGestureCanvas/constants.ts @@ -11,16 +11,16 @@ const SPRING_CONFIG: WithSpringConfig = { }; // The default zoom range within the user can pinch to zoom the content inside the canvas -const defaultZoomRange: Required = { +const DEFAULT_ZOOM_RANGE: Required = { min: 1, max: 20, }; -// The zoom scale bounce factors are used to determine the amount of bounce +// The zoom range bounce factors are used to determine the amount of bounce // that is allowed when the user zooms more than the min or max zoom levels -const zoomScaleBounceFactors: Required = { +const ZOOM_RANGE_BOUNCE_FACTORS: Required = { min: 0.7, max: 1.5, }; -export {SPRING_CONFIG, defaultZoomRange, zoomScaleBounceFactors}; +export {SPRING_CONFIG, DEFAULT_ZOOM_RANGE, ZOOM_RANGE_BOUNCE_FACTORS}; diff --git a/src/components/MultiGestureCanvas/getCanvasFitScale.ts b/src/components/MultiGestureCanvas/getCanvasFitScale.ts deleted file mode 100644 index 8fbb72e1f294..000000000000 --- a/src/components/MultiGestureCanvas/getCanvasFitScale.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type {CanvasSize, ContentSize} from './types'; - -type GetCanvasFitScale = (props: {canvasSize: CanvasSize; contentSize: ContentSize}) => {scaleX: number; scaleY: number; minScale: number; maxScale: number}; - -const getCanvasFitScale: GetCanvasFitScale = ({canvasSize, contentSize}) => { - const scaleX = canvasSize.width / contentSize.width; - const scaleY = canvasSize.height / contentSize.height; - - const minScale = Math.min(scaleX, scaleY); - const maxScale = Math.max(scaleX, scaleY); - - return {scaleX, scaleY, minScale, maxScale}; -}; - -export default getCanvasFitScale; diff --git a/src/components/MultiGestureCanvas/index.tsx b/src/components/MultiGestureCanvas/index.tsx index 7388d1942dad..aba12c2eec59 100644 --- a/src/components/MultiGestureCanvas/index.tsx +++ b/src/components/MultiGestureCanvas/index.tsx @@ -2,13 +2,12 @@ import React, {useCallback, useContext, useEffect, useMemo, useRef} from 'react' import {View} from 'react-native'; import {Gesture, GestureDetector} from 'react-native-gesture-handler'; import type PagerView from 'react-native-pager-view'; -import Animated, {cancelAnimation, runOnUI, useAnimatedReaction, useAnimatedStyle, useDerivedValue, useSharedValue, withSpring} from 'react-native-reanimated'; +import Animated, {cancelAnimation, runOnUI, useAnimatedReaction, useAnimatedStyle, useDerivedValue, useSharedValue, useWorkletCallback, withSpring} from 'react-native-reanimated'; import AttachmentCarouselPagerContext from '@components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; import type ChildrenProps from '@src/types/utils/ChildrenProps'; -import {defaultZoomRange, SPRING_CONFIG} from './constants'; -import getCanvasFitScale from './getCanvasFitScale'; +import {DEFAULT_ZOOM_RANGE, SPRING_CONFIG, ZOOM_RANGE_BOUNCE_FACTORS} from './constants'; import type {CanvasSize, ContentSize, OnScaleChangedCallback, ZoomRange} from './types'; import usePanGesture from './usePanGesture'; import usePinchGesture from './usePinchGesture'; @@ -20,7 +19,7 @@ type MultiGestureCanvasProps = ChildrenProps & { * Wheter the canvas is currently active (in the screen) or not. * Disables certain gestures and functionality */ - isActive: boolean; + isActive?: boolean; /** The width and height of the canvas. * This is needed in order to properly scale the content in the canvas @@ -33,7 +32,7 @@ type MultiGestureCanvasProps = ChildrenProps & { contentSize?: ContentSize; /** Range of zoom that can be applied to the content by pinching or double tapping. */ - zoomRange?: ZoomRange; + zoomRange?: Partial; /** Handles scale changed event */ onScaleChanged?: OnScaleChangedCallback; @@ -86,8 +85,8 @@ function MultiGestureCanvas({ const zoomRange = useMemo( () => ({ - min: zoomRangeProp?.min ?? defaultZoomRange.min, - max: zoomRangeProp?.max ?? defaultZoomRange.max, + min: zoomRangeProp?.min ?? DEFAULT_ZOOM_RANGE.min, + max: zoomRangeProp?.max ?? DEFAULT_ZOOM_RANGE.max, }), [zoomRangeProp?.max, zoomRangeProp?.min], ); @@ -95,7 +94,7 @@ function MultiGestureCanvas({ // Based on the (original) content size and the canvas size, we calculate the horizontal and vertical scale factors // to fit the content inside the canvas // We later use the lower of the two scale factors to fit the content inside the canvas - const {minScale: minContentScale, maxScale: maxContentScale} = useMemo(() => getCanvasFitScale({canvasSize, contentSize}), [canvasSize, contentSize]); + const {minScale: minContentScale, maxScale: maxContentScale} = useMemo(() => MultiGestureCanvasUtils.getCanvasFitScale({canvasSize, contentSize}), [canvasSize, contentSize]); const zoomScale = useSharedValue(1); @@ -119,7 +118,7 @@ function MultiGestureCanvas({ /** * Stops any currently running decay animation from panning */ - const stopAnimation = MultiGestureCanvasUtils.useWorkletCallback(() => { + const stopAnimation = useWorkletCallback(() => { cancelAnimation(offsetX); cancelAnimation(offsetY); }); @@ -127,7 +126,7 @@ function MultiGestureCanvas({ /** * Resets the canvas to the initial state and animates back smoothly */ - const reset = MultiGestureCanvasUtils.useWorkletCallback((animated: boolean) => { + const reset = useWorkletCallback((animated: boolean) => { pinchScale.value = 1; stopAnimation(); @@ -269,6 +268,5 @@ function MultiGestureCanvas({ MultiGestureCanvas.displayName = 'MultiGestureCanvas'; export default MultiGestureCanvas; -export {defaultZoomRange}; -export {zoomScaleBounceFactors} from './constants'; +export {DEFAULT_ZOOM_RANGE, ZOOM_RANGE_BOUNCE_FACTORS}; export type {MultiGestureCanvasProps}; diff --git a/src/components/MultiGestureCanvas/types.ts b/src/components/MultiGestureCanvas/types.ts index b4de586cd9da..d0f8a775f2aa 100644 --- a/src/components/MultiGestureCanvas/types.ts +++ b/src/components/MultiGestureCanvas/types.ts @@ -1,5 +1,4 @@ import type {SharedValue} from 'react-native-reanimated'; -import type {WorkletFunction} from 'react-native-reanimated/lib/typescript/reanimated2/commonTypes'; /** Dimensions of the canvas rendered by the MultiGestureCanvas */ type CanvasSize = { @@ -15,8 +14,8 @@ type ContentSize = { /** Range of zoom that can be applied to the content by pinching or double tapping. */ type ZoomRange = { - min?: number; - max?: number; + min: number; + max: number; }; /** Triggered whenever the scale of the MultiGestureCanvas changes */ @@ -27,6 +26,9 @@ type OnTapCallback = (() => void) | undefined; /** Types used of variables used within the MultiGestureCanvas component and it's hooks */ type MultiGestureCanvasVariables = { + canvasSize: CanvasSize; + contentSize: ContentSize; + zoomRange: ZoomRange; minContentScale: number; maxContentScale: number; isSwipingInPager: SharedValue; @@ -39,9 +41,10 @@ type MultiGestureCanvasVariables = { panTranslateY: SharedValue; pinchTranslateX: SharedValue; pinchTranslateY: SharedValue; - stopAnimation: WorkletFunction<[], void>; - reset: WorkletFunction<[boolean], void>; - onTap: OnTapCallback; + stopAnimation: () => void; + reset: (animated: boolean) => void; + onTap: OnTapCallback | undefined; + onScaleChanged: OnScaleChangedCallback | undefined; }; export type {CanvasSize, ContentSize, ZoomRange, OnScaleChangedCallback, MultiGestureCanvasVariables}; diff --git a/src/components/MultiGestureCanvas/usePanGesture.ts b/src/components/MultiGestureCanvas/usePanGesture.ts index 7f7c2152d126..3ef791ad64b0 100644 --- a/src/components/MultiGestureCanvas/usePanGesture.ts +++ b/src/components/MultiGestureCanvas/usePanGesture.ts @@ -1,9 +1,9 @@ /* eslint-disable no-param-reassign */ import type {PanGesture} from 'react-native-gesture-handler'; import {Gesture} from 'react-native-gesture-handler'; -import {useDerivedValue, useSharedValue, withDecay, withSpring} from 'react-native-reanimated'; +import {useDerivedValue, useSharedValue, useWorkletCallback, withDecay, withSpring} from 'react-native-reanimated'; import {SPRING_CONFIG} from './constants'; -import type {CanvasSize, ContentSize, MultiGestureCanvasVariables} from './types'; +import type {MultiGestureCanvasVariables} from './types'; import * as MultiGestureCanvasUtils from './utils'; // This value determines how fast the pan animation should phase out @@ -11,18 +11,10 @@ import * as MultiGestureCanvasUtils from './utils'; // https://docs.swmansion.com/react-native-reanimated/docs/animations/withDecay/ const PAN_DECAY_DECELARATION = 0.9915; -type UsePanGestureProps = { - canvasSize: CanvasSize; - contentSize: ContentSize; - zoomScale: MultiGestureCanvasVariables['zoomScale']; - totalScale: MultiGestureCanvasVariables['totalScale']; - offsetX: MultiGestureCanvasVariables['offsetX']; - offsetY: MultiGestureCanvasVariables['offsetY']; - panTranslateX: MultiGestureCanvasVariables['panTranslateX']; - panTranslateY: MultiGestureCanvasVariables['panTranslateY']; - isSwipingInPager: MultiGestureCanvasVariables['isSwipingInPager']; - stopAnimation: MultiGestureCanvasVariables['stopAnimation']; -}; +type UsePanGestureProps = Pick< + MultiGestureCanvasVariables, + 'canvasSize' | 'contentSize' | 'zoomScale' | 'totalScale' | 'offsetX' | 'offsetY' | 'panTranslateX' | 'panTranslateY' | 'isSwipingInPager' | 'stopAnimation' +>; const usePanGesture = ({canvasSize, contentSize, zoomScale, totalScale, offsetX, offsetY, panTranslateX, panTranslateY, isSwipingInPager, stopAnimation}: UsePanGestureProps): PanGesture => { // The content size after fitting it to the canvas and zooming @@ -37,7 +29,7 @@ const usePanGesture = ({canvasSize, contentSize, zoomScale, totalScale, offsetX, // Calculates bounds of the scaled content // Can we pan left/right/up/down // Can be used to limit gesture or implementing tension effect - const getBounds = MultiGestureCanvasUtils.useWorkletCallback(() => { + const getBounds = useWorkletCallback(() => { let horizontalBoundary = 0; let verticalBoundary = 0; @@ -73,7 +65,7 @@ const usePanGesture = ({canvasSize, contentSize, zoomScale, totalScale, offsetX, // We want to smoothly decay/end the gesture by phasing out the pan animation // In case the content is outside of the boundaries of the canvas, // we need to move the content back into the boundaries - const finishPanGesture = MultiGestureCanvasUtils.useWorkletCallback(() => { + const finishPanGesture = useWorkletCallback(() => { // If the content is centered within the canvas, we don't need to run any animations if (offsetX.value === 0 && offsetY.value === 0 && panTranslateX.value === 0 && panTranslateY.value === 0) { return; diff --git a/src/components/MultiGestureCanvas/usePinchGesture.ts b/src/components/MultiGestureCanvas/usePinchGesture.ts index d149316f6e85..a2a1f58864d6 100644 --- a/src/components/MultiGestureCanvas/usePinchGesture.ts +++ b/src/components/MultiGestureCanvas/usePinchGesture.ts @@ -2,24 +2,14 @@ import {useEffect, useState} from 'react'; import type {PinchGesture} from 'react-native-gesture-handler'; import {Gesture} from 'react-native-gesture-handler'; -import {runOnJS, useAnimatedReaction, useSharedValue, withSpring} from 'react-native-reanimated'; -import {SPRING_CONFIG, zoomScaleBounceFactors} from './constants'; -import type {CanvasSize, MultiGestureCanvasVariables, OnScaleChangedCallback, ZoomRange} from './types'; -import * as MultiGestureCanvasUtils from './utils'; - -type UsePinchGestureProps = { - canvasSize: CanvasSize; - zoomScale: MultiGestureCanvasVariables['zoomScale']; - zoomRange: Required; - offsetX: MultiGestureCanvasVariables['offsetX']; - offsetY: MultiGestureCanvasVariables['offsetY']; - pinchTranslateX: MultiGestureCanvasVariables['pinchTranslateX']; - pinchTranslateY: MultiGestureCanvasVariables['pinchTranslateY']; - pinchScale: MultiGestureCanvasVariables['pinchScale']; - isSwipingInPager: MultiGestureCanvasVariables['isSwipingInPager']; - stopAnimation: MultiGestureCanvasVariables['stopAnimation']; - onScaleChanged: OnScaleChangedCallback; -}; +import {runOnJS, useAnimatedReaction, useSharedValue, useWorkletCallback, withSpring} from 'react-native-reanimated'; +import {SPRING_CONFIG, ZOOM_RANGE_BOUNCE_FACTORS} from './constants'; +import type {MultiGestureCanvasVariables} from './types'; + +type UsePinchGestureProps = Pick< + MultiGestureCanvasVariables, + 'canvasSize' | 'zoomScale' | 'zoomRange' | 'offsetX' | 'offsetY' | 'pinchTranslateX' | 'pinchTranslateY' | 'pinchScale' | 'isSwipingInPager' | 'stopAnimation' | 'onScaleChanged' +>; const usePinchGesture = ({ canvasSize, @@ -67,7 +57,7 @@ const usePinchGesture = ({ * Calculates the adjusted focal point of the pinch gesture, * based on the canvas size and the current offset */ - const getAdjustedFocal = MultiGestureCanvasUtils.useWorkletCallback( + const getAdjustedFocal = useWorkletCallback( (focalX: number, focalY: number) => ({ x: focalX - (canvasSize.width / 2 + offsetX.value), y: focalY - (canvasSize.height / 2 + offsetY.value), @@ -115,7 +105,7 @@ const usePinchGesture = ({ const newZoomScale = pinchScale.value * evt.scale; // Limit the zoom scale to zoom range including bounce range - if (zoomScale.value >= zoomRange.min * zoomScaleBounceFactors.min && zoomScale.value <= zoomRange.max * zoomScaleBounceFactors.max) { + if (zoomScale.value >= zoomRange.min * ZOOM_RANGE_BOUNCE_FACTORS.min && zoomScale.value <= zoomRange.max * ZOOM_RANGE_BOUNCE_FACTORS.max) { zoomScale.value = newZoomScale; currentPinchScale.value = evt.scale; diff --git a/src/components/MultiGestureCanvas/useTapGestures.ts b/src/components/MultiGestureCanvas/useTapGestures.ts index ba928d08349c..0a1102a4c9a3 100644 --- a/src/components/MultiGestureCanvas/useTapGestures.ts +++ b/src/components/MultiGestureCanvas/useTapGestures.ts @@ -2,27 +2,17 @@ import {useMemo} from 'react'; import type {TapGesture} from 'react-native-gesture-handler'; import {Gesture} from 'react-native-gesture-handler'; -import {runOnJS, withSpring} from 'react-native-reanimated'; +import {runOnJS, useWorkletCallback, withSpring} from 'react-native-reanimated'; import {SPRING_CONFIG} from './constants'; -import type {CanvasSize, ContentSize, MultiGestureCanvasVariables, OnScaleChangedCallback} from './types'; +import type {MultiGestureCanvasVariables} from './types'; import * as MultiGestureCanvasUtils from './utils'; const DOUBLE_TAP_SCALE = 3; -type UseTapGesturesProps = { - canvasSize: CanvasSize; - contentSize: ContentSize; - minContentScale: MultiGestureCanvasVariables['minContentScale']; - maxContentScale: MultiGestureCanvasVariables['maxContentScale']; - offsetX: MultiGestureCanvasVariables['offsetX']; - offsetY: MultiGestureCanvasVariables['offsetY']; - pinchScale: MultiGestureCanvasVariables['pinchScale']; - zoomScale: MultiGestureCanvasVariables['zoomScale']; - reset: MultiGestureCanvasVariables['reset']; - stopAnimation: MultiGestureCanvasVariables['stopAnimation']; - onScaleChanged: OnScaleChangedCallback; - onTap: MultiGestureCanvasVariables['onTap']; -}; +type UseTapGesturesProps = Pick< + MultiGestureCanvasVariables, + 'canvasSize' | 'contentSize' | 'minContentScale' | 'maxContentScale' | 'offsetX' | 'offsetY' | 'pinchScale' | 'zoomScale' | 'reset' | 'stopAnimation' | 'onScaleChanged' | 'onTap' +>; const useTapGestures = ({ canvasSize, @@ -45,7 +35,7 @@ const useTapGestures = ({ // On double tap the content should be zoomed to fill, but at least zoomed by DOUBLE_TAP_SCALE const doubleTapScale = useMemo(() => Math.max(DOUBLE_TAP_SCALE, maxContentScale / minContentScale), [maxContentScale, minContentScale]); - const zoomToCoordinates = MultiGestureCanvasUtils.useWorkletCallback( + const zoomToCoordinates = useWorkletCallback( (focalX: number, focalY: number) => { 'worklet'; diff --git a/src/components/MultiGestureCanvas/utils.ts b/src/components/MultiGestureCanvas/utils.ts index d6d018f5be25..e5688489c048 100644 --- a/src/components/MultiGestureCanvas/utils.ts +++ b/src/components/MultiGestureCanvas/utils.ts @@ -1,5 +1,16 @@ -import {useCallback} from 'react'; -import type {WorkletFunction} from 'react-native-reanimated/lib/typescript/reanimated2/commonTypes'; +import type {CanvasSize, ContentSize} from './types'; + +type GetCanvasFitScale = (props: {canvasSize: CanvasSize; contentSize: ContentSize}) => {scaleX: number; scaleY: number; minScale: number; maxScale: number}; + +const getCanvasFitScale: GetCanvasFitScale = ({canvasSize, contentSize}) => { + const scaleX = canvasSize.width / contentSize.width; + const scaleY = canvasSize.height / contentSize.height; + + const minScale = Math.min(scaleX, scaleY); + const maxScale = Math.max(scaleX, scaleY); + + return {scaleX, scaleY, minScale, maxScale}; +}; /** Clamps a value between a lower and upper bound */ function clamp(value: number, lowerBound: number, upperBound: number) { @@ -8,19 +19,4 @@ function clamp(value: number, lowerBound: number, upperBound: number) { return Math.min(Math.max(lowerBound, value), upperBound); } -/** - * Creates a memoized callback on the UI thread - * Same as `useWorkletCallback` from `react-native-reanimated` but without the deprecation warning - */ -// eslint-disable-next-line @typescript-eslint/ban-types -function useWorkletCallback( - callback: Parameters ReturnValue>>[0], - deps: Parameters ReturnValue>>[1] = [], -): WorkletFunction { - 'worklet'; - - // eslint-disable-next-line react-hooks/exhaustive-deps - return useCallback<(...args: Args) => ReturnValue>(callback, deps) as WorkletFunction; -} - -export {clamp, useWorkletCallback}; +export {getCanvasFitScale, clamp}; From 71135aa3970d195e48d8ab15eed144c0117be885 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Tue, 16 Jan 2024 14:55:00 +0100 Subject: [PATCH 244/580] more improvements --- src/components/Lightbox.tsx | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/components/Lightbox.tsx b/src/components/Lightbox.tsx index 6bf57ab4d9a8..5b554b84fe33 100644 --- a/src/components/Lightbox.tsx +++ b/src/components/Lightbox.tsx @@ -3,9 +3,9 @@ import type {LayoutChangeEvent, NativeSyntheticEvent, StyleProp, ViewStyle} from import {ActivityIndicator, PixelRatio, StyleSheet, View} from 'react-native'; import useStyleUtils from '@hooks/useStyleUtils'; import Image from './Image'; -import MultiGestureCanvas, {defaultZoomRange} from './MultiGestureCanvas'; -import getCanvasFitScale from './MultiGestureCanvas/getCanvasFitScale'; +import MultiGestureCanvas, {DEFAULT_ZOOM_RANGE} from './MultiGestureCanvas'; import type {ContentSize, OnScaleChangedCallback, ZoomRange} from './MultiGestureCanvas/types'; +import {getCanvasFitScale} from './MultiGestureCanvas/utils'; // Increase/decrease this number to change the number of concurrent lightboxes // The more concurrent lighboxes, the worse performance gets (especially on low-end devices) @@ -25,7 +25,7 @@ type ImageOnLoadEvent = NativeSyntheticEvent; type LightboxProps = { /** Whether source url requires authentication */ - isAuthTokenRequired: boolean; + isAuthTokenRequired?: boolean; /** URI to full-sized attachment */ uri: string; @@ -37,19 +37,19 @@ type LightboxProps = { onError: () => void; /** Additional styles to add to the component */ - style: StyleProp; + style?: StyleProp; /** The index of the carousel item */ - index: number; + index?: number; /** The index of the currently active carousel item */ - activeIndex: number; + activeIndex?: number; /** Whether the Lightbox is used within a carousel component and there are other sibling elements */ - hasSiblingCarouselItems: boolean; + hasSiblingCarouselItems?: boolean; /** Range of zoom that can be applied to the content by pinching or double tapping. */ - zoomRange: ZoomRange; + zoomRange?: Partial; }; /** @@ -64,7 +64,7 @@ function Lightbox({ index = 0, activeIndex = 0, hasSiblingCarouselItems = false, - zoomRange = defaultZoomRange, + zoomRange = DEFAULT_ZOOM_RANGE, }: LightboxProps) { const StyleUtils = useStyleUtils(); From 397c1aa82866f08bd35b693baaf18aa509c7fb31 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Tue, 16 Jan 2024 14:56:13 +0100 Subject: [PATCH 245/580] rename context type --- .../Pager/AttachmentCarouselPagerContext.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts b/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts index ae318a8c7eef..b901fa0eacf0 100644 --- a/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts +++ b/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts @@ -3,7 +3,7 @@ import {createContext} from 'react'; import type PagerView from 'react-native-pager-view'; import type {SharedValue} from 'react-native-reanimated'; -type AttachmentCarouselPagerContextType = { +type AttachmentCarouselPagerContextValue = { onTap: () => void; onScaleChanged: (scale: number) => void; pagerRef: ForwardedRef; @@ -11,7 +11,7 @@ type AttachmentCarouselPagerContextType = { isSwipingInPager: SharedValue; }; -const AttachmentCarouselPagerContext = createContext(null); +const AttachmentCarouselPagerContext = createContext(null); export default AttachmentCarouselPagerContext; -export type {AttachmentCarouselPagerContextType}; +export type {AttachmentCarouselPagerContextValue}; From 993afa86543c3e0ba17f550f1bce9c1ce899bbb7 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Tue, 16 Jan 2024 15:09:48 +0100 Subject: [PATCH 246/580] further address comments --- .../Pager/AttachmentCarouselPagerContext.ts | 1 - src/styles/utils/index.ts | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts b/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts index b901fa0eacf0..8ce3d41b97f9 100644 --- a/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts +++ b/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts @@ -4,7 +4,6 @@ import type PagerView from 'react-native-pager-view'; import type {SharedValue} from 'react-native-reanimated'; type AttachmentCarouselPagerContextValue = { - onTap: () => void; onScaleChanged: (scale: number) => void; pagerRef: ForwardedRef; shouldPagerScroll: SharedValue; diff --git a/src/styles/utils/index.ts b/src/styles/utils/index.ts index 059bc227393b..0b5acb959dd3 100644 --- a/src/styles/utils/index.ts +++ b/src/styles/utils/index.ts @@ -1008,8 +1008,8 @@ function getTransparentColor(color: string) { return `${color}00`; } -function getOpacityStyle(isHidden: boolean) { - return {opacity: isHidden ? 0 : 1}; +function getOpacityStyle(opacity: number) { + return {opacity}; } const staticStyleUtils = { From 74343c5f277a84816a180a36d32f7266c82cf2c5 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Tue, 16 Jan 2024 15:20:21 +0100 Subject: [PATCH 247/580] simplify Lightbox from https://github.com/Expensify/App/pull/34080 --- src/components/Lightbox.tsx | 129 ++++++++++++++++-------------------- 1 file changed, 56 insertions(+), 73 deletions(-) diff --git a/src/components/Lightbox.tsx b/src/components/Lightbox.tsx index 5b554b84fe33..3c092b5b4840 100644 --- a/src/components/Lightbox.tsx +++ b/src/components/Lightbox.tsx @@ -14,15 +14,10 @@ const NUMBER_OF_CONCURRENT_LIGHTBOXES: LightboxConcurrencyLimit = 3; const DEFAULT_IMAGE_SIZE = 200; const DEFAULT_IMAGE_DIMENSION: ContentSize = {width: DEFAULT_IMAGE_SIZE, height: DEFAULT_IMAGE_SIZE}; -type LightboxImageDimensions = { - lightboxSize?: ContentSize; - fallbackSize?: ContentSize; -}; - -const cachedDimensions = new Map(); - type ImageOnLoadEvent = NativeSyntheticEvent; +const cachedImageDimensions = new Map(); + type LightboxProps = { /** Whether source url requires authentication */ isAuthTokenRequired?: boolean; @@ -68,26 +63,50 @@ function Lightbox({ }: LightboxProps) { const StyleUtils = useStyleUtils(); - const [containerSize, setContainerSize] = useState({width: 0, height: 0}); - const isContainerLoaded = containerSize.width !== 0 && containerSize.height !== 0; + const [canvasSize, setCanvasSize] = useState({width: 0, height: 0}); + const isCanvasLoaded = canvasSize.width !== 0 && canvasSize.height !== 0; + const updateCanvasSize = useCallback( + ({ + nativeEvent: { + layout: {width, height}, + }, + }: LayoutChangeEvent) => setCanvasSize({width: PixelRatio.roundToNearestPixel(width), height: PixelRatio.roundToNearestPixel(height)}), + [], + ); - const [imageDimensions, setInternalImageDimensions] = useState(() => cachedDimensions.get(uri)); - const setImageDimensions = useCallback( - (newDimensions: LightboxImageDimensions) => { - setInternalImageDimensions(newDimensions); - cachedDimensions.set(uri, newDimensions); + const [contentSize, setInternalContentSize] = useState(() => cachedImageDimensions.get(uri)); + const setContentSize = useCallback( + (newDimensions: ContentSize | undefined) => { + setInternalContentSize(newDimensions); + cachedImageDimensions.set(uri, newDimensions); }, [uri], ); + const updateContentSize = useCallback( + ({nativeEvent: {width, height}}: ImageOnLoadEvent) => setContentSize({width: width * PixelRatio.get(), height: height * PixelRatio.get()}), + [setContentSize], + ); + const contentLoaded = contentSize != null; + const isItemActive = index === activeIndex; const [isActive, setActive] = useState(isItemActive); - const [isImageLoaded, setImageLoaded] = useState(false); - const isInactiveCarouselItem = hasSiblingCarouselItems && !isActive; + const [isFallbackVisible, setFallbackVisible] = useState(isInactiveCarouselItem); - const [isFallbackLoaded, setFallbackLoaded] = useState(false); + const [isFallbackImageLoaded, setFallbackImageLoaded] = useState(false); + const fallbackSize = useMemo(() => { + if (!hasSiblingCarouselItems || !contentSize || !isCanvasLoaded) { + return DEFAULT_IMAGE_DIMENSION; + } + + const {minScale} = getCanvasFitScale({canvasSize, contentSize}); + + return { + width: PixelRatio.roundToNearestPixel(contentSize.width * minScale), + height: PixelRatio.roundToNearestPixel(contentSize.height * minScale), + }; + }, [hasSiblingCarouselItems, contentSize, isCanvasLoaded, canvasSize]); - const isLightboxLoaded = imageDimensions?.lightboxSize !== undefined; const isLightboxInRange = useMemo(() => { if (NUMBER_OF_CONCURRENT_LIGHTBOXES === 'UNLIMITED') { return true; @@ -97,24 +116,16 @@ function Lightbox({ const indexOutOfRange = index > activeIndex + indexCanvasOffset || index < activeIndex - indexCanvasOffset; return !indexOutOfRange; }, [activeIndex, index]); - const isLightboxVisible = isLightboxInRange && (isActive || isLightboxLoaded || isFallbackLoaded); + const [isLightboxImageLoaded, setLightboxImageLoaded] = useState(false); + const isLightboxVisible = isLightboxInRange && (isActive || isLightboxImageLoaded || isFallbackImageLoaded); // If the fallback image is currently visible, we want to hide the Lightbox until the fallback gets hidden, // so that we don't see two overlapping images at the same time. // If there the Lightbox is not used within a carousel, we don't need to hide the Lightbox, // because it's only going to be rendered after the fallback image is hidden. - const shouldHideLightbox = hasSiblingCarouselItems && isFallbackVisible; - - const isLoading = isActive && (!isContainerLoaded || !isImageLoaded); + const shouldShowLightbox = !hasSiblingCarouselItems || !isFallbackVisible; - const updateCanvasSize = useCallback( - ({ - nativeEvent: { - layout: {width, height}, - }, - }: LayoutChangeEvent) => setContainerSize({width: PixelRatio.roundToNearestPixel(width), height: PixelRatio.roundToNearestPixel(height)}), - [], - ); + const isLoading = isActive && (!isCanvasLoaded || !contentLoaded); // We delay setting a page to active state by a (few) millisecond(s), // to prevent the image transformer from flashing while still rendering @@ -131,8 +142,8 @@ function Lightbox({ if (isLightboxVisible) { return; } - setImageLoaded(false); - }, [isLightboxVisible]); + setContentSize(undefined); + }, [isLightboxVisible, setContentSize]); useEffect(() => { if (!hasSiblingCarouselItems) { @@ -140,65 +151,46 @@ function Lightbox({ } if (isActive) { - if (isImageLoaded && isFallbackVisible) { + if (contentLoaded && isFallbackVisible) { // We delay hiding the fallback image while image transformer is still rendering setTimeout(() => { setFallbackVisible(false); - setFallbackLoaded(false); + setFallbackImageLoaded(false); }, 100); } } else { - if (isLightboxVisible && isLightboxLoaded) { + if (isLightboxVisible && isLightboxImageLoaded) { return; } // Show fallback when the image goes out of focus or when the image is loading setFallbackVisible(true); } - }, [hasSiblingCarouselItems, isActive, isImageLoaded, isFallbackVisible, isLightboxLoaded, isLightboxVisible]); - - const fallbackSize = useMemo(() => { - const imageSize = imageDimensions?.lightboxSize ?? imageDimensions?.fallbackSize; - - if (!hasSiblingCarouselItems || !imageSize || !isContainerLoaded) { - return DEFAULT_IMAGE_DIMENSION; - } - - const {minScale} = getCanvasFitScale({canvasSize: containerSize, contentSize: imageSize}); - - return { - width: PixelRatio.roundToNearestPixel(imageSize.width * minScale), - height: PixelRatio.roundToNearestPixel(imageSize.height * minScale), - }; - }, [containerSize, hasSiblingCarouselItems, imageDimensions?.fallbackSize, imageDimensions?.lightboxSize, isContainerLoaded]); + }, [contentLoaded, hasSiblingCarouselItems, isActive, isFallbackVisible, isLightboxImageLoaded, isLightboxVisible]); return ( - {isContainerLoaded && ( + {isCanvasLoaded && ( <> {isLightboxVisible && ( - + setImageLoaded(true)} - onLoad={(e: ImageOnLoadEvent) => { - const width = e.nativeEvent.width * PixelRatio.get(); - const height = e.nativeEvent.height * PixelRatio.get(); - setImageDimensions({...imageDimensions, lightboxSize: {width, height}}); - }} + onLoad={updateContentSize} + onLoadEnd={() => setLightboxImageLoaded(true)} /> @@ -212,17 +204,8 @@ function Lightbox({ resizeMode="contain" style={fallbackSize} isAuthTokenRequired={isAuthTokenRequired} - onLoadEnd={() => setFallbackLoaded(true)} - onLoad={(e: ImageOnLoadEvent) => { - const width = e.nativeEvent.width * PixelRatio.get(); - const height = e.nativeEvent.height * PixelRatio.get(); - - if (isLightboxLoaded) { - return; - } - - setImageDimensions({...imageDimensions, fallbackSize: {width, height}}); - }} + onLoad={updateContentSize} + onLoadEnd={() => setFallbackImageLoaded(true)} /> )} From 27aeea97851e7e3badb5c97b8ccdf6f5ddd3aefe Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Tue, 16 Jan 2024 15:25:58 +0100 Subject: [PATCH 248/580] fix: arrow buttons --- .../Pager/AttachmentCarouselPagerContext.ts | 1 + .../AttachmentCarousel/Pager/index.tsx | 8 +++++--- .../AttachmentCarousel/index.native.js | 15 ++++++++------- 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts b/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts index 8ce3d41b97f9..b901fa0eacf0 100644 --- a/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts +++ b/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts @@ -4,6 +4,7 @@ import type PagerView from 'react-native-pager-view'; import type {SharedValue} from 'react-native-reanimated'; type AttachmentCarouselPagerContextValue = { + onTap: () => void; onScaleChanged: (scale: number) => void; pagerRef: ForwardedRef; shouldPagerScroll: SharedValue; diff --git a/src/components/Attachments/AttachmentCarousel/Pager/index.tsx b/src/components/Attachments/AttachmentCarousel/Pager/index.tsx index 687ed64d9ca3..efe360213d4d 100644 --- a/src/components/Attachments/AttachmentCarousel/Pager/index.tsx +++ b/src/components/Attachments/AttachmentCarousel/Pager/index.tsx @@ -29,11 +29,12 @@ type AttachmentCarouselPagerProps = { items: PagerItem[]; renderItem: (props: {item: PagerItem; index: number; isActive: boolean}) => React.ReactNode; initialIndex: number; + onTap: () => void; onPageSelected: () => void; onScaleChanged: (scale: number) => void; }; -function AttachmentCarouselPager({items, renderItem, initialIndex, onPageSelected, onScaleChanged}: AttachmentCarouselPagerProps, ref: ForwardedRef) { +function AttachmentCarouselPager({items, renderItem, initialIndex, onTap, onPageSelected, onScaleChanged}: AttachmentCarouselPagerProps, ref: ForwardedRef) { const styles = useThemeStyles(); const shouldPagerScroll = useSharedValue(true); const pagerRef = useRef(null); @@ -89,12 +90,13 @@ function AttachmentCarouselPager({items, renderItem, initialIndex, onPageSelecte const contextValue = useMemo( () => ({ - onScaleChanged, pagerRef, shouldPagerScroll, isSwipingInPager, + onTap, + onScaleChanged, }), - [isSwipingInPager, shouldPagerScroll, onScaleChanged], + [shouldPagerScroll, isSwipingInPager, onTap, onScaleChanged], ); return ( diff --git a/src/components/Attachments/AttachmentCarousel/index.native.js b/src/components/Attachments/AttachmentCarousel/index.native.js index 8f168093c217..062a5da05ce2 100644 --- a/src/components/Attachments/AttachmentCarousel/index.native.js +++ b/src/components/Attachments/AttachmentCarousel/index.native.js @@ -101,10 +101,9 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source, index={index} activeIndex={page} isFocused={isActive && activeSource === item.source} - onPress={() => setShouldShowArrows(!shouldShowArrows)} /> ), - [activeSource, attachments.length, page, setShouldShowArrows, shouldShowArrows], + [activeSource, attachments.length, page], ); const handleScaleChange = useCallback( @@ -122,11 +121,7 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source, ); return ( - setShouldShowArrows(true)} - onMouseLeave={() => setShouldShowArrows(false)} - > + {page == null ? ( ) : ( @@ -154,6 +149,12 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source, items={attachments} renderItem={renderItem} initialIndex={page} + onTap={() => { + if (!isZoomedOut) { + return; + } + setShouldShowArrows(!shouldShowArrows); + }} onPageSelected={({nativeEvent: {position: newPage}}) => updatePage(newPage)} onScaleChanged={handleScaleChange} ref={pagerRef} From 6d92147e1b31c6b4bb510e98a26db4a7b8ff6488 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Tue, 16 Jan 2024 16:48:25 +0100 Subject: [PATCH 249/580] fix: pager not swiping --- .../Pager/AttachmentCarouselPagerContext.ts | 5 +- .../AttachmentCarousel/Pager/index.tsx | 64 +++++++------------ .../AttachmentCarousel/index.native.js | 1 + src/components/MultiGestureCanvas/index.tsx | 47 +++++++------- src/components/MultiGestureCanvas/types.ts | 4 +- .../MultiGestureCanvas/usePanGesture.ts | 8 +-- .../MultiGestureCanvas/usePinchGesture.ts | 34 +++++----- .../MultiGestureCanvas/useTapGestures.ts | 24 ++++--- 8 files changed, 87 insertions(+), 100 deletions(-) diff --git a/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts b/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts index b901fa0eacf0..f23153f5e2e2 100644 --- a/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts +++ b/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts @@ -6,9 +6,8 @@ import type {SharedValue} from 'react-native-reanimated'; type AttachmentCarouselPagerContextValue = { onTap: () => void; onScaleChanged: (scale: number) => void; - pagerRef: ForwardedRef; - shouldPagerScroll: SharedValue; - isSwipingInPager: SharedValue; + pagerRef: ForwardedRef; // + isPagerSwiping: SharedValue; }; const AttachmentCarouselPagerContext = createContext(null); diff --git a/src/components/Attachments/AttachmentCarousel/Pager/index.tsx b/src/components/Attachments/AttachmentCarousel/Pager/index.tsx index efe360213d4d..0331fc4044ce 100644 --- a/src/components/Attachments/AttachmentCarousel/Pager/index.tsx +++ b/src/components/Attachments/AttachmentCarousel/Pager/index.tsx @@ -5,7 +5,7 @@ import type {NativeViewGestureHandlerProps} from 'react-native-gesture-handler'; import {createNativeWrapper} from 'react-native-gesture-handler'; import type {PagerViewProps} from 'react-native-pager-view'; import PagerView from 'react-native-pager-view'; -import Animated, {runOnJS, useAnimatedProps, useAnimatedReaction, useSharedValue} from 'react-native-reanimated'; +import Animated, {useSharedValue} from 'react-native-reanimated'; import useThemeStyles from '@hooks/useThemeStyles'; import AttachmentCarouselPagerContext from './AttachmentCarouselPagerContext'; import usePageScrollHandler from './usePageScrollHandler'; @@ -27,6 +27,7 @@ type PagerItem = { type AttachmentCarouselPagerProps = { items: PagerItem[]; + scrollEnabled?: boolean; renderItem: (props: {item: PagerItem; index: number; isActive: boolean}) => React.ReactNode; initialIndex: number; onTap: () => void; @@ -34,44 +35,42 @@ type AttachmentCarouselPagerProps = { onScaleChanged: (scale: number) => void; }; -function AttachmentCarouselPager({items, renderItem, initialIndex, onTap, onPageSelected, onScaleChanged}: AttachmentCarouselPagerProps, ref: ForwardedRef) { +function AttachmentCarouselPager( + {items, scrollEnabled = true, renderItem, initialIndex, onTap, onPageSelected, onScaleChanged}: AttachmentCarouselPagerProps, + ref: ForwardedRef, +) { const styles = useThemeStyles(); - const shouldPagerScroll = useSharedValue(true); const pagerRef = useRef(null); - const isSwipingInPager = useSharedValue(false); - const activeIndex = useSharedValue(initialIndex); + const isPagerSwiping = useSharedValue(false); + const activePage = useSharedValue(initialIndex); + const [activePageState, setActivePageState] = useState(initialIndex); const pageScrollHandler = usePageScrollHandler( { onPageScroll: (e) => { 'worklet'; - activeIndex.value = e.position; - isSwipingInPager.value = e.offset !== 0; + activePage.value = e.position; + isPagerSwiping.value = e.offset !== 0; }, }, [], ); - const [activePage, setActivePage] = useState(initialIndex); - useEffect(() => { - setActivePage(initialIndex); - activeIndex.value = initialIndex; - }, [activeIndex, initialIndex]); - - // we use reanimated for this since onPageSelected is called - // in the middle of the pager animation - useAnimatedReaction( - () => isSwipingInPager.value, - (stillScrolling) => { - if (stillScrolling) { - return; - } + setActivePageState(initialIndex); + activePage.value = initialIndex; + }, [activePage, initialIndex]); - runOnJS(setActivePage)(activeIndex.value); - }, + const contextValue = useMemo( + () => ({ + pagerRef, + isPagerSwiping, + onTap, + onScaleChanged, + }), + [isPagerSwiping, onTap, onScaleChanged], ); useImperativeHandle( @@ -84,28 +83,13 @@ function AttachmentCarouselPager({items, renderItem, initialIndex, onTap, onPage [], ); - const animatedProps = useAnimatedProps(() => ({ - scrollEnabled: shouldPagerScroll.value, - })); - - const contextValue = useMemo( - () => ({ - pagerRef, - shouldPagerScroll, - isSwipingInPager, - onTap, - onScaleChanged, - }), - [shouldPagerScroll, isSwipingInPager, onTap, onScaleChanged], - ); - return ( - {renderItem({item, index, isActive: index === activePage})} + {renderItem({item, index, isActive: index === activePageState})} ))} diff --git a/src/components/Attachments/AttachmentCarousel/index.native.js b/src/components/Attachments/AttachmentCarousel/index.native.js index 062a5da05ce2..f3be3c97a8c6 100644 --- a/src/components/Attachments/AttachmentCarousel/index.native.js +++ b/src/components/Attachments/AttachmentCarousel/index.native.js @@ -147,6 +147,7 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source, { diff --git a/src/components/MultiGestureCanvas/index.tsx b/src/components/MultiGestureCanvas/index.tsx index aba12c2eec59..3bbc201f255f 100644 --- a/src/components/MultiGestureCanvas/index.tsx +++ b/src/components/MultiGestureCanvas/index.tsx @@ -1,8 +1,8 @@ import React, {useCallback, useContext, useEffect, useMemo, useRef} from 'react'; import {View} from 'react-native'; import {Gesture, GestureDetector} from 'react-native-gesture-handler'; -import type PagerView from 'react-native-pager-view'; -import Animated, {cancelAnimation, runOnUI, useAnimatedReaction, useAnimatedStyle, useDerivedValue, useSharedValue, useWorkletCallback, withSpring} from 'react-native-reanimated'; +import type {GestureRef} from 'react-native-gesture-handler/lib/typescript/handlers/gestures/gesture'; +import Animated, {cancelAnimation, runOnUI, useAnimatedStyle, useDerivedValue, useSharedValue, useWorkletCallback, withSpring} from 'react-native-reanimated'; import AttachmentCarouselPagerContext from '@components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -43,14 +43,12 @@ function MultiGestureCanvas({ contentSize = {width: 1, height: 1}, zoomRange: zoomRangeProp, isActive = true, - onScaleChanged: onScaleChangedProp, children, + onScaleChanged: onScaleChangedProp, }: MultiGestureCanvasProps) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); - const pagerRefFallback = useRef(null); - const shouldPagerScrollFallback = useSharedValue(false); const isSwipingInPagerFallback = useSharedValue(false); // If the MultiGestureCanvas used inside a AttachmentCarouselPager, we need to adapt the behaviour based on the pager state @@ -58,18 +56,17 @@ function MultiGestureCanvas({ const { onTap, onScaleChanged: onScaleChangedContext, - shouldPagerScroll, - isSwipingInPager, + isPagerSwiping, + pagerRef, } = useMemo( () => attachmentCarouselPagerContext ?? { onTap: () => {}, onScaleChanged: () => {}, - pagerRef: pagerRefFallback, - shouldPagerScroll: shouldPagerScrollFallback, - isSwipingInPager: isSwipingInPagerFallback, + pagerRef: undefined, + isPagerSwiping: isSwipingInPagerFallback, }, - [attachmentCarouselPagerContext, isSwipingInPagerFallback, shouldPagerScrollFallback], + [attachmentCarouselPagerContext, isSwipingInPagerFallback], ); /** @@ -126,7 +123,9 @@ function MultiGestureCanvas({ /** * Resets the canvas to the initial state and animates back smoothly */ - const reset = useWorkletCallback((animated: boolean) => { + const reset = useWorkletCallback((animated: boolean, callbackProp?: () => void) => { + const callback = callbackProp ?? (() => {}); + pinchScale.value = 1; stopAnimation(); @@ -140,7 +139,7 @@ function MultiGestureCanvas({ panTranslateY.value = withSpring(0, SPRING_CONFIG); pinchTranslateX.value = withSpring(0, SPRING_CONFIG); pinchTranslateY.value = withSpring(0, SPRING_CONFIG); - zoomScale.value = withSpring(1, SPRING_CONFIG); + zoomScale.value = withSpring(1, SPRING_CONFIG, callback); return; } @@ -151,6 +150,8 @@ function MultiGestureCanvas({ pinchTranslateX.value = 0; pinchTranslateY.value = 0; zoomScale.value = 1; + + callback(); }); const {singleTapGesture: basicSingleTapGesture, doubleTapGesture} = useTapGestures({ @@ -169,6 +170,11 @@ function MultiGestureCanvas({ }); const singleTapGesture = basicSingleTapGesture.requireExternalGestureToFail(doubleTapGesture, panGestureRef); + const panGestureSimultaneousList = useMemo( + () => (pagerRef === undefined ? [singleTapGesture, doubleTapGesture] : [pagerRef as unknown as Exclude, singleTapGesture, doubleTapGesture]), + [doubleTapGesture, pagerRef, singleTapGesture], + ); + const panGesture = usePanGesture({ canvasSize, contentSize, @@ -178,10 +184,10 @@ function MultiGestureCanvas({ offsetY, panTranslateX, panTranslateY, - isSwipingInPager, + isPagerSwiping, stopAnimation, }) - .simultaneousWithExternalGesture(singleTapGesture, doubleTapGesture) + .simultaneousWithExternalGesture(...panGestureSimultaneousList) .withRef(panGestureRef); const pinchGesture = usePinchGesture({ @@ -193,20 +199,11 @@ function MultiGestureCanvas({ pinchTranslateX, pinchTranslateY, pinchScale, - isSwipingInPager, + isPagerSwiping, stopAnimation, onScaleChanged, }).simultaneousWithExternalGesture(panGesture, singleTapGesture, doubleTapGesture); - // Enables/disables the pager scroll based on the zoom scale - // When the content is zoomed in/out, the pager should be disabled - useAnimatedReaction( - () => zoomScale.value, - () => { - shouldPagerScroll.value = zoomScale.value === 1; - }, - ); - // Trigger a reset when the canvas gets inactive, but only if it was already mounted before const mounted = useRef(false); useEffect(() => { diff --git a/src/components/MultiGestureCanvas/types.ts b/src/components/MultiGestureCanvas/types.ts index d0f8a775f2aa..6d4bc2d252ba 100644 --- a/src/components/MultiGestureCanvas/types.ts +++ b/src/components/MultiGestureCanvas/types.ts @@ -31,7 +31,7 @@ type MultiGestureCanvasVariables = { zoomRange: ZoomRange; minContentScale: number; maxContentScale: number; - isSwipingInPager: SharedValue; + isPagerSwiping: SharedValue; zoomScale: SharedValue; totalScale: SharedValue; pinchScale: SharedValue; @@ -42,7 +42,7 @@ type MultiGestureCanvasVariables = { pinchTranslateX: SharedValue; pinchTranslateY: SharedValue; stopAnimation: () => void; - reset: (animated: boolean) => void; + reset: (animated: boolean, callbackProp: () => void) => void; onTap: OnTapCallback | undefined; onScaleChanged: OnScaleChangedCallback | undefined; }; diff --git a/src/components/MultiGestureCanvas/usePanGesture.ts b/src/components/MultiGestureCanvas/usePanGesture.ts index 3ef791ad64b0..8a646446fad4 100644 --- a/src/components/MultiGestureCanvas/usePanGesture.ts +++ b/src/components/MultiGestureCanvas/usePanGesture.ts @@ -13,10 +13,10 @@ const PAN_DECAY_DECELARATION = 0.9915; type UsePanGestureProps = Pick< MultiGestureCanvasVariables, - 'canvasSize' | 'contentSize' | 'zoomScale' | 'totalScale' | 'offsetX' | 'offsetY' | 'panTranslateX' | 'panTranslateY' | 'isSwipingInPager' | 'stopAnimation' + 'canvasSize' | 'contentSize' | 'zoomScale' | 'totalScale' | 'offsetX' | 'offsetY' | 'panTranslateX' | 'panTranslateY' | 'isPagerSwiping' | 'stopAnimation' >; -const usePanGesture = ({canvasSize, contentSize, zoomScale, totalScale, offsetX, offsetY, panTranslateX, panTranslateY, isSwipingInPager, stopAnimation}: UsePanGestureProps): PanGesture => { +const usePanGesture = ({canvasSize, contentSize, zoomScale, totalScale, offsetX, offsetY, panTranslateX, panTranslateY, isPagerSwiping, stopAnimation}: UsePanGestureProps): PanGesture => { // The content size after fitting it to the canvas and zooming const zoomedContentWidth = useDerivedValue(() => contentSize.width * totalScale.value, [contentSize.width]); const zoomedContentHeight = useDerivedValue(() => contentSize.height * totalScale.value, [contentSize.height]); @@ -117,7 +117,7 @@ const usePanGesture = ({canvasSize, contentSize, zoomScale, totalScale, offsetX, // eslint-disable-next-line @typescript-eslint/naming-convention .onTouchesMove((_evt, state) => { // We only allow panning when the content is zoomed in - if (zoomScale.value <= 1 || isSwipingInPager.value) { + if (zoomScale.value <= 1 || isPagerSwiping.value) { return; } @@ -147,7 +147,7 @@ const usePanGesture = ({canvasSize, contentSize, zoomScale, totalScale, offsetX, panTranslateY.value = 0; // If we are swiping (in the pager), we don't want to return to boundaries - if (isSwipingInPager.value) { + if (isPagerSwiping.value) { return; } diff --git a/src/components/MultiGestureCanvas/usePinchGesture.ts b/src/components/MultiGestureCanvas/usePinchGesture.ts index a2a1f58864d6..2ff375dc7edd 100644 --- a/src/components/MultiGestureCanvas/usePinchGesture.ts +++ b/src/components/MultiGestureCanvas/usePinchGesture.ts @@ -8,7 +8,7 @@ import type {MultiGestureCanvasVariables} from './types'; type UsePinchGestureProps = Pick< MultiGestureCanvasVariables, - 'canvasSize' | 'zoomScale' | 'zoomRange' | 'offsetX' | 'offsetY' | 'pinchTranslateX' | 'pinchTranslateY' | 'pinchScale' | 'isSwipingInPager' | 'stopAnimation' | 'onScaleChanged' + 'canvasSize' | 'zoomScale' | 'zoomRange' | 'offsetX' | 'offsetY' | 'pinchTranslateX' | 'pinchTranslateY' | 'pinchScale' | 'isPagerSwiping' | 'stopAnimation' | 'onScaleChanged' >; const usePinchGesture = ({ @@ -20,7 +20,7 @@ const usePinchGesture = ({ pinchTranslateX: totalPinchTranslateX, pinchTranslateY: totalPinchTranslateY, pinchScale, - isSwipingInPager, + isPagerSwiping, stopAnimation, onScaleChanged, }: UsePinchGestureProps): PinchGesture => { @@ -44,6 +44,16 @@ const usePinchGesture = ({ const pinchBounceTranslateX = useSharedValue(0); const pinchBounceTranslateY = useSharedValue(0); + const triggerScaleChangedEvent = () => { + 'worklet'; + + if (onScaleChanged === undefined) { + return; + } + + runOnJS(onScaleChanged)(zoomScale.value); + }; + // Update the total (pinch) translation based on the regular pinch + bounce useAnimatedReaction( () => [pinchTranslateX.value, pinchTranslateY.value, pinchBounceTranslateX.value, pinchBounceTranslateY.value], @@ -80,7 +90,7 @@ const usePinchGesture = ({ // eslint-disable-next-line @typescript-eslint/naming-convention .onTouchesDown((_evt, state) => { // We don't want to activate pinch gesture when we are swiping in the pager - if (!isSwipingInPager.value) { + if (!isPagerSwiping.value) { return; } @@ -109,9 +119,7 @@ const usePinchGesture = ({ zoomScale.value = newZoomScale; currentPinchScale.value = evt.scale; - if (onScaleChanged !== undefined) { - runOnJS(onScaleChanged)(zoomScale.value); - } + triggerScaleChangedEvent(); } // Calculate new pinch translation @@ -145,26 +153,18 @@ const usePinchGesture = ({ pinchBounceTranslateY.value = withSpring(0, SPRING_CONFIG); } - const triggerScaleChangeCallback = () => { - if (onScaleChanged === undefined) { - return; - } - - runOnJS(onScaleChanged)(zoomScale.value); - }; - if (zoomScale.value < zoomRange.min) { // If the zoom scale is less than the minimum zoom scale, we need to set the zoom scale to the minimum pinchScale.value = zoomRange.min; - zoomScale.value = withSpring(zoomRange.min, SPRING_CONFIG, triggerScaleChangeCallback); + zoomScale.value = withSpring(zoomRange.min, SPRING_CONFIG, triggerScaleChangedEvent); } else if (zoomScale.value > zoomRange.max) { // If the zoom scale is higher than the maximum zoom scale, we need to set the zoom scale to the maximum pinchScale.value = zoomRange.max; - zoomScale.value = withSpring(zoomRange.max, SPRING_CONFIG, triggerScaleChangeCallback); + zoomScale.value = withSpring(zoomRange.max, SPRING_CONFIG, triggerScaleChangedEvent); } else { // Otherwise, we just update the pinch scale offset pinchScale.value = zoomScale.value; - triggerScaleChangeCallback(); + triggerScaleChangedEvent(); } }); diff --git a/src/components/MultiGestureCanvas/useTapGestures.ts b/src/components/MultiGestureCanvas/useTapGestures.ts index 0a1102a4c9a3..137c2287e8cc 100644 --- a/src/components/MultiGestureCanvas/useTapGestures.ts +++ b/src/components/MultiGestureCanvas/useTapGestures.ts @@ -36,7 +36,7 @@ const useTapGestures = ({ const doubleTapScale = useMemo(() => Math.max(DOUBLE_TAP_SCALE, maxContentScale / minContentScale), [maxContentScale, minContentScale]); const zoomToCoordinates = useWorkletCallback( - (focalX: number, focalY: number) => { + (focalX: number, focalY: number, callbackProp: () => void) => { 'worklet'; stopAnimation(); @@ -100,9 +100,11 @@ const useTapGestures = ({ offsetAfterZooming.y = 0; } + const callback = callbackProp || (() => {}); + offsetX.value = withSpring(offsetAfterZooming.x, SPRING_CONFIG); offsetY.value = withSpring(offsetAfterZooming.y, SPRING_CONFIG); - zoomScale.value = withSpring(doubleTapScale, SPRING_CONFIG); + zoomScale.value = withSpring(doubleTapScale, SPRING_CONFIG, callback); pinchScale.value = doubleTapScale; }, [scaledContentWidth, scaledContentHeight, canvasSize, doubleTapScale], @@ -113,22 +115,26 @@ const useTapGestures = ({ .maxDelay(150) .maxDistance(20) .onEnd((evt) => { + const triggerScaleChangedEvent = () => { + 'worklet'; + + if (onScaleChanged != null) { + runOnJS(onScaleChanged)(zoomScale.value); + } + }; + // If the content is already zoomed, we want to reset the zoom, // otherwise we want to zoom in if (zoomScale.value > 1) { - reset(true); + reset(true, triggerScaleChangedEvent); } else { - zoomToCoordinates(evt.x, evt.y); - } - - if (onScaleChanged !== undefined) { - runOnJS(onScaleChanged)(zoomScale.value); + zoomToCoordinates(evt.x, evt.y, triggerScaleChangedEvent); } }); const singleTapGesture = Gesture.Tap() .numberOfTaps(1) - .maxDuration(50) + .maxDuration(125) .onBegin(() => { stopAnimation(); }) From 710a3fb137ea33d2f5c8fed55eadaa56d5c5aa07 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Tue, 16 Jan 2024 16:50:38 +0100 Subject: [PATCH 250/580] remove undefined --- src/components/MultiGestureCanvas/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/MultiGestureCanvas/types.ts b/src/components/MultiGestureCanvas/types.ts index 6d4bc2d252ba..cd3c5eb1cae5 100644 --- a/src/components/MultiGestureCanvas/types.ts +++ b/src/components/MultiGestureCanvas/types.ts @@ -19,7 +19,7 @@ type ZoomRange = { }; /** Triggered whenever the scale of the MultiGestureCanvas changes */ -type OnScaleChangedCallback = ((zoomScale: number) => void) | undefined; +type OnScaleChangedCallback = (zoomScale: number) => void; /** Triggered when the canvas is tapped (single tap) */ type OnTapCallback = (() => void) | undefined; From c2f3bc06004e2622886ac73fa6149b9b24d6ab54 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Tue, 16 Jan 2024 17:01:07 +0100 Subject: [PATCH 251/580] Change write string commands --- src/libs/API/types.ts | 2 +- src/libs/actions/BankAccounts.ts | 26 +++++++--------- src/libs/actions/Card.ts | 8 ++--- src/libs/actions/Chronos.ts | 3 +- src/libs/actions/PaymentMethods.ts | 10 +++--- src/libs/actions/PersonalDetails.ts | 20 ++++++------ src/libs/actions/PushNotification.ts | 3 +- src/libs/actions/Report.ts | 46 ++++++++++++++-------------- src/libs/actions/Session/index.ts | 20 ++++++------ src/libs/actions/TeachersUnite.ts | 5 +-- src/libs/actions/User.ts | 30 +++++++++--------- src/libs/actions/Wallet.ts | 12 ++++---- 12 files changed, 92 insertions(+), 93 deletions(-) diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index 1f5c58463e8a..4b71d67613a2 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -130,7 +130,7 @@ type WriteCommandParameters = { [WRITE_COMMANDS.RECONNECT_APP]: ReconnectAppParams; [WRITE_COMMANDS.OPEN_PROFILE]: OpenProfileParams; [WRITE_COMMANDS.HANDLE_RESTRICTED_EVENT]: HandleRestrictedEventParams; - [WRITE_COMMANDS.OPEN_REPORT]: HandleRestrictedEventParams; + [WRITE_COMMANDS.OPEN_REPORT]: OpenReportParams; }; const READ_COMMANDS = { diff --git a/src/libs/actions/BankAccounts.ts b/src/libs/actions/BankAccounts.ts index 6a4a0dcdfe2a..6b5882d76d1a 100644 --- a/src/libs/actions/BankAccounts.ts +++ b/src/libs/actions/BankAccounts.ts @@ -1,7 +1,7 @@ import Onyx from 'react-native-onyx'; import * as API from '@libs/API'; import type {OpenReimbursementAccountPageParams} from '@libs/API/parameters'; -import {READ_COMMANDS} from '@libs/API/types'; +import {READ_COMMANDS, WRITE_COMMANDS} from '@libs/API/types'; import * as ErrorUtils from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; import * as PlaidDataProps from '@pages/ReimbursementAccount/plaidDataPropTypes'; @@ -125,8 +125,6 @@ function getVBBADataForOnyx(currentStep?: BankAccountStep): OnyxData { * Submit Bank Account step with Plaid data so php can perform some checks. */ function connectBankAccountWithPlaid(bankAccountID: number, selectedPlaidBankAccount: PlaidBankAccount) { - const commandName = 'ConnectBankAccountWithPlaid'; - type ConnectBankAccountWithPlaidParams = { bankAccountID: number; routingNumber: string; @@ -145,7 +143,7 @@ function connectBankAccountWithPlaid(bankAccountID: number, selectedPlaidBankAcc plaidAccessToken: selectedPlaidBankAccount.plaidAccessToken, }; - API.write(commandName, parameters, getVBBADataForOnyx()); + API.write(WRITE_COMMANDS.CONNECT_BANK_ACCOUNT_WITH_PLAID, parameters, getVBBADataForOnyx()); } /** @@ -154,8 +152,6 @@ function connectBankAccountWithPlaid(bankAccountID: number, selectedPlaidBankAcc * TODO: offline pattern for this command will have to be added later once the pattern B design doc is complete */ function addPersonalBankAccount(account: PlaidBankAccount) { - const commandName = 'AddPersonalBankAccount'; - type AddPersonalBankAccountParams = { addressName: string; routingNumber: string; @@ -213,7 +209,7 @@ function addPersonalBankAccount(account: PlaidBankAccount) { ], }; - API.write(commandName, parameters, onyxData); + API.write(WRITE_COMMANDS.ADD_PERSONAL_BANK_ACCOUNT, parameters, onyxData); } function deletePaymentBankAccount(bankAccountID: number) { @@ -241,7 +237,7 @@ function deletePaymentBankAccount(bankAccountID: number) { ], }; - API.write('DeletePaymentBankAccount', parameters, onyxData); + API.write(WRITE_COMMANDS.DELETE_PAYMENT_BANK_ACCOUNT, parameters, onyxData); } /** @@ -250,7 +246,7 @@ function deletePaymentBankAccount(bankAccountID: number) { * This action is called by the requestor step in the Verified Bank Account flow */ function updatePersonalInformationForBankAccount(params: RequestorStepProps) { - API.write('UpdatePersonalInformationForBankAccount', params, getVBBADataForOnyx(CONST.BANK_ACCOUNT.STEP.REQUESTOR)); + API.write(WRITE_COMMANDS.UPDATE_PERSONAL_INFORMATION_FOR_BANK_ACCOUNT, params, getVBBADataForOnyx(CONST.BANK_ACCOUNT.STEP.REQUESTOR)); } function validateBankAccount(bankAccountID: number, validateCode: string) { @@ -295,7 +291,7 @@ function validateBankAccount(bankAccountID: number, validateCode: string) { ], }; - API.write('ValidateBankAccountWithTransactions', parameters, onyxData); + API.write(WRITE_COMMANDS.VALIDATE_BANK_ACCOUNT_WITH_TRANSACTIONS, parameters, onyxData); } function clearReimbursementAccount() { @@ -350,14 +346,14 @@ function updateCompanyInformationForBankAccount(bankAccount: BankAccountCompanyI const parameters: UpdateCompanyInformationForBankAccountParams = {...bankAccount, policyID}; - API.write('UpdateCompanyInformationForBankAccount', parameters, getVBBADataForOnyx(CONST.BANK_ACCOUNT.STEP.COMPANY)); + API.write(WRITE_COMMANDS.UPDATE_COMPANY_INFORMATION_FOR_BANK_ACCOUNT, parameters, getVBBADataForOnyx(CONST.BANK_ACCOUNT.STEP.COMPANY)); } /** * Add beneficial owners for the bank account, accept the ACH terms and conditions and verify the accuracy of the information provided */ function updateBeneficialOwnersForBankAccount(params: ACHContractStepProps) { - API.write('UpdateBeneficialOwnersForBankAccount', params, getVBBADataForOnyx()); + API.write(WRITE_COMMANDS.UPDATE_BENEFICIAL_OWNERS_FOR_BANK_ACCOUNT, params, getVBBADataForOnyx()); } /** @@ -379,7 +375,7 @@ function connectBankAccountManually(bankAccountID: number, accountNumber?: strin plaidMask, }; - API.write('ConnectBankAccountManually', parameters, getVBBADataForOnyx(CONST.BANK_ACCOUNT.STEP.BANK_ACCOUNT)); + API.write(WRITE_COMMANDS.CONNECT_BANK_ACCOUNT_MANUALLY, parameters, getVBBADataForOnyx(CONST.BANK_ACCOUNT.STEP.BANK_ACCOUNT)); } /** @@ -396,7 +392,7 @@ function verifyIdentityForBankAccount(bankAccountID: number, onfidoData: OnfidoD onfidoData: JSON.stringify(onfidoData), }; - API.write('VerifyIdentityForBankAccount', parameters, getVBBADataForOnyx()); + API.write(WRITE_COMMANDS.VERIFY_IDENTITY_FOR_BANK_ACCOUNT, parameters, getVBBADataForOnyx()); } function openWorkspaceView() { @@ -450,7 +446,7 @@ function handlePlaidError(bankAccountID: number, error: string, errorDescription plaidRequestID, }; - API.write('BankAccount_HandlePlaidError', parameters); + API.write(WRITE_COMMANDS.BANK_ACCOUNT_HANDLE_PLAID_ERROR, parameters); } /** diff --git a/src/libs/actions/Card.ts b/src/libs/actions/Card.ts index 0d583001ddc9..7d277a2f170d 100644 --- a/src/libs/actions/Card.ts +++ b/src/libs/actions/Card.ts @@ -2,7 +2,7 @@ import Onyx from 'react-native-onyx'; import type {OnyxUpdate} from 'react-native-onyx'; import * as API from '@libs/API'; import type {RevealExpensifyCardDetailsParams} from '@libs/API/parameters'; -import {SIDE_EFFECT_REQUEST_COMMANDS} from '@libs/API/types'; +import {SIDE_EFFECT_REQUEST_COMMANDS, WRITE_COMMANDS} from '@libs/API/types'; import * as Localize from '@libs/Localize'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -49,7 +49,7 @@ function reportVirtualExpensifyCardFraud(cardID: number) { cardID, }; - API.write('ReportVirtualExpensifyCardFraud', parameters, {optimisticData, successData, failureData}); + API.write(WRITE_COMMANDS.REPORT_VIRTUAL_EXPENSIFY_CARD_FRAUD, parameters, {optimisticData, successData, failureData}); } /** @@ -99,7 +99,7 @@ function requestReplacementExpensifyCard(cardId: number, reason: ReplacementReas reason, }; - API.write('RequestReplacementExpensifyCard', parameters, {optimisticData, successData, failureData}); + API.write(WRITE_COMMANDS.REQUEST_REPLACEMENT_EXPENSIFY_CARD, parameters, {optimisticData, successData, failureData}); } /** @@ -153,7 +153,7 @@ function activatePhysicalExpensifyCard(cardLastFourDigits: string, cardID: numbe cardID, }; - API.write('ActivatePhysicalExpensifyCard', parameters, {optimisticData, successData, failureData}); + API.write(WRITE_COMMANDS.ACTIVATE_PHYSICAL_EXPENSIFY_CARD, parameters, {optimisticData, successData, failureData}); } /** diff --git a/src/libs/actions/Chronos.ts b/src/libs/actions/Chronos.ts index 0bb949687e6d..e49ace95ce5c 100644 --- a/src/libs/actions/Chronos.ts +++ b/src/libs/actions/Chronos.ts @@ -1,6 +1,7 @@ import type {OnyxUpdate} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import * as API from '@libs/API'; +import {WRITE_COMMANDS} from '@libs/API/types'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {ChronosOOOEvent} from '@src/types/onyx/OriginalMessage'; @@ -47,7 +48,7 @@ const removeEvent = (reportID: string, reportActionID: string, eventID: string, ]; API.write( - 'Chronos_RemoveOOOEvent', + WRITE_COMMANDS.CHRONOS_REMOVE_OOO_EVENT, { googleEventID: eventID, reportActionID, diff --git a/src/libs/actions/PaymentMethods.ts b/src/libs/actions/PaymentMethods.ts index c3a19073eb63..2d8f11e40a0b 100644 --- a/src/libs/actions/PaymentMethods.ts +++ b/src/libs/actions/PaymentMethods.ts @@ -4,7 +4,7 @@ import Onyx from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx/lib/types'; import type {ValueOf} from 'type-fest'; import * as API from '@libs/API'; -import {READ_COMMANDS} from '@libs/API/types'; +import {READ_COMMANDS, WRITE_COMMANDS} from '@libs/API/types'; import * as CardUtils from '@libs/CardUtils'; import Navigation from '@libs/Navigation/Navigation'; import CONST from '@src/CONST'; @@ -142,7 +142,7 @@ function makeDefaultPaymentMethod(bankAccountID: number, fundID: number, previou fundID, }; - API.write('MakeDefaultPaymentMethod', parameters, { + API.write(WRITE_COMMANDS.MAKE_DEFAULT_PAYMENT_METHOD, parameters, { optimisticData: getMakeDefaultPaymentOnyxData(bankAccountID, fundID, previousPaymentMethod, currentPaymentMethod, true), failureData: getMakeDefaultPaymentOnyxData(bankAccountID, fundID, previousPaymentMethod, currentPaymentMethod, false), }); @@ -204,7 +204,7 @@ function addPaymentCard(params: PaymentCardParams) { }, ]; - API.write('AddPaymentCard', parameters, { + API.write(WRITE_COMMANDS.ADD_PAYMENT_CARD, parameters, { optimisticData, successData, failureData, @@ -270,7 +270,7 @@ function transferWalletBalance(paymentMethod: PaymentMethod) { }, ]; - API.write('TransferWalletBalance', parameters, { + API.write(WRITE_COMMANDS.TRANSFER_WALLET_BALANCE, parameters, { optimisticData, successData, failureData, @@ -372,7 +372,7 @@ function deletePaymentCard(fundID: number) { }, ]; - API.write('DeletePaymentCard', parameters, { + API.write(WRITE_COMMANDS.DELETE_PAYMENT_CARD, parameters, { optimisticData, }); } diff --git a/src/libs/actions/PersonalDetails.ts b/src/libs/actions/PersonalDetails.ts index 730dd682ebf5..29bd91bdd067 100644 --- a/src/libs/actions/PersonalDetails.ts +++ b/src/libs/actions/PersonalDetails.ts @@ -3,7 +3,7 @@ import type {OnyxEntry, OnyxUpdate} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import * as API from '@libs/API'; import type {OpenPublicProfilePageParams} from '@libs/API/parameters'; -import {READ_COMMANDS} from '@libs/API/types'; +import {READ_COMMANDS, WRITE_COMMANDS} from '@libs/API/types'; import type {CustomRNImageManipulatorResult} from '@libs/cropOrRotateImage/types'; import DateUtils from '@libs/DateUtils'; import * as LocalePhoneNumber from '@libs/LocalePhoneNumber'; @@ -115,7 +115,7 @@ function updatePronouns(pronouns: string) { const parameters: UpdatePronounsParams = {pronouns}; - API.write('UpdatePronouns', parameters, { + API.write(WRITE_COMMANDS.UPDATE_PRONOUNS, parameters, { optimisticData: [ { onyxMethod: Onyx.METHOD.MERGE, @@ -142,7 +142,7 @@ function updateDisplayName(firstName: string, lastName: string) { const parameters: UpdateDisplayNameParams = {firstName, lastName}; - API.write('UpdateDisplayName', parameters, { + API.write(WRITE_COMMANDS.UPDATE_DISPLAY_NAME, parameters, { optimisticData: [ { onyxMethod: Onyx.METHOD.MERGE, @@ -173,7 +173,7 @@ function updateLegalName(legalFirstName: string, legalLastName: string) { const parameters: UpdateLegalNameParams = {legalFirstName, legalLastName}; - API.write('UpdateLegalName', parameters, { + API.write(WRITE_COMMANDS.UPDATE_LEGAL_NAME, parameters, { optimisticData: [ { onyxMethod: Onyx.METHOD.MERGE, @@ -199,7 +199,7 @@ function updateDateOfBirth({dob}: DateOfBirthForm) { const parameters: UpdateDateOfBirthParams = {dob}; - API.write('UpdateDateOfBirth', parameters, { + API.write(WRITE_COMMANDS.UPDATE_DATE_OF_BIRTH, parameters, { optimisticData: [ { onyxMethod: Onyx.METHOD.MERGE, @@ -240,7 +240,7 @@ function updateAddress(street: string, street2: string, city: string, state: str parameters.addressStateLong = state; } - API.write('UpdateHomeAddress', parameters, { + API.write(WRITE_COMMANDS.UPDATE_HOME_ADDRESS, parameters, { optimisticData: [ { onyxMethod: Onyx.METHOD.MERGE, @@ -282,7 +282,7 @@ function updateAutomaticTimezone(timezone: Timezone) { timezone: JSON.stringify(formatedTimezone), }; - API.write('UpdateAutomaticTimezone', parameters, { + API.write(WRITE_COMMANDS.UPDATE_AUTOMATIC_TIMEZONE, parameters, { optimisticData: [ { onyxMethod: Onyx.METHOD.MERGE, @@ -315,7 +315,7 @@ function updateSelectedTimezone(selectedTimezone: SelectedTimezone) { }; if (currentUserAccountID) { - API.write('UpdateSelectedTimezone', parameters, { + API.write(WRITE_COMMANDS.UPDATE_SELECTED_TIMEZONE, parameters, { optimisticData: [ { onyxMethod: Onyx.METHOD.MERGE, @@ -481,7 +481,7 @@ function updateAvatar(file: File | CustomRNImageManipulatorResult) { const parameters: UpdateUserAvatarParams = {file}; - API.write('UpdateUserAvatar', parameters, {optimisticData, successData, failureData}); + API.write(WRITE_COMMANDS.UPDATE_USER_AVATAR, parameters, {optimisticData, successData, failureData}); } /** @@ -524,7 +524,7 @@ function deleteAvatar() { const parameters: DeleteUserAvatarParams = {}; - API.write('DeleteUserAvatar', parameters, {optimisticData, failureData}); + API.write(WRITE_COMMANDS.DELETE_USER_AVATAR, parameters, {optimisticData, failureData}); } /** diff --git a/src/libs/actions/PushNotification.ts b/src/libs/actions/PushNotification.ts index 888892fdc188..bc4d4eb05c5a 100644 --- a/src/libs/actions/PushNotification.ts +++ b/src/libs/actions/PushNotification.ts @@ -1,5 +1,6 @@ import Onyx from 'react-native-onyx'; import * as API from '@libs/API'; +import {WRITE_COMMANDS} from '@libs/API/types'; import ONYXKEYS from '@src/ONYXKEYS'; import * as Device from './Device'; @@ -19,7 +20,7 @@ Onyx.connect({ */ function setPushNotificationOptInStatus(isOptingIn: boolean) { Device.getDeviceID().then((deviceID) => { - const commandName = isOptingIn ? 'OptInToPushNotifications' : 'OptOutOfPushNotifications'; + const commandName = isOptingIn ? WRITE_COMMANDS.OPT_IN_TO_PUSH_NOTIFICATIONS : WRITE_COMMANDS.OPT_OUT_OF_PUSH_NOTIFICATIONS; const optimisticData = [ { onyxMethod: Onyx.METHOD.MERGE, diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 20e523d3e5e0..1da62e5d6702 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -19,7 +19,7 @@ import type { OpenRoomMembersPageParams, SearchForReportsParams, } from '@libs/API/parameters'; -import {READ_COMMANDS, SIDE_EFFECT_REQUEST_COMMANDS} from '@libs/API/types'; +import {READ_COMMANDS, SIDE_EFFECT_REQUEST_COMMANDS, WRITE_COMMANDS} from '@libs/API/types'; import * as CollectionUtils from '@libs/CollectionUtils'; import DateUtils from '@libs/DateUtils'; import * as EmojiUtils from '@libs/EmojiUtils'; @@ -298,7 +298,7 @@ function addActions(reportID: string, text = '', file?: File) { let reportCommentText = ''; let reportCommentAction: Partial | undefined; let attachmentAction: Partial | undefined; - let commandName = 'AddComment'; + let commandName: typeof WRITE_COMMANDS.ADD_COMMENT | typeof WRITE_COMMANDS.ADD_ATTACHMENT = WRITE_COMMANDS.ADD_COMMENT; if (text) { const reportComment = ReportUtils.buildOptimisticAddCommentReportAction(text); @@ -309,7 +309,7 @@ function addActions(reportID: string, text = '', file?: File) { if (file) { // When we are adding an attachment we will call AddAttachment. // It supports sending an attachment with an optional comment and AddComment supports adding a single text comment only. - commandName = 'AddAttachment'; + commandName = WRITE_COMMANDS.ADD_ATTACHMENT; const attachment = ReportUtils.buildOptimisticAddCommentReportAction('', file); attachmentAction = attachment.reportAction; } @@ -666,7 +666,7 @@ function openReport( }); } else { // eslint-disable-next-line rulesdir/no-multiple-api-calls - API.write(SIDE_EFFECT_REQUEST_COMMANDS.OPEN_REPORT, parameters, {optimisticData, successData, failureData}); + API.write(WRITE_COMMANDS.OPEN_REPORT, parameters, {optimisticData, successData, failureData}); } } @@ -798,7 +798,7 @@ function reconnect(reportID: string) { reportID, }; - API.write('ReconnectToReport', parameters, {optimisticData, successData, failureData}); + API.write(WRITE_COMMANDS.RECONNECT_TO_REPORT, parameters, {optimisticData, successData, failureData}); } /** @@ -923,7 +923,7 @@ function readNewestAction(reportID: string) { lastReadTime, }; - API.write('ReadNewestAction', parameters, {optimisticData}); + API.write(WRITE_COMMANDS.READ_NEWEST_ACTION, parameters, {optimisticData}); DeviceEventEmitter.emit(`readNewestAction_${reportID}`, lastReadTime); } @@ -969,7 +969,7 @@ function markCommentAsUnread(reportID: string, reportActionCreated: string) { lastReadTime, }; - API.write('MarkAsUnread', parameters, {optimisticData}); + API.write(WRITE_COMMANDS.MARK_AS_UNREAD, parameters, {optimisticData}); DeviceEventEmitter.emit(`unreadAction_${reportID}`, lastReadTime); } @@ -996,7 +996,7 @@ function togglePinnedState(reportID: string, isPinnedChat: boolean) { pinnedValue, }; - API.write('TogglePinnedChat', parameters, {optimisticData}); + API.write(WRITE_COMMANDS.TOGGLE_PINNED_CHAT, parameters, {optimisticData}); } /** @@ -1189,7 +1189,7 @@ function deleteReportComment(reportID: string, reportAction: ReportAction) { reportActionID, }; - API.write('DeleteComment', parameters, {optimisticData, successData, failureData}); + API.write(WRITE_COMMANDS.DELETE_COMMENT, parameters, {optimisticData, successData, failureData}); } /** @@ -1344,7 +1344,7 @@ function editReportComment(reportID: string, originalReportAction: OnyxEntry, se isDevRequest: Environment.isDevelopment(), }; - API.write('FlagComment', parameters, {optimisticData, successData, failureData}); + API.write(WRITE_COMMANDS.FLAG_COMMENT, parameters, {optimisticData, successData, failureData}); } /** Updates a given user's private notes on a report */ @@ -2420,7 +2420,7 @@ const updatePrivateNotes = (reportID: string, accountID: number, note: string) = const parameters: UpdateReportPrivateNoteParameters = {reportID, privateNotes: note}; - API.write('UpdateReportPrivateNote', parameters, {optimisticData, successData, failureData}); + API.write(WRITE_COMMANDS.UPDATE_REPORT_PRIVATE_NOTE, parameters, {optimisticData, successData, failureData}); }; /** Fetches all the private notes for a given report */ @@ -2608,7 +2608,7 @@ function resolveActionableMentionWhisper(reportId: string, reportAction: OnyxEnt resolution, }; - API.write('ResolveActionableMentionWhisper', parameters, {optimisticData, failureData}); + API.write(WRITE_COMMANDS.RESOLVE_ACTIONABLE_MENTION_WHISPER, parameters, {optimisticData, failureData}); } export { diff --git a/src/libs/actions/Session/index.ts b/src/libs/actions/Session/index.ts index 7685a242f42a..e34ca26743d4 100644 --- a/src/libs/actions/Session/index.ts +++ b/src/libs/actions/Session/index.ts @@ -8,7 +8,7 @@ import type {ValueOf} from 'type-fest'; import * as PersistedRequests from '@libs/actions/PersistedRequests'; import * as API from '@libs/API'; import type {AuthenticatePusherParams, BeginSignInParams, SignInWithShortLivedAuthTokenParams} from '@libs/API/parameters'; -import {READ_COMMANDS, SIDE_EFFECT_REQUEST_COMMANDS} from '@libs/API/types'; +import {READ_COMMANDS, SIDE_EFFECT_REQUEST_COMMANDS, WRITE_COMMANDS} from '@libs/API/types'; import * as Authentication from '@libs/Authentication'; import * as ErrorUtils from '@libs/ErrorUtils'; import HttpUtils from '@libs/HttpUtils'; @@ -89,7 +89,7 @@ function signOut() { shouldRetry: false, }; - API.write('LogOut', params); + API.write(WRITE_COMMANDS.LOG_OUT, params); clearCache().then(() => { Log.info('Cleared all cache data', true, {}, true); }); @@ -185,7 +185,7 @@ function resendValidationLink(login = credentials.login) { const params: ResendValidationLinkParams = {email: login}; - API.write('RequestAccountValidationLink', params, {optimisticData, successData, failureData}); + API.write(WRITE_COMMANDS.REQUEST_ACCOUNT_VALIDATION_LINK, params, {optimisticData, successData, failureData}); } /** @@ -218,7 +218,7 @@ function resendValidateCode(login = credentials.login) { const params: RequestNewValidateCodeParams = {email: login}; - API.write('RequestNewValidateCode', params, {optimisticData, finallyData}); + API.write(WRITE_COMMANDS.REQUEST_NEW_VALIDATE_CODE, params, {optimisticData, finallyData}); } type OnyxData = { @@ -300,7 +300,7 @@ function beginAppleSignIn(idToken: string | undefined | null) { const params: BeginAppleSignInParams = {idToken, preferredLocale}; - API.write('SignInWithApple', params, {optimisticData, successData, failureData}); + API.write(WRITE_COMMANDS.SIGN_IN_WITH_APPLE, params, {optimisticData, successData, failureData}); } /** @@ -317,7 +317,7 @@ function beginGoogleSignIn(token: string | null) { const params: BeginGoogleSignInParams = {token, preferredLocale}; - API.write('SignInWithGoogle', params, {optimisticData, successData, failureData}); + API.write(WRITE_COMMANDS.SIGN_IN_WITH_GOOGLE, params, {optimisticData, successData, failureData}); } /** @@ -446,7 +446,7 @@ function signIn(validateCode: string, twoFactorAuthCode?: string) { params.validateCode = validateCode || credentials.validateCode; } - API.write('SigninUser', params, {optimisticData, successData, failureData}); + API.write(WRITE_COMMANDS.SIGN_IN_USER, params, {optimisticData, successData, failureData}); }); } @@ -528,7 +528,7 @@ function signInWithValidateCode(accountID: number, code: string, twoFactorAuthCo deviceInfo, }; - API.write('SigninUserWithLink', params, {optimisticData, successData, failureData}); + API.write(WRITE_COMMANDS.SIGN_IN_USER_WITH_LINK, params, {optimisticData, successData, failureData}); }); } @@ -738,7 +738,7 @@ function requestUnlinkValidationLink() { const params: RequestUnlinkValidationLinkParams = {email: credentials.login}; - API.write('RequestUnlinkValidationLink', params, {optimisticData, successData, failureData}); + API.write(WRITE_COMMANDS.REQUEST_UNLINK_VALIDATION_LINK, params, {optimisticData, successData, failureData}); } function unlinkLogin(accountID: number, validateCode: string) { @@ -789,7 +789,7 @@ function unlinkLogin(accountID: number, validateCode: string) { validateCode, }; - API.write('UnlinkLogin', params, { + API.write(WRITE_COMMANDS.UNLINK_LOGIN, params, { optimisticData, successData, failureData, diff --git a/src/libs/actions/TeachersUnite.ts b/src/libs/actions/TeachersUnite.ts index 14b1a6455349..140f7a96d863 100644 --- a/src/libs/actions/TeachersUnite.ts +++ b/src/libs/actions/TeachersUnite.ts @@ -1,6 +1,7 @@ import Onyx from 'react-native-onyx'; import type {OnyxEntry, OnyxUpdate} from 'react-native-onyx'; import * as API from '@libs/API'; +import {WRITE_COMMANDS} from '@libs/API/types'; import Navigation from '@libs/Navigation/Navigation'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as ReportUtils from '@libs/ReportUtils'; @@ -65,7 +66,7 @@ function referTeachersUniteVolunteer(partnerUserID: string, firstName: string, l partnerUserID, }; - API.write('ReferTeachersUniteVolunteer', parameters, {optimisticData}); + API.write(WRITE_COMMANDS.REFER_TEACHERS_UNITE_VOLUNTEER, parameters, {optimisticData}); Navigation.dismissModal(publicRoomReportID); } @@ -193,7 +194,7 @@ function addSchoolPrincipal(firstName: string, partnerUserID: string, lastName: reportCreationData: JSON.stringify(reportCreationData), }; - API.write('AddSchoolPrincipal', parameters, {optimisticData, successData, failureData}); + API.write(WRITE_COMMANDS.ADD_SCHOOL_PRINCIPAL, parameters, {optimisticData, successData, failureData}); Navigation.dismissModal(expenseChatReportID); } diff --git a/src/libs/actions/User.ts b/src/libs/actions/User.ts index f8d0a407703f..484e3612d048 100644 --- a/src/libs/actions/User.ts +++ b/src/libs/actions/User.ts @@ -5,7 +5,7 @@ import type {OnyxEntry} from 'react-native-onyx/lib/types'; import type {ValueOf} from 'type-fest'; import * as API from '@libs/API'; import type {GetStatementPDFParams} from '@libs/API/parameters'; -import {READ_COMMANDS} from '@libs/API/types'; +import {READ_COMMANDS, WRITE_COMMANDS} from '@libs/API/types'; import * as ErrorUtils from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; import * as SequentialQueue from '@libs/Network/SequentialQueue'; @@ -79,7 +79,7 @@ function closeAccount(reason: string) { const parameters: CloseAccountParams = {message: reason}; - API.write('CloseAccount', parameters, { + API.write(WRITE_COMMANDS.CLOSE_ACCOUNT, parameters, { optimisticData, failureData, }); @@ -153,7 +153,7 @@ function requestContactMethodValidateCode(contactMethod: string) { const parameters: RequestContactMethodValidateCodeParams = {email: contactMethod}; - API.write('RequestContactMethodValidateCode', parameters, {optimisticData, successData, failureData}); + API.write(WRITE_COMMANDS.REQUEST_CONTACT_METHOD_VALIDATE_CODE, parameters, {optimisticData, successData, failureData}); } /** @@ -179,7 +179,7 @@ function updateNewsletterSubscription(isSubscribed: boolean) { const parameters: UpdateNewsletterSubscriptionParams = {isSubscribed}; - API.write('UpdateNewsletterSubscription', parameters, { + API.write(WRITE_COMMANDS.UPDATE_NEWSLETTER_SUBSCRIPTION, parameters, { optimisticData, failureData, }); @@ -241,7 +241,7 @@ function deleteContactMethod(contactMethod: string, loginList: Record, autom automatic, }; - API.write('UpdateChatPriorityMode', parameters, {optimisticData}); + API.write(WRITE_COMMANDS.UPDATE_CHAT_PRIORITY_MODE, parameters, {optimisticData}); if (!autoSwitchedToFocusMode) { Navigation.goBack(ROUTES.SETTINGS_PREFERENCES); @@ -774,7 +774,7 @@ function setContactMethodAsDefault(newDefaultContactMethod: string) { partnerUserID: newDefaultContactMethod, }; - API.write('SetContactMethodAsDefault', parameters, { + API.write(WRITE_COMMANDS.SET_CONTACT_METHOD_AS_DEFAULT, parameters, { optimisticData, successData, failureData, @@ -799,7 +799,7 @@ function updateTheme(theme: ValueOf) { value: theme, }; - API.write('UpdateTheme', parameters, {optimisticData}); + API.write(WRITE_COMMANDS.UPDATE_THEME, parameters, {optimisticData}); Navigation.navigate(ROUTES.SETTINGS_PREFERENCES); } @@ -828,7 +828,7 @@ function updateCustomStatus(status: Status) { const parameters: UpdateStatusParams = {text: status.text, emojiCode: status.emojiCode, clearAfter: status.clearAfter}; - API.write('UpdateStatus', parameters, { + API.write(WRITE_COMMANDS.UPDATE_STATUS, parameters, { optimisticData, }); } @@ -848,7 +848,7 @@ function clearCustomStatus() { }, }, ]; - API.write('ClearStatus', undefined, { + API.write(WRITE_COMMANDS.CLEAR_STATUS, undefined, { optimisticData, }); } diff --git a/src/libs/actions/Wallet.ts b/src/libs/actions/Wallet.ts index b61c1816eb7e..44d30fd01b83 100644 --- a/src/libs/actions/Wallet.ts +++ b/src/libs/actions/Wallet.ts @@ -2,7 +2,7 @@ import type {OnyxUpdate} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import * as API from '@libs/API'; -import {READ_COMMANDS} from '@libs/API/types'; +import {READ_COMMANDS, WRITE_COMMANDS} from '@libs/API/types'; import type {PrivatePersonalDetails} from '@libs/GetPhysicalCardUtils'; import type CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -112,7 +112,7 @@ function updatePersonalDetails(personalDetails: PersonalDetails) { }, ]; - API.write('UpdatePersonalDetailsForWallet', personalDetails, { + API.write(WRITE_COMMANDS.UPDATE_PERSONAL_DETAILS_FOR_WALLET, personalDetails, { optimisticData, finallyData, }); @@ -165,7 +165,7 @@ function verifyIdentity(parameters: IdentityVerification) { }, }, ]; - API.write('VerifyIdentity', parameters, { + API.write(WRITE_COMMANDS.VERIFY_IDENTITY, parameters, { optimisticData, successData, failureData, @@ -219,7 +219,7 @@ function acceptWalletTerms(parameters: WalletTerms) { const requestParams: WalletTerms = {hasAcceptedTerms: parameters.hasAcceptedTerms, reportID: parameters.reportID}; - API.write('AcceptWalletTerms', requestParams, {optimisticData, successData, failureData}); + API.write(WRITE_COMMANDS.ACCEPT_WALLET_TERMS, requestParams, {optimisticData, successData, failureData}); } /** @@ -273,7 +273,7 @@ function answerQuestionsForWallet(answers: WalletQuestionAnswer[], idNumber: str idNumber, }; - API.write('AnswerQuestionsForWallet', requestParams, { + API.write(WRITE_COMMANDS.ANSWER_QUESTIONS_FOR_WALLET, requestParams, { optimisticData, finallyData, }); @@ -328,7 +328,7 @@ function requestPhysicalExpensifyCard(cardID: number, authToken: string, private }, ]; - API.write('RequestPhysicalExpensifyCard', requestParams, {optimisticData}); + API.write(WRITE_COMMANDS.REQUEST_PHYSICAL_EXPENSIFY_CARD, requestParams, {optimisticData}); } export { From 25e962aed59418d5611d4875bf88af9991e82568 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Tue, 16 Jan 2024 18:16:27 +0100 Subject: [PATCH 252/580] Remove all Params types --- src/libs/actions/BankAccounts.ts | 48 +----------------- src/libs/actions/Card.ts | 14 ------ src/libs/actions/PaymentMethods.ts | 22 --------- src/libs/actions/PersonalDetails.ts | 39 --------------- src/libs/actions/Report.ts | 5 -- src/libs/actions/Session/index.ts | 55 --------------------- src/libs/actions/TeachersUnite.ts | 15 ------ src/libs/actions/User.ts | 41 --------------- src/libs/actions/Wallet.ts | 12 ----- src/types/onyx/ReimbursementAccountDraft.ts | 14 +----- 10 files changed, 2 insertions(+), 263 deletions(-) diff --git a/src/libs/actions/BankAccounts.ts b/src/libs/actions/BankAccounts.ts index 6b5882d76d1a..4361dec74da1 100644 --- a/src/libs/actions/BankAccounts.ts +++ b/src/libs/actions/BankAccounts.ts @@ -125,15 +125,6 @@ function getVBBADataForOnyx(currentStep?: BankAccountStep): OnyxData { * Submit Bank Account step with Plaid data so php can perform some checks. */ function connectBankAccountWithPlaid(bankAccountID: number, selectedPlaidBankAccount: PlaidBankAccount) { - type ConnectBankAccountWithPlaidParams = { - bankAccountID: number; - routingNumber: string; - accountNumber: string; - bank?: string; - plaidAccountID: string; - plaidAccessToken: string; - }; - const parameters: ConnectBankAccountWithPlaidParams = { bankAccountID, routingNumber: selectedPlaidBankAccount.routingNumber, @@ -152,17 +143,6 @@ function connectBankAccountWithPlaid(bankAccountID: number, selectedPlaidBankAcc * TODO: offline pattern for this command will have to be added later once the pattern B design doc is complete */ function addPersonalBankAccount(account: PlaidBankAccount) { - type AddPersonalBankAccountParams = { - addressName: string; - routingNumber: string; - accountNumber: string; - isSavings: boolean; - setupType: string; - bank?: string; - plaidAccountID: string; - plaidAccessToken: string; - }; - const parameters: AddPersonalBankAccountParams = { addressName: account.addressName, routingNumber: account.routingNumber, @@ -213,8 +193,6 @@ function addPersonalBankAccount(account: PlaidBankAccount) { } function deletePaymentBankAccount(bankAccountID: number) { - type DeletePaymentBankAccountParams = {bankAccountID: number}; - const parameters: DeletePaymentBankAccountParams = {bankAccountID}; const onyxData: OnyxData = { @@ -245,16 +223,11 @@ function deletePaymentBankAccount(bankAccountID: number) { * * This action is called by the requestor step in the Verified Bank Account flow */ -function updatePersonalInformationForBankAccount(params: RequestorStepProps) { +function updatePersonalInformationForBankAccount(params: UpdatePersonalInformationForBankAccountParams) { API.write(WRITE_COMMANDS.UPDATE_PERSONAL_INFORMATION_FOR_BANK_ACCOUNT, params, getVBBADataForOnyx(CONST.BANK_ACCOUNT.STEP.REQUESTOR)); } function validateBankAccount(bankAccountID: number, validateCode: string) { - type ValidateBankAccountWithTransactionsParams = { - bankAccountID: number; - validateCode: string; - }; - const parameters: ValidateBankAccountWithTransactionsParams = { bankAccountID, validateCode, @@ -361,13 +334,6 @@ function updateBeneficialOwnersForBankAccount(params: ACHContractStepProps) { * */ function connectBankAccountManually(bankAccountID: number, accountNumber?: string, routingNumber?: string, plaidMask?: string) { - type ConnectBankAccountManuallyParams = { - bankAccountID: number; - accountNumber?: string; - routingNumber?: string; - plaidMask?: string; - }; - const parameters: ConnectBankAccountManuallyParams = { bankAccountID, accountNumber, @@ -382,11 +348,6 @@ function connectBankAccountManually(bankAccountID: number, accountNumber?: strin * Verify the user's identity via Onfido */ function verifyIdentityForBankAccount(bankAccountID: number, onfidoData: OnfidoData) { - type VerifyIdentityForBankAccountParams = { - bankAccountID: number; - onfidoData: string; - }; - const parameters: VerifyIdentityForBankAccountParams = { bankAccountID, onfidoData: JSON.stringify(onfidoData), @@ -432,13 +393,6 @@ function openWorkspaceView() { } function handlePlaidError(bankAccountID: number, error: string, errorDescription: string, plaidRequestID: string) { - type BankAccountHandlePlaidErrorParams = { - bankAccountID: number; - error: string; - errorDescription: string; - plaidRequestID: string; - }; - const parameters: BankAccountHandlePlaidErrorParams = { bankAccountID, error, diff --git a/src/libs/actions/Card.ts b/src/libs/actions/Card.ts index 7d277a2f170d..018a748e664f 100644 --- a/src/libs/actions/Card.ts +++ b/src/libs/actions/Card.ts @@ -41,10 +41,6 @@ function reportVirtualExpensifyCardFraud(cardID: number) { }, ]; - type ReportVirtualExpensifyCardFraudParams = { - cardID: number; - }; - const parameters: ReportVirtualExpensifyCardFraudParams = { cardID, }; @@ -89,11 +85,6 @@ function requestReplacementExpensifyCard(cardId: number, reason: ReplacementReas }, ]; - type RequestReplacementExpensifyCardParams = { - cardId: number; - reason: string; - }; - const parameters: RequestReplacementExpensifyCardParams = { cardId, reason, @@ -143,11 +134,6 @@ function activatePhysicalExpensifyCard(cardLastFourDigits: string, cardID: numbe }, ]; - type ActivatePhysicalExpensifyCardParams = { - cardLastFourDigits: string; - cardID: number; - }; - const parameters: ActivatePhysicalExpensifyCardParams = { cardLastFourDigits, cardID, diff --git a/src/libs/actions/PaymentMethods.ts b/src/libs/actions/PaymentMethods.ts index 2d8f11e40a0b..60a1b983fbe1 100644 --- a/src/libs/actions/PaymentMethods.ts +++ b/src/libs/actions/PaymentMethods.ts @@ -132,11 +132,6 @@ function getMakeDefaultPaymentOnyxData( * */ function makeDefaultPaymentMethod(bankAccountID: number, fundID: number, previousPaymentMethod: PaymentMethod, currentPaymentMethod: PaymentMethod) { - type MakeDefaultPaymentMethodParams = { - bankAccountID: number; - fundID: number; - }; - const parameters: MakeDefaultPaymentMethodParams = { bankAccountID, fundID, @@ -148,8 +143,6 @@ function makeDefaultPaymentMethod(bankAccountID: number, fundID: number, previou }); } -type PaymentCardParams = {expirationDate: string; cardNumber: string; securityCode: string; nameOnCard: string; addressZipCode: string}; - /** * Calls the API to add a new card. * @@ -158,17 +151,6 @@ function addPaymentCard(params: PaymentCardParams) { const cardMonth = CardUtils.getMonthFromExpirationDateString(params.expirationDate); const cardYear = CardUtils.getYearFromExpirationDateString(params.expirationDate); - type AddPaymentCardParams = { - cardNumber: string; - cardYear: string; - cardMonth: string; - cardCVV: string; - addressName: string; - addressZip: string; - currency: ValueOf; - isP2PDebitCard: boolean; - }; - const parameters: AddPaymentCardParams = { cardNumber: params.cardNumber, cardYear, @@ -356,10 +338,6 @@ function clearWalletTermsError() { } function deletePaymentCard(fundID: number) { - type DeletePaymentCardParams = { - fundID: number; - }; - const parameters: DeletePaymentCardParams = { fundID, }; diff --git a/src/libs/actions/PersonalDetails.ts b/src/libs/actions/PersonalDetails.ts index 29bd91bdd067..de4ed0ab10fb 100644 --- a/src/libs/actions/PersonalDetails.ts +++ b/src/libs/actions/PersonalDetails.ts @@ -109,10 +109,6 @@ function getCountryISO(countryName: string): string { function updatePronouns(pronouns: string) { if (currentUserAccountID) { - type UpdatePronounsParams = { - pronouns: string; - }; - const parameters: UpdatePronounsParams = {pronouns}; API.write(WRITE_COMMANDS.UPDATE_PRONOUNS, parameters, { @@ -135,11 +131,6 @@ function updatePronouns(pronouns: string) { function updateDisplayName(firstName: string, lastName: string) { if (currentUserAccountID) { - type UpdateDisplayNameParams = { - firstName: string; - lastName: string; - }; - const parameters: UpdateDisplayNameParams = {firstName, lastName}; API.write(WRITE_COMMANDS.UPDATE_DISPLAY_NAME, parameters, { @@ -166,11 +157,6 @@ function updateDisplayName(firstName: string, lastName: string) { } function updateLegalName(legalFirstName: string, legalLastName: string) { - type UpdateLegalNameParams = { - legalFirstName: string; - legalLastName: string; - }; - const parameters: UpdateLegalNameParams = {legalFirstName, legalLastName}; API.write(WRITE_COMMANDS.UPDATE_LEGAL_NAME, parameters, { @@ -193,10 +179,6 @@ function updateLegalName(legalFirstName: string, legalLastName: string) { * @param dob - date of birth */ function updateDateOfBirth({dob}: DateOfBirthForm) { - type UpdateDateOfBirthParams = { - dob?: string; - }; - const parameters: UpdateDateOfBirthParams = {dob}; API.write(WRITE_COMMANDS.UPDATE_DATE_OF_BIRTH, parameters, { @@ -215,16 +197,6 @@ function updateDateOfBirth({dob}: DateOfBirthForm) { } function updateAddress(street: string, street2: string, city: string, state: string, zip: string, country: string) { - type UpdateHomeAddressParams = { - homeAddressStreet: string; - addressStreet2: string; - homeAddressCity: string; - addressState: string; - addressZipCode: string; - addressCountry: string; - addressStateLong?: string; - }; - const parameters: UpdateHomeAddressParams = { homeAddressStreet: street, addressStreet2: street2, @@ -274,9 +246,6 @@ function updateAutomaticTimezone(timezone: Timezone) { return; } - type UpdateAutomaticTimezoneParams = { - timezone: string; - }; const formatedTimezone = DateUtils.formatToSupportedTimezone(timezone); const parameters: UpdateAutomaticTimezoneParams = { timezone: JSON.stringify(formatedTimezone), @@ -306,10 +275,6 @@ function updateSelectedTimezone(selectedTimezone: SelectedTimezone) { selected: selectedTimezone, }; - type UpdateSelectedTimezoneParams = { - timezone: string; - }; - const parameters: UpdateSelectedTimezoneParams = { timezone: JSON.stringify(timezone), }; @@ -475,10 +440,6 @@ function updateAvatar(file: File | CustomRNImageManipulatorResult) { }, ]; - type UpdateUserAvatarParams = { - file: File | CustomRNImageManipulatorResult; - }; - const parameters: UpdateUserAvatarParams = {file}; API.write(WRITE_COMMANDS.UPDATE_USER_AVATAR, parameters, {optimisticData, successData, failureData}); diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 1da62e5d6702..5be36d9e8f01 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -2598,11 +2598,6 @@ function resolveActionableMentionWhisper(reportId: string, reportAction: OnyxEnt }, ]; - type ResolveActionableMentionWhisperParams = { - reportActionID: string; - resolution: ValueOf; - }; - const parameters: ResolveActionableMentionWhisperParams = { reportActionID: reportAction.reportActionID, resolution, diff --git a/src/libs/actions/Session/index.ts b/src/libs/actions/Session/index.ts index e34ca26743d4..328c6d7698bb 100644 --- a/src/libs/actions/Session/index.ts +++ b/src/libs/actions/Session/index.ts @@ -72,14 +72,6 @@ Onyx.connect({ function signOut() { Log.info('Flushing logs before signing out', true, {}, true); - type LogOutParams = { - authToken: string | null; - partnerUserID: string; - partnerName: string; - partnerPassword: string; - shouldRetry: boolean; - }; - const params: LogOutParams = { // Send current authToken because we will immediately clear it once triggering this command authToken: NetworkStore.getAuthToken(), @@ -179,10 +171,6 @@ function resendValidationLink(login = credentials.login) { }, ]; - type ResendValidationLinkParams = { - email?: string; - }; - const params: ResendValidationLinkParams = {email: login}; API.write(WRITE_COMMANDS.REQUEST_ACCOUNT_VALIDATION_LINK, params, {optimisticData, successData, failureData}); @@ -212,10 +200,6 @@ function resendValidateCode(login = credentials.login) { }, ]; - type RequestNewValidateCodeParams = { - email?: string; - }; - const params: RequestNewValidateCodeParams = {email: login}; API.write(WRITE_COMMANDS.REQUEST_NEW_VALIDATE_CODE, params, {optimisticData, finallyData}); @@ -293,11 +277,6 @@ function beginSignIn(email: string) { function beginAppleSignIn(idToken: string | undefined | null) { const {optimisticData, successData, failureData} = signInAttemptState(); - type BeginAppleSignInParams = { - idToken: typeof idToken; - preferredLocale: ValueOf | null; - }; - const params: BeginAppleSignInParams = {idToken, preferredLocale}; API.write(WRITE_COMMANDS.SIGN_IN_WITH_APPLE, params, {optimisticData, successData, failureData}); @@ -310,11 +289,6 @@ function beginAppleSignIn(idToken: string | undefined | null) { function beginGoogleSignIn(token: string | null) { const {optimisticData, successData, failureData} = signInAttemptState(); - type BeginGoogleSignInParams = { - token: string | null; - preferredLocale: ValueOf | null; - }; - const params: BeginGoogleSignInParams = {token, preferredLocale}; API.write(WRITE_COMMANDS.SIGN_IN_WITH_GOOGLE, params, {optimisticData, successData, failureData}); @@ -426,14 +400,6 @@ function signIn(validateCode: string, twoFactorAuthCode?: string) { ]; Device.getDeviceInfoWithID().then((deviceInfo) => { - type SignInUserParams = { - twoFactorAuthCode?: string; - email?: string; - preferredLocale: ValueOf | null; - validateCode?: string; - deviceInfo: string; - }; - const params: SignInUserParams = { twoFactorAuthCode, email: credentials.login, @@ -512,14 +478,6 @@ function signInWithValidateCode(accountID: number, code: string, twoFactorAuthCo }, ]; Device.getDeviceInfoWithID().then((deviceInfo) => { - type SignInUserWithLinkParams = { - accountID: number; - validateCode?: string; - twoFactorAuthCode?: string; - preferredLocale: ValueOf | null; - deviceInfo: string; - }; - const params: SignInUserWithLinkParams = { accountID, validateCode, @@ -732,10 +690,6 @@ function requestUnlinkValidationLink() { }, ]; - type RequestUnlinkValidationLinkParams = { - email?: string; - }; - const params: RequestUnlinkValidationLinkParams = {email: credentials.login}; API.write(WRITE_COMMANDS.REQUEST_UNLINK_VALIDATION_LINK, params, {optimisticData, successData, failureData}); @@ -779,11 +733,6 @@ function unlinkLogin(accountID: number, validateCode: string) { }, ]; - type UnlinkLoginParams = { - accountID: number; - validateCode: string; - }; - const params: UnlinkLoginParams = { accountID, validateCode, @@ -864,10 +813,6 @@ function validateTwoFactorAuth(twoFactorAuthCode: string) { }, ]; - type ValidateTwoFactorAuthParams = { - twoFactorAuthCode: string; - }; - const params: ValidateTwoFactorAuthParams = {twoFactorAuthCode}; API.write('TwoFactorAuth_Validate', params, {optimisticData, successData, failureData}); diff --git a/src/libs/actions/TeachersUnite.ts b/src/libs/actions/TeachersUnite.ts index 140f7a96d863..cc782f91f751 100644 --- a/src/libs/actions/TeachersUnite.ts +++ b/src/libs/actions/TeachersUnite.ts @@ -52,13 +52,6 @@ function referTeachersUniteVolunteer(partnerUserID: string, firstName: string, l }, ]; - type ReferTeachersUniteVolunteerParams = { - reportID: string; - firstName: string; - lastName: string; - partnerUserID: string; - }; - const parameters: ReferTeachersUniteVolunteerParams = { reportID: publicRoomReportID, firstName, @@ -178,14 +171,6 @@ function addSchoolPrincipal(firstName: string, partnerUserID: string, lastName: }, ]; - type AddSchoolPrincipalParams = { - firstName: string; - lastName: string; - partnerUserID: string; - policyID: string; - reportCreationData: string; - }; - const parameters: AddSchoolPrincipalParams = { firstName, lastName, diff --git a/src/libs/actions/User.ts b/src/libs/actions/User.ts index 484e3612d048..ce5d8594507e 100644 --- a/src/libs/actions/User.ts +++ b/src/libs/actions/User.ts @@ -75,8 +75,6 @@ function closeAccount(reason: string) { }, ]; - type CloseAccountParams = {message: string}; - const parameters: CloseAccountParams = {message: reason}; API.write(WRITE_COMMANDS.CLOSE_ACCOUNT, parameters, { @@ -149,8 +147,6 @@ function requestContactMethodValidateCode(contactMethod: string) { }, ]; - type RequestContactMethodValidateCodeParams = {email: string}; - const parameters: RequestContactMethodValidateCodeParams = {email: contactMethod}; API.write(WRITE_COMMANDS.REQUEST_CONTACT_METHOD_VALIDATE_CODE, parameters, {optimisticData, successData, failureData}); @@ -175,8 +171,6 @@ function updateNewsletterSubscription(isSubscribed: boolean) { }, ]; - type UpdateNewsletterSubscriptionParams = {isSubscribed: boolean}; - const parameters: UpdateNewsletterSubscriptionParams = {isSubscribed}; API.write(WRITE_COMMANDS.UPDATE_NEWSLETTER_SUBSCRIPTION, parameters, { @@ -237,8 +231,6 @@ function deleteContactMethod(contactMethod: string, loginList: Record, autom }); } - type UpdateChatPriorityModeParams = { - value: ValueOf; - automatic: boolean; - }; - const parameters: UpdateChatPriorityModeParams = { value: mode, automatic, @@ -766,10 +739,6 @@ function setContactMethodAsDefault(newDefaultContactMethod: string) { }, ]; - type SetContactMethodAsDefaultParams = { - partnerUserID: string; - }; - const parameters: SetContactMethodAsDefaultParams = { partnerUserID: newDefaultContactMethod, }; @@ -791,10 +760,6 @@ function updateTheme(theme: ValueOf) { }, ]; - type UpdateThemeParams = { - value: string; - }; - const parameters: UpdateThemeParams = { value: theme, }; @@ -820,12 +785,6 @@ function updateCustomStatus(status: Status) { }, ]; - type UpdateStatusParams = { - text?: string; - emojiCode: string; - clearAfter?: string; - }; - const parameters: UpdateStatusParams = {text: status.text, emojiCode: status.emojiCode, clearAfter: status.clearAfter}; API.write(WRITE_COMMANDS.UPDATE_STATUS, parameters, { diff --git a/src/libs/actions/Wallet.ts b/src/libs/actions/Wallet.ts index 44d30fd01b83..0aeb927be6fa 100644 --- a/src/libs/actions/Wallet.ts +++ b/src/libs/actions/Wallet.ts @@ -287,18 +287,6 @@ function requestPhysicalExpensifyCard(cardID: number, authToken: string, private address: {city, country, state, street, zip}, } = privatePersonalDetails; - type RequestPhysicalExpensifyCardParams = { - authToken: string; - legalFirstName: string; - legalLastName: string; - phoneNumber: string; - addressCity: string; - addressCountry: string; - addressState: string; - addressStreet: string; - addressZip: string; - }; - const requestParams: RequestPhysicalExpensifyCardParams = { authToken, legalFirstName, diff --git a/src/types/onyx/ReimbursementAccountDraft.ts b/src/types/onyx/ReimbursementAccountDraft.ts index cab1283943bc..b86f7e9dcb62 100644 --- a/src/types/onyx/ReimbursementAccountDraft.ts +++ b/src/types/onyx/ReimbursementAccountDraft.ts @@ -23,19 +23,7 @@ type CompanyStepProps = { hasNoConnectionToCannabis?: boolean; }; -type RequestorStepProps = { - firstName?: string; - lastName?: string; - requestorAddressStreet?: string; - requestorAddressCity?: string; - requestorAddressState?: string; - requestorAddressZipCode?: string; - dob?: string | Date; - ssnLast4?: string; - isControllingOfficer?: boolean; - isOnfidoSetupComplete?: boolean; - onfidoData?: OnfidoData; -}; + type ACHContractStepProps = { ownsMoreThan25Percent?: boolean; From 240994ad1a204e0b37628935db9583e4dec8ca41 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Tue, 16 Jan 2024 18:25:51 +0100 Subject: [PATCH 253/580] Add Write params to separate files --- .../ActivatePhysicalExpensifyCardParams.ts | 5 +++++ .../API/parameters/AddNewContactMethodParams.ts | 3 +++ src/libs/API/parameters/AddPaymentCardParams.ts | 11 +++++++++++ .../parameters/AddPersonalBankAccountParams.ts | 12 ++++++++++++ .../API/parameters/AddSchoolPrincipalParams.ts | 9 +++++++++ .../BankAccountHandlePlaidErrorParams.ts | 7 +++++++ src/libs/API/parameters/BeginAppleSignInParams.ts | 6 ++++++ .../API/parameters/BeginGoogleSignInParams.ts | 6 ++++++ src/libs/API/parameters/CloseAccountParams.ts | 3 +++ .../ConnectBankAccountManuallyParams.ts | 7 +++++++ .../ConnectBankAccountWithPlaidParams.ts | 10 ++++++++++ .../API/parameters/DeleteContactMethodParams.ts | 3 +++ .../parameters/DeletePaymentBankAccountParams.ts | 3 +++ .../API/parameters/DeletePaymentCardParams.ts | 4 ++++ src/libs/API/parameters/LogOutParams.ts | 9 +++++++++ .../parameters/MakeDefaultPaymentMethodParams.ts | 5 +++++ src/libs/API/parameters/PaymentCardParams.ts | 3 +++ .../ReferTeachersUniteVolunteerParams.ts | 8 ++++++++ .../ReportVirtualExpensifyCardFraudParams.ts | 4 ++++ .../RequestContactMethodValidateCodeParams.ts | 3 +++ .../parameters/RequestNewValidateCodeParams.ts | 5 +++++ .../RequestPhysicalExpensifyCardParams.ts | 13 +++++++++++++ .../RequestReplacementExpensifyCardParams.ts | 5 +++++ .../RequestUnlinkValidationLinkParams.ts | 5 +++++ .../API/parameters/ResendValidationLinkParams.ts | 5 +++++ .../ResolveActionableMentionWhisperParams.ts | 6 ++++++ .../parameters/SetContactMethodAsDefaultParams.ts | 5 +++++ src/libs/API/parameters/SignInUserParams.ts | 12 ++++++++++++ .../API/parameters/SignInUserWithLinkParams.ts | 9 +++++++++ src/libs/API/parameters/UnlinkLoginParams.ts | 6 ++++++ .../parameters/UpdateAutomaticTimezoneParams.ts | 4 ++++ .../parameters/UpdateChatPriorityModeParams.ts | 6 ++++++ .../API/parameters/UpdateDateOfBirthParams.ts | 4 ++++ .../API/parameters/UpdateDisplayNameParams.ts | 5 +++++ .../UpdateFrequentlyUsedEmojisParams.ts | 3 +++ .../API/parameters/UpdateHomeAddressParams.ts | 10 ++++++++++ src/libs/API/parameters/UpdateLegalNameParams.ts | 5 +++++ .../UpdateNewsletterSubscriptionParams.ts | 3 +++ ...datePersonalInformationForBankAccountParams.ts | 15 +++++++++++++++ .../UpdatePreferredEmojiSkinToneParams.ts | 5 +++++ src/libs/API/parameters/UpdatePronounsParams.ts | 4 ++++ .../parameters/UpdateSelectedTimezoneParams.ts | 4 ++++ src/libs/API/parameters/UpdateStatusParams.ts | 7 +++++++ src/libs/API/parameters/UpdateThemeParams.ts | 5 +++++ src/libs/API/parameters/UpdateUserAvatarParams.ts | 5 +++++ .../ValidateBankAccountWithTransactionsParams.ts | 6 ++++++ src/libs/API/parameters/ValidateLoginParams.ts | 6 ++++++ .../parameters/ValidateSecondaryLoginParams.ts | 3 +++ .../API/parameters/ValidateTwoFactorAuthParams.ts | 5 +++++ .../VerifyIdentityForBankAccountParams.ts | 5 +++++ 50 files changed, 302 insertions(+) create mode 100644 src/libs/API/parameters/ActivatePhysicalExpensifyCardParams.ts create mode 100644 src/libs/API/parameters/AddNewContactMethodParams.ts create mode 100644 src/libs/API/parameters/AddPaymentCardParams.ts create mode 100644 src/libs/API/parameters/AddPersonalBankAccountParams.ts create mode 100644 src/libs/API/parameters/AddSchoolPrincipalParams.ts create mode 100644 src/libs/API/parameters/BankAccountHandlePlaidErrorParams.ts create mode 100644 src/libs/API/parameters/BeginAppleSignInParams.ts create mode 100644 src/libs/API/parameters/BeginGoogleSignInParams.ts create mode 100644 src/libs/API/parameters/CloseAccountParams.ts create mode 100644 src/libs/API/parameters/ConnectBankAccountManuallyParams.ts create mode 100644 src/libs/API/parameters/ConnectBankAccountWithPlaidParams.ts create mode 100644 src/libs/API/parameters/DeleteContactMethodParams.ts create mode 100644 src/libs/API/parameters/DeletePaymentBankAccountParams.ts create mode 100644 src/libs/API/parameters/DeletePaymentCardParams.ts create mode 100644 src/libs/API/parameters/LogOutParams.ts create mode 100644 src/libs/API/parameters/MakeDefaultPaymentMethodParams.ts create mode 100644 src/libs/API/parameters/PaymentCardParams.ts create mode 100644 src/libs/API/parameters/ReferTeachersUniteVolunteerParams.ts create mode 100644 src/libs/API/parameters/ReportVirtualExpensifyCardFraudParams.ts create mode 100644 src/libs/API/parameters/RequestContactMethodValidateCodeParams.ts create mode 100644 src/libs/API/parameters/RequestNewValidateCodeParams.ts create mode 100644 src/libs/API/parameters/RequestPhysicalExpensifyCardParams.ts create mode 100644 src/libs/API/parameters/RequestReplacementExpensifyCardParams.ts create mode 100644 src/libs/API/parameters/RequestUnlinkValidationLinkParams.ts create mode 100644 src/libs/API/parameters/ResendValidationLinkParams.ts create mode 100644 src/libs/API/parameters/ResolveActionableMentionWhisperParams.ts create mode 100644 src/libs/API/parameters/SetContactMethodAsDefaultParams.ts create mode 100644 src/libs/API/parameters/SignInUserParams.ts create mode 100644 src/libs/API/parameters/SignInUserWithLinkParams.ts create mode 100644 src/libs/API/parameters/UnlinkLoginParams.ts create mode 100644 src/libs/API/parameters/UpdateAutomaticTimezoneParams.ts create mode 100644 src/libs/API/parameters/UpdateChatPriorityModeParams.ts create mode 100644 src/libs/API/parameters/UpdateDateOfBirthParams.ts create mode 100644 src/libs/API/parameters/UpdateDisplayNameParams.ts create mode 100644 src/libs/API/parameters/UpdateFrequentlyUsedEmojisParams.ts create mode 100644 src/libs/API/parameters/UpdateHomeAddressParams.ts create mode 100644 src/libs/API/parameters/UpdateLegalNameParams.ts create mode 100644 src/libs/API/parameters/UpdateNewsletterSubscriptionParams.ts create mode 100644 src/libs/API/parameters/UpdatePersonalInformationForBankAccountParams.ts create mode 100644 src/libs/API/parameters/UpdatePreferredEmojiSkinToneParams.ts create mode 100644 src/libs/API/parameters/UpdatePronounsParams.ts create mode 100644 src/libs/API/parameters/UpdateSelectedTimezoneParams.ts create mode 100644 src/libs/API/parameters/UpdateStatusParams.ts create mode 100644 src/libs/API/parameters/UpdateThemeParams.ts create mode 100644 src/libs/API/parameters/UpdateUserAvatarParams.ts create mode 100644 src/libs/API/parameters/ValidateBankAccountWithTransactionsParams.ts create mode 100644 src/libs/API/parameters/ValidateLoginParams.ts create mode 100644 src/libs/API/parameters/ValidateSecondaryLoginParams.ts create mode 100644 src/libs/API/parameters/ValidateTwoFactorAuthParams.ts create mode 100644 src/libs/API/parameters/VerifyIdentityForBankAccountParams.ts diff --git a/src/libs/API/parameters/ActivatePhysicalExpensifyCardParams.ts b/src/libs/API/parameters/ActivatePhysicalExpensifyCardParams.ts new file mode 100644 index 000000000000..98d7f9f4ae32 --- /dev/null +++ b/src/libs/API/parameters/ActivatePhysicalExpensifyCardParams.ts @@ -0,0 +1,5 @@ +type ActivatePhysicalExpensifyCardParams = { + cardLastFourDigits: string; + cardID: number; +}; +export default ActivatePhysicalExpensifyCardParams; diff --git a/src/libs/API/parameters/AddNewContactMethodParams.ts b/src/libs/API/parameters/AddNewContactMethodParams.ts new file mode 100644 index 000000000000..f5cd7824c191 --- /dev/null +++ b/src/libs/API/parameters/AddNewContactMethodParams.ts @@ -0,0 +1,3 @@ +type AddNewContactMethodParams = {partnerUserID: string}; + +export default AddNewContactMethodParams; diff --git a/src/libs/API/parameters/AddPaymentCardParams.ts b/src/libs/API/parameters/AddPaymentCardParams.ts new file mode 100644 index 000000000000..4c9bf8691420 --- /dev/null +++ b/src/libs/API/parameters/AddPaymentCardParams.ts @@ -0,0 +1,11 @@ +type AddPaymentCardParams = { + cardNumber: string; + cardYear: string; + cardMonth: string; + cardCVV: string; + addressName: string; + addressZip: string; + currency: ValueOf; + isP2PDebitCard: boolean; +}; +export default AddPaymentCardParams; diff --git a/src/libs/API/parameters/AddPersonalBankAccountParams.ts b/src/libs/API/parameters/AddPersonalBankAccountParams.ts new file mode 100644 index 000000000000..1fa8fc0eb48d --- /dev/null +++ b/src/libs/API/parameters/AddPersonalBankAccountParams.ts @@ -0,0 +1,12 @@ +type AddPersonalBankAccountParams = { + addressName: string; + routingNumber: string; + accountNumber: string; + isSavings: boolean; + setupType: string; + bank?: string; + plaidAccountID: string; + plaidAccessToken: string; +}; + +export default AddPersonalBankAccountParams; diff --git a/src/libs/API/parameters/AddSchoolPrincipalParams.ts b/src/libs/API/parameters/AddSchoolPrincipalParams.ts new file mode 100644 index 000000000000..5602dd22973c --- /dev/null +++ b/src/libs/API/parameters/AddSchoolPrincipalParams.ts @@ -0,0 +1,9 @@ +type AddSchoolPrincipalParams = { + firstName: string; + lastName: string; + partnerUserID: string; + policyID: string; + reportCreationData: string; +}; + +export default AddSchoolPrincipalParams; diff --git a/src/libs/API/parameters/BankAccountHandlePlaidErrorParams.ts b/src/libs/API/parameters/BankAccountHandlePlaidErrorParams.ts new file mode 100644 index 000000000000..02ee6cd75219 --- /dev/null +++ b/src/libs/API/parameters/BankAccountHandlePlaidErrorParams.ts @@ -0,0 +1,7 @@ +type BankAccountHandlePlaidErrorParams = { + bankAccountID: number; + error: string; + errorDescription: string; + plaidRequestID: string; +}; +export default BankAccountHandlePlaidErrorParams; diff --git a/src/libs/API/parameters/BeginAppleSignInParams.ts b/src/libs/API/parameters/BeginAppleSignInParams.ts new file mode 100644 index 000000000000..d2b84b5c9cfe --- /dev/null +++ b/src/libs/API/parameters/BeginAppleSignInParams.ts @@ -0,0 +1,6 @@ +type BeginAppleSignInParams = { + idToken: typeof idToken; + preferredLocale: ValueOf | null; +}; + +export default BeginAppleSignInParams; diff --git a/src/libs/API/parameters/BeginGoogleSignInParams.ts b/src/libs/API/parameters/BeginGoogleSignInParams.ts new file mode 100644 index 000000000000..27fe8830e9f4 --- /dev/null +++ b/src/libs/API/parameters/BeginGoogleSignInParams.ts @@ -0,0 +1,6 @@ +type BeginGoogleSignInParams = { + token: string | null; + preferredLocale: ValueOf | null; +}; + +export default BeginGoogleSignInParams; diff --git a/src/libs/API/parameters/CloseAccountParams.ts b/src/libs/API/parameters/CloseAccountParams.ts new file mode 100644 index 000000000000..643d5468778f --- /dev/null +++ b/src/libs/API/parameters/CloseAccountParams.ts @@ -0,0 +1,3 @@ +type CloseAccountParams = {message: string}; + +export default CloseAccountParams; diff --git a/src/libs/API/parameters/ConnectBankAccountManuallyParams.ts b/src/libs/API/parameters/ConnectBankAccountManuallyParams.ts new file mode 100644 index 000000000000..4f166cfd3aa9 --- /dev/null +++ b/src/libs/API/parameters/ConnectBankAccountManuallyParams.ts @@ -0,0 +1,7 @@ +type ConnectBankAccountManuallyParams = { + bankAccountID: number; + accountNumber?: string; + routingNumber?: string; + plaidMask?: string; +}; +export default ConnectBankAccountManuallyParams; diff --git a/src/libs/API/parameters/ConnectBankAccountWithPlaidParams.ts b/src/libs/API/parameters/ConnectBankAccountWithPlaidParams.ts new file mode 100644 index 000000000000..63df9d280412 --- /dev/null +++ b/src/libs/API/parameters/ConnectBankAccountWithPlaidParams.ts @@ -0,0 +1,10 @@ +type ConnectBankAccountWithPlaidParams = { + bankAccountID: number; + routingNumber: string; + accountNumber: string; + bank?: string; + plaidAccountID: string; + plaidAccessToken: string; +}; + +export default ConnectBankAccountWithPlaidParams; diff --git a/src/libs/API/parameters/DeleteContactMethodParams.ts b/src/libs/API/parameters/DeleteContactMethodParams.ts new file mode 100644 index 000000000000..274c3ba73512 --- /dev/null +++ b/src/libs/API/parameters/DeleteContactMethodParams.ts @@ -0,0 +1,3 @@ +type DeleteContactMethodParams = {partnerUserID: string}; + +export default DeleteContactMethodParams; diff --git a/src/libs/API/parameters/DeletePaymentBankAccountParams.ts b/src/libs/API/parameters/DeletePaymentBankAccountParams.ts new file mode 100644 index 000000000000..737a61ccc16b --- /dev/null +++ b/src/libs/API/parameters/DeletePaymentBankAccountParams.ts @@ -0,0 +1,3 @@ +type DeletePaymentBankAccountParams = {bankAccountID: number}; + +export default DeletePaymentBankAccountParams; diff --git a/src/libs/API/parameters/DeletePaymentCardParams.ts b/src/libs/API/parameters/DeletePaymentCardParams.ts new file mode 100644 index 000000000000..e82edfbf525a --- /dev/null +++ b/src/libs/API/parameters/DeletePaymentCardParams.ts @@ -0,0 +1,4 @@ +type DeletePaymentCardParams = { + fundID: number; +}; +export default DeletePaymentCardParams; diff --git a/src/libs/API/parameters/LogOutParams.ts b/src/libs/API/parameters/LogOutParams.ts new file mode 100644 index 000000000000..7cb81080b19f --- /dev/null +++ b/src/libs/API/parameters/LogOutParams.ts @@ -0,0 +1,9 @@ +type LogOutParams = { + authToken: string | null; + partnerUserID: string; + partnerName: string; + partnerPassword: string; + shouldRetry: boolean; +}; + +export default LogOutParams; diff --git a/src/libs/API/parameters/MakeDefaultPaymentMethodParams.ts b/src/libs/API/parameters/MakeDefaultPaymentMethodParams.ts new file mode 100644 index 000000000000..9cc07214845c --- /dev/null +++ b/src/libs/API/parameters/MakeDefaultPaymentMethodParams.ts @@ -0,0 +1,5 @@ +type MakeDefaultPaymentMethodParams = { + bankAccountID: number; + fundID: number; +}; +export default MakeDefaultPaymentMethodParams; diff --git a/src/libs/API/parameters/PaymentCardParams.ts b/src/libs/API/parameters/PaymentCardParams.ts new file mode 100644 index 000000000000..3e705994e9fa --- /dev/null +++ b/src/libs/API/parameters/PaymentCardParams.ts @@ -0,0 +1,3 @@ +type PaymentCardParams = {expirationDate: string; cardNumber: string; securityCode: string; nameOnCard: string; addressZipCode: string}; + +export default PaymentCardParams; diff --git a/src/libs/API/parameters/ReferTeachersUniteVolunteerParams.ts b/src/libs/API/parameters/ReferTeachersUniteVolunteerParams.ts new file mode 100644 index 000000000000..0eb31c47865d --- /dev/null +++ b/src/libs/API/parameters/ReferTeachersUniteVolunteerParams.ts @@ -0,0 +1,8 @@ +type ReferTeachersUniteVolunteerParams = { + reportID: string; + firstName: string; + lastName: string; + partnerUserID: string; +}; + +export default ReferTeachersUniteVolunteerParams; diff --git a/src/libs/API/parameters/ReportVirtualExpensifyCardFraudParams.ts b/src/libs/API/parameters/ReportVirtualExpensifyCardFraudParams.ts new file mode 100644 index 000000000000..350795d46355 --- /dev/null +++ b/src/libs/API/parameters/ReportVirtualExpensifyCardFraudParams.ts @@ -0,0 +1,4 @@ +type ReportVirtualExpensifyCardFraudParams = { + cardID: number; +}; +export default ReportVirtualExpensifyCardFraudParams; diff --git a/src/libs/API/parameters/RequestContactMethodValidateCodeParams.ts b/src/libs/API/parameters/RequestContactMethodValidateCodeParams.ts new file mode 100644 index 000000000000..13a26b717619 --- /dev/null +++ b/src/libs/API/parameters/RequestContactMethodValidateCodeParams.ts @@ -0,0 +1,3 @@ +type RequestContactMethodValidateCodeParams = {email: string}; + +export default RequestContactMethodValidateCodeParams; diff --git a/src/libs/API/parameters/RequestNewValidateCodeParams.ts b/src/libs/API/parameters/RequestNewValidateCodeParams.ts new file mode 100644 index 000000000000..329b234023d0 --- /dev/null +++ b/src/libs/API/parameters/RequestNewValidateCodeParams.ts @@ -0,0 +1,5 @@ +type RequestNewValidateCodeParams = { + email?: string; +}; + +export default RequestNewValidateCodeParams; diff --git a/src/libs/API/parameters/RequestPhysicalExpensifyCardParams.ts b/src/libs/API/parameters/RequestPhysicalExpensifyCardParams.ts new file mode 100644 index 000000000000..91995b6e37aa --- /dev/null +++ b/src/libs/API/parameters/RequestPhysicalExpensifyCardParams.ts @@ -0,0 +1,13 @@ +type RequestPhysicalExpensifyCardParams = { + authToken: string; + legalFirstName: string; + legalLastName: string; + phoneNumber: string; + addressCity: string; + addressCountry: string; + addressState: string; + addressStreet: string; + addressZip: string; +}; + +export default RequestPhysicalExpensifyCardParams; diff --git a/src/libs/API/parameters/RequestReplacementExpensifyCardParams.ts b/src/libs/API/parameters/RequestReplacementExpensifyCardParams.ts new file mode 100644 index 000000000000..f136086338f4 --- /dev/null +++ b/src/libs/API/parameters/RequestReplacementExpensifyCardParams.ts @@ -0,0 +1,5 @@ +type RequestReplacementExpensifyCardParams = { + cardId: number; + reason: string; +}; +export default RequestReplacementExpensifyCardParams; diff --git a/src/libs/API/parameters/RequestUnlinkValidationLinkParams.ts b/src/libs/API/parameters/RequestUnlinkValidationLinkParams.ts new file mode 100644 index 000000000000..2a37f7119304 --- /dev/null +++ b/src/libs/API/parameters/RequestUnlinkValidationLinkParams.ts @@ -0,0 +1,5 @@ +type RequestUnlinkValidationLinkParams = { + email?: string; +}; + +export default RequestUnlinkValidationLinkParams; diff --git a/src/libs/API/parameters/ResendValidationLinkParams.ts b/src/libs/API/parameters/ResendValidationLinkParams.ts new file mode 100644 index 000000000000..663ccc33f1e5 --- /dev/null +++ b/src/libs/API/parameters/ResendValidationLinkParams.ts @@ -0,0 +1,5 @@ +type ResendValidationLinkParams = { + email?: string; +}; + +export default ResendValidationLinkParams; diff --git a/src/libs/API/parameters/ResolveActionableMentionWhisperParams.ts b/src/libs/API/parameters/ResolveActionableMentionWhisperParams.ts new file mode 100644 index 000000000000..bdfc965a7307 --- /dev/null +++ b/src/libs/API/parameters/ResolveActionableMentionWhisperParams.ts @@ -0,0 +1,6 @@ +type ResolveActionableMentionWhisperParams = { + reportActionID: string; + resolution: ValueOf; +}; + +export default ResolveActionableMentionWhisperParams; diff --git a/src/libs/API/parameters/SetContactMethodAsDefaultParams.ts b/src/libs/API/parameters/SetContactMethodAsDefaultParams.ts new file mode 100644 index 000000000000..03a33eb81c36 --- /dev/null +++ b/src/libs/API/parameters/SetContactMethodAsDefaultParams.ts @@ -0,0 +1,5 @@ +type SetContactMethodAsDefaultParams = { + partnerUserID: string; +}; + +export default SetContactMethodAsDefaultParams; diff --git a/src/libs/API/parameters/SignInUserParams.ts b/src/libs/API/parameters/SignInUserParams.ts new file mode 100644 index 000000000000..9fe973c42862 --- /dev/null +++ b/src/libs/API/parameters/SignInUserParams.ts @@ -0,0 +1,12 @@ +import type {ValueOf} from 'type-fest'; +import type CONST from '@src/CONST'; + +type SignInUserParams = { + twoFactorAuthCode?: string; + email?: string; + preferredLocale: ValueOf | null; + validateCode?: string; + deviceInfo: string; +}; + +export default SignInUserParams; diff --git a/src/libs/API/parameters/SignInUserWithLinkParams.ts b/src/libs/API/parameters/SignInUserWithLinkParams.ts new file mode 100644 index 000000000000..ea7f0ff2ee05 --- /dev/null +++ b/src/libs/API/parameters/SignInUserWithLinkParams.ts @@ -0,0 +1,9 @@ +type SignInUserWithLinkParams = { + accountID: number; + validateCode?: string; + twoFactorAuthCode?: string; + preferredLocale: ValueOf | null; + deviceInfo: string; +}; + +export default SignInUserWithLinkParams; diff --git a/src/libs/API/parameters/UnlinkLoginParams.ts b/src/libs/API/parameters/UnlinkLoginParams.ts new file mode 100644 index 000000000000..1a60e480bb18 --- /dev/null +++ b/src/libs/API/parameters/UnlinkLoginParams.ts @@ -0,0 +1,6 @@ +type UnlinkLoginParams = { + accountID: number; + validateCode: string; +}; + +export default UnlinkLoginParams; diff --git a/src/libs/API/parameters/UpdateAutomaticTimezoneParams.ts b/src/libs/API/parameters/UpdateAutomaticTimezoneParams.ts new file mode 100644 index 000000000000..07c13e0cf55e --- /dev/null +++ b/src/libs/API/parameters/UpdateAutomaticTimezoneParams.ts @@ -0,0 +1,4 @@ +type UpdateAutomaticTimezoneParams = { + timezone: string; +}; +export default UpdateAutomaticTimezoneParams; diff --git a/src/libs/API/parameters/UpdateChatPriorityModeParams.ts b/src/libs/API/parameters/UpdateChatPriorityModeParams.ts new file mode 100644 index 000000000000..d033729e9189 --- /dev/null +++ b/src/libs/API/parameters/UpdateChatPriorityModeParams.ts @@ -0,0 +1,6 @@ +type UpdateChatPriorityModeParams = { + value: ValueOf; + automatic: boolean; +}; + +export default UpdateChatPriorityModeParams; diff --git a/src/libs/API/parameters/UpdateDateOfBirthParams.ts b/src/libs/API/parameters/UpdateDateOfBirthParams.ts new file mode 100644 index 000000000000..e98336d16bff --- /dev/null +++ b/src/libs/API/parameters/UpdateDateOfBirthParams.ts @@ -0,0 +1,4 @@ +type UpdateDateOfBirthParams = { + dob?: string; +}; +export default UpdateDateOfBirthParams; diff --git a/src/libs/API/parameters/UpdateDisplayNameParams.ts b/src/libs/API/parameters/UpdateDisplayNameParams.ts new file mode 100644 index 000000000000..0febd6765fc0 --- /dev/null +++ b/src/libs/API/parameters/UpdateDisplayNameParams.ts @@ -0,0 +1,5 @@ +type UpdateDisplayNameParams = { + firstName: string; + lastName: string; +}; +export default UpdateDisplayNameParams; diff --git a/src/libs/API/parameters/UpdateFrequentlyUsedEmojisParams.ts b/src/libs/API/parameters/UpdateFrequentlyUsedEmojisParams.ts new file mode 100644 index 000000000000..f790ada3aad9 --- /dev/null +++ b/src/libs/API/parameters/UpdateFrequentlyUsedEmojisParams.ts @@ -0,0 +1,3 @@ +type UpdateFrequentlyUsedEmojisParams = {value: string}; + +export default UpdateFrequentlyUsedEmojisParams; diff --git a/src/libs/API/parameters/UpdateHomeAddressParams.ts b/src/libs/API/parameters/UpdateHomeAddressParams.ts new file mode 100644 index 000000000000..30cc933069df --- /dev/null +++ b/src/libs/API/parameters/UpdateHomeAddressParams.ts @@ -0,0 +1,10 @@ +type UpdateHomeAddressParams = { + homeAddressStreet: string; + addressStreet2: string; + homeAddressCity: string; + addressState: string; + addressZipCode: string; + addressCountry: string; + addressStateLong?: string; +}; +export default UpdateHomeAddressParams; diff --git a/src/libs/API/parameters/UpdateLegalNameParams.ts b/src/libs/API/parameters/UpdateLegalNameParams.ts new file mode 100644 index 000000000000..ac9ad77aabf5 --- /dev/null +++ b/src/libs/API/parameters/UpdateLegalNameParams.ts @@ -0,0 +1,5 @@ +type UpdateLegalNameParams = { + legalFirstName: string; + legalLastName: string; +}; +export default UpdateLegalNameParams; diff --git a/src/libs/API/parameters/UpdateNewsletterSubscriptionParams.ts b/src/libs/API/parameters/UpdateNewsletterSubscriptionParams.ts new file mode 100644 index 000000000000..311d3a5518df --- /dev/null +++ b/src/libs/API/parameters/UpdateNewsletterSubscriptionParams.ts @@ -0,0 +1,3 @@ +type UpdateNewsletterSubscriptionParams = {isSubscribed: boolean}; + +export default UpdateNewsletterSubscriptionParams; diff --git a/src/libs/API/parameters/UpdatePersonalInformationForBankAccountParams.ts b/src/libs/API/parameters/UpdatePersonalInformationForBankAccountParams.ts new file mode 100644 index 000000000000..1555500e84ee --- /dev/null +++ b/src/libs/API/parameters/UpdatePersonalInformationForBankAccountParams.ts @@ -0,0 +1,15 @@ +type UpdatePersonalInformationForBankAccountParams = { + firstName?: string; + lastName?: string; + requestorAddressStreet?: string; + requestorAddressCity?: string; + requestorAddressState?: string; + requestorAddressZipCode?: string; + dob?: string | Date; + ssnLast4?: string; + isControllingOfficer?: boolean; + isOnfidoSetupComplete?: boolean; + onfidoData?: OnfidoData; +}; + +export default UpdatePersonalInformationForBankAccountParams; diff --git a/src/libs/API/parameters/UpdatePreferredEmojiSkinToneParams.ts b/src/libs/API/parameters/UpdatePreferredEmojiSkinToneParams.ts new file mode 100644 index 000000000000..a769e3635bfa --- /dev/null +++ b/src/libs/API/parameters/UpdatePreferredEmojiSkinToneParams.ts @@ -0,0 +1,5 @@ +type UpdatePreferredEmojiSkinToneParams = { + value: number; +}; + +export default UpdatePreferredEmojiSkinToneParams; diff --git a/src/libs/API/parameters/UpdatePronounsParams.ts b/src/libs/API/parameters/UpdatePronounsParams.ts new file mode 100644 index 000000000000..711cb9aa84f4 --- /dev/null +++ b/src/libs/API/parameters/UpdatePronounsParams.ts @@ -0,0 +1,4 @@ +type UpdatePronounsParams = { + pronouns: string; +}; +export default UpdatePronounsParams; diff --git a/src/libs/API/parameters/UpdateSelectedTimezoneParams.ts b/src/libs/API/parameters/UpdateSelectedTimezoneParams.ts new file mode 100644 index 000000000000..f8ef26ef147e --- /dev/null +++ b/src/libs/API/parameters/UpdateSelectedTimezoneParams.ts @@ -0,0 +1,4 @@ +type UpdateSelectedTimezoneParams = { + timezone: string; +}; +export default UpdateSelectedTimezoneParams; diff --git a/src/libs/API/parameters/UpdateStatusParams.ts b/src/libs/API/parameters/UpdateStatusParams.ts new file mode 100644 index 000000000000..ba812e554cd7 --- /dev/null +++ b/src/libs/API/parameters/UpdateStatusParams.ts @@ -0,0 +1,7 @@ +type UpdateStatusParams = { + text?: string; + emojiCode: string; + clearAfter?: string; +}; + +export default UpdateStatusParams; diff --git a/src/libs/API/parameters/UpdateThemeParams.ts b/src/libs/API/parameters/UpdateThemeParams.ts new file mode 100644 index 000000000000..10a8c243d6e4 --- /dev/null +++ b/src/libs/API/parameters/UpdateThemeParams.ts @@ -0,0 +1,5 @@ +type UpdateThemeParams = { + value: string; +}; + +export default UpdateThemeParams; diff --git a/src/libs/API/parameters/UpdateUserAvatarParams.ts b/src/libs/API/parameters/UpdateUserAvatarParams.ts new file mode 100644 index 000000000000..cc45e50ba0b6 --- /dev/null +++ b/src/libs/API/parameters/UpdateUserAvatarParams.ts @@ -0,0 +1,5 @@ +type UpdateUserAvatarParams = { + file: File | CustomRNImageManipulatorResult; +}; + +export default UpdateUserAvatarParams; diff --git a/src/libs/API/parameters/ValidateBankAccountWithTransactionsParams.ts b/src/libs/API/parameters/ValidateBankAccountWithTransactionsParams.ts new file mode 100644 index 000000000000..546889b7a68e --- /dev/null +++ b/src/libs/API/parameters/ValidateBankAccountWithTransactionsParams.ts @@ -0,0 +1,6 @@ +type ValidateBankAccountWithTransactionsParams = { + bankAccountID: number; + validateCode: string; +}; + +export default ValidateBankAccountWithTransactionsParams; diff --git a/src/libs/API/parameters/ValidateLoginParams.ts b/src/libs/API/parameters/ValidateLoginParams.ts new file mode 100644 index 000000000000..361c374e4e32 --- /dev/null +++ b/src/libs/API/parameters/ValidateLoginParams.ts @@ -0,0 +1,6 @@ +type ValidateLoginParams = { + accountID: number; + validateCode: string; +}; + +export default ValidateLoginParams; diff --git a/src/libs/API/parameters/ValidateSecondaryLoginParams.ts b/src/libs/API/parameters/ValidateSecondaryLoginParams.ts new file mode 100644 index 000000000000..870a756da524 --- /dev/null +++ b/src/libs/API/parameters/ValidateSecondaryLoginParams.ts @@ -0,0 +1,3 @@ +type ValidateSecondaryLoginParams = {partnerUserID: string; validateCode: string}; + +export default ValidateSecondaryLoginParams; diff --git a/src/libs/API/parameters/ValidateTwoFactorAuthParams.ts b/src/libs/API/parameters/ValidateTwoFactorAuthParams.ts new file mode 100644 index 000000000000..dad8f53089dd --- /dev/null +++ b/src/libs/API/parameters/ValidateTwoFactorAuthParams.ts @@ -0,0 +1,5 @@ +type ValidateTwoFactorAuthParams = { + twoFactorAuthCode: string; +}; + +export default ValidateTwoFactorAuthParams; diff --git a/src/libs/API/parameters/VerifyIdentityForBankAccountParams.ts b/src/libs/API/parameters/VerifyIdentityForBankAccountParams.ts new file mode 100644 index 000000000000..424cef92c08f --- /dev/null +++ b/src/libs/API/parameters/VerifyIdentityForBankAccountParams.ts @@ -0,0 +1,5 @@ +type VerifyIdentityForBankAccountParams = { + bankAccountID: number; + onfidoData: string; +}; +export default VerifyIdentityForBankAccountParams; From f8e2d226cadbdd44d2443924645b85f98088f93e Mon Sep 17 00:00:00 2001 From: Pedro Guerreiro Date: Tue, 16 Jan 2024 18:25:05 +0000 Subject: [PATCH 254/580] refactor(typescript): migrate aboutpage --- .../AboutPage/{AboutPage.js => AboutPage.tsx} | 55 +++++++++++-------- 1 file changed, 31 insertions(+), 24 deletions(-) rename src/pages/settings/AboutPage/{AboutPage.js => AboutPage.tsx} (73%) diff --git a/src/pages/settings/AboutPage/AboutPage.js b/src/pages/settings/AboutPage/AboutPage.tsx similarity index 73% rename from src/pages/settings/AboutPage/AboutPage.js rename to src/pages/settings/AboutPage/AboutPage.tsx index a460c95cdfe6..454d3e06e82e 100644 --- a/src/pages/settings/AboutPage/AboutPage.js +++ b/src/pages/settings/AboutPage/AboutPage.tsx @@ -1,31 +1,32 @@ import React, {useCallback, useMemo, useRef} from 'react'; import {View} from 'react-native'; +// eslint-disable-next-line no-restricted-imports +import type {GestureResponderEvent, Text as RNText} from 'react-native'; import DeviceInfo from 'react-native-device-info'; -import _ from 'underscore'; import * as Expensicons from '@components/Icon/Expensicons'; import IllustratedHeaderPageLayout from '@components/IllustratedHeaderPageLayout'; import LottieAnimations from '@components/LottieAnimations'; import MenuItemList from '@components/MenuItemList'; import Text from '@components/Text'; import TextLink from '@components/TextLink'; -import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; import withWindowDimensions, {windowDimensionsPropTypes} from '@components/withWindowDimensions'; +import useLocalize from '@hooks/useLocalize'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import useWaitForNavigation from '@hooks/useWaitForNavigation'; -import compose from '@libs/compose'; import * as Environment from '@libs/Environment/Environment'; import Navigation from '@libs/Navigation/Navigation'; import * as ReportActionContextMenu from '@pages/home/report/ContextMenu/ReportActionContextMenu'; import * as Link from '@userActions/Link'; import * as Report from '@userActions/Report'; import CONST from '@src/CONST'; +import type {TranslationPaths} from '@src/languages/types'; import ROUTES from '@src/ROUTES'; import SCREENS from '@src/SCREENS'; +import type IconAsset from '@src/types/utils/IconAsset'; import pkg from '../../../../package.json'; const propTypes = { - ...withLocalizePropTypes, ...windowDimensionsPropTypes, }; @@ -40,15 +41,17 @@ function getFlavor() { return ''; } -function AboutPage(props) { +type MenuItem = {translationKey: TranslationPaths; icon: IconAsset; iconRight?: IconAsset; action: () => Promise; link?: string}; + +function AboutPage() { + const {translate} = useLocalize(); const theme = useTheme(); const styles = useThemeStyles(); - const {translate} = props; - const popoverAnchor = useRef(null); + const popoverAnchor = useRef(null); const waitForNavigate = useWaitForNavigation(); const menuItems = useMemo(() => { - const baseMenuItems = [ + const baseMenuItems: MenuItem[] = [ { translationKey: 'initialSettingsPage.aboutPage.appDownloadLinks', icon: Expensicons.Link, @@ -65,6 +68,7 @@ function AboutPage(props) { iconRight: Expensicons.NewWindow, action: () => { Link.openExternalLink(CONST.GITHUB_URL); + return Promise.resolve(); }, link: CONST.GITHUB_URL, }, @@ -74,6 +78,7 @@ function AboutPage(props) { iconRight: Expensicons.NewWindow, action: () => { Link.openExternalLink(CONST.UPWORK_URL); + return Promise.resolve(); }, link: CONST.UPWORK_URL, }, @@ -83,16 +88,18 @@ function AboutPage(props) { action: waitForNavigate(Report.navigateToConciergeChat), }, ]; - return _.map(baseMenuItems, (item) => ({ - key: item.translationKey, - title: translate(item.translationKey), - icon: item.icon, - iconRight: item.iconRight, - onPress: item.action, + return baseMenuItems.map(({translationKey, icon, iconRight, action, link}: MenuItem) => ({ + key: translationKey, + title: translate(translationKey), + icon, + iconRight, + onPress: action, shouldShowRightIcon: true, - onSecondaryInteraction: !_.isEmpty(item.link) ? (e) => ReportActionContextMenu.showContextMenu(CONST.CONTEXT_MENU_TYPES.LINK, e, item.link, popoverAnchor) : undefined, + onSecondaryInteraction: link + ? (e: GestureResponderEvent | MouseEvent) => ReportActionContextMenu.showContextMenu(CONST.CONTEXT_MENU_TYPES.LINK, e, link, popoverAnchor.current) + : undefined, ref: popoverAnchor, - shouldBlockSelection: Boolean(item.link), + shouldBlockSelection: !!link, })); }, [translate, waitForNavigate]); @@ -114,15 +121,15 @@ function AboutPage(props) { return ( Navigation.goBack(ROUTES.SETTINGS)} illustration={LottieAnimations.Coin} backgroundColor={theme.PAGE_THEMES[SCREENS.SETTINGS.ABOUT].backgroundColor} overlayContent={overlayContent} > - {props.translate('footer.aboutExpensify')} - {props.translate('initialSettingsPage.aboutPage.description')} + {translate('footer.aboutExpensify')} + {translate('initialSettingsPage.aboutPage.description')} - {props.translate('initialSettingsPage.readTheTermsAndPrivacy.phrase1')}{' '} + {translate('initialSettingsPage.readTheTermsAndPrivacy.phrase1')}{' '} - {props.translate('initialSettingsPage.readTheTermsAndPrivacy.phrase2')} + {translate('initialSettingsPage.readTheTermsAndPrivacy.phrase2')} {' '} - {props.translate('initialSettingsPage.readTheTermsAndPrivacy.phrase3')}{' '} + {translate('initialSettingsPage.readTheTermsAndPrivacy.phrase3')}{' '} - {props.translate('initialSettingsPage.readTheTermsAndPrivacy.phrase4')} + {translate('initialSettingsPage.readTheTermsAndPrivacy.phrase4')} . @@ -157,4 +164,4 @@ function AboutPage(props) { AboutPage.propTypes = propTypes; AboutPage.displayName = 'AboutPage'; -export default compose(withLocalize, withWindowDimensions)(AboutPage); +export default withWindowDimensions(AboutPage); From dd6068d2f1e86d745170be512443bc4287e9061a Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Tue, 16 Jan 2024 20:25:35 +0100 Subject: [PATCH 255/580] Fix parameters files --- .../API/parameters/AddPaymentCardParams.ts | 3 + .../API/parameters/BeginAppleSignInParams.ts | 5 +- .../API/parameters/BeginGoogleSignInParams.ts | 3 + .../ResolveActionableMentionWhisperParams.ts | 3 + .../parameters/SignInUserWithLinkParams.ts | 3 + .../UpdateChatPriorityModeParams.ts | 3 + .../API/parameters/UpdateHomeAddressParams.ts | 1 + .../API/parameters/UpdateLegalNameParams.ts | 1 + .../API/parameters/UpdatePronounsParams.ts | 1 + .../UpdateSelectedTimezoneParams.ts | 1 + .../API/parameters/UpdateUserAvatarParams.ts | 2 + src/libs/API/parameters/index.ts | 83 +++++++++++++++---- 12 files changed, 91 insertions(+), 18 deletions(-) diff --git a/src/libs/API/parameters/AddPaymentCardParams.ts b/src/libs/API/parameters/AddPaymentCardParams.ts index 4c9bf8691420..1c9b1fc4fa30 100644 --- a/src/libs/API/parameters/AddPaymentCardParams.ts +++ b/src/libs/API/parameters/AddPaymentCardParams.ts @@ -1,3 +1,6 @@ +import type {ValueOf} from 'type-fest'; +import type CONST from '@src/CONST'; + type AddPaymentCardParams = { cardNumber: string; cardYear: string; diff --git a/src/libs/API/parameters/BeginAppleSignInParams.ts b/src/libs/API/parameters/BeginAppleSignInParams.ts index d2b84b5c9cfe..c427d99fcef9 100644 --- a/src/libs/API/parameters/BeginAppleSignInParams.ts +++ b/src/libs/API/parameters/BeginAppleSignInParams.ts @@ -1,5 +1,8 @@ +import type {ValueOf} from 'type-fest'; +import type CONST from '@src/CONST'; + type BeginAppleSignInParams = { - idToken: typeof idToken; + idToken: string | undefined | null; preferredLocale: ValueOf | null; }; diff --git a/src/libs/API/parameters/BeginGoogleSignInParams.ts b/src/libs/API/parameters/BeginGoogleSignInParams.ts index 27fe8830e9f4..fae84d76b0d9 100644 --- a/src/libs/API/parameters/BeginGoogleSignInParams.ts +++ b/src/libs/API/parameters/BeginGoogleSignInParams.ts @@ -1,3 +1,6 @@ +import type {ValueOf} from 'type-fest'; +import type CONST from '@src/CONST'; + type BeginGoogleSignInParams = { token: string | null; preferredLocale: ValueOf | null; diff --git a/src/libs/API/parameters/ResolveActionableMentionWhisperParams.ts b/src/libs/API/parameters/ResolveActionableMentionWhisperParams.ts index bdfc965a7307..87dfc934eb5f 100644 --- a/src/libs/API/parameters/ResolveActionableMentionWhisperParams.ts +++ b/src/libs/API/parameters/ResolveActionableMentionWhisperParams.ts @@ -1,3 +1,6 @@ +import type {ValueOf} from 'type-fest'; +import type CONST from '@src/CONST'; + type ResolveActionableMentionWhisperParams = { reportActionID: string; resolution: ValueOf; diff --git a/src/libs/API/parameters/SignInUserWithLinkParams.ts b/src/libs/API/parameters/SignInUserWithLinkParams.ts index ea7f0ff2ee05..ae3589d4e305 100644 --- a/src/libs/API/parameters/SignInUserWithLinkParams.ts +++ b/src/libs/API/parameters/SignInUserWithLinkParams.ts @@ -1,3 +1,6 @@ +import type {ValueOf} from 'type-fest'; +import type CONST from '@src/CONST'; + type SignInUserWithLinkParams = { accountID: number; validateCode?: string; diff --git a/src/libs/API/parameters/UpdateChatPriorityModeParams.ts b/src/libs/API/parameters/UpdateChatPriorityModeParams.ts index d033729e9189..8bbb7bf6943c 100644 --- a/src/libs/API/parameters/UpdateChatPriorityModeParams.ts +++ b/src/libs/API/parameters/UpdateChatPriorityModeParams.ts @@ -1,3 +1,6 @@ +import type {ValueOf} from 'type-fest'; +import type CONST from '@src/CONST'; + type UpdateChatPriorityModeParams = { value: ValueOf; automatic: boolean; diff --git a/src/libs/API/parameters/UpdateHomeAddressParams.ts b/src/libs/API/parameters/UpdateHomeAddressParams.ts index 30cc933069df..7de71fe67c58 100644 --- a/src/libs/API/parameters/UpdateHomeAddressParams.ts +++ b/src/libs/API/parameters/UpdateHomeAddressParams.ts @@ -7,4 +7,5 @@ type UpdateHomeAddressParams = { addressCountry: string; addressStateLong?: string; }; + export default UpdateHomeAddressParams; diff --git a/src/libs/API/parameters/UpdateLegalNameParams.ts b/src/libs/API/parameters/UpdateLegalNameParams.ts index ac9ad77aabf5..2c55cec13cc4 100644 --- a/src/libs/API/parameters/UpdateLegalNameParams.ts +++ b/src/libs/API/parameters/UpdateLegalNameParams.ts @@ -2,4 +2,5 @@ type UpdateLegalNameParams = { legalFirstName: string; legalLastName: string; }; + export default UpdateLegalNameParams; diff --git a/src/libs/API/parameters/UpdatePronounsParams.ts b/src/libs/API/parameters/UpdatePronounsParams.ts index 711cb9aa84f4..f7ac30a5b2ef 100644 --- a/src/libs/API/parameters/UpdatePronounsParams.ts +++ b/src/libs/API/parameters/UpdatePronounsParams.ts @@ -1,4 +1,5 @@ type UpdatePronounsParams = { pronouns: string; }; + export default UpdatePronounsParams; diff --git a/src/libs/API/parameters/UpdateSelectedTimezoneParams.ts b/src/libs/API/parameters/UpdateSelectedTimezoneParams.ts index f8ef26ef147e..595e14f7c54c 100644 --- a/src/libs/API/parameters/UpdateSelectedTimezoneParams.ts +++ b/src/libs/API/parameters/UpdateSelectedTimezoneParams.ts @@ -1,4 +1,5 @@ type UpdateSelectedTimezoneParams = { timezone: string; }; + export default UpdateSelectedTimezoneParams; diff --git a/src/libs/API/parameters/UpdateUserAvatarParams.ts b/src/libs/API/parameters/UpdateUserAvatarParams.ts index cc45e50ba0b6..2dce38e8763c 100644 --- a/src/libs/API/parameters/UpdateUserAvatarParams.ts +++ b/src/libs/API/parameters/UpdateUserAvatarParams.ts @@ -1,3 +1,5 @@ +import type {CustomRNImageManipulatorResult} from '@libs/cropOrRotateImage/types'; + type UpdateUserAvatarParams = { file: File | CustomRNImageManipulatorResult; }; diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index 8c6733f1ca11..e503ba29c5bb 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -1,26 +1,75 @@ +export type {default as ActivatePhysicalExpensifyCardParams} from './ActivatePhysicalExpensifyCardParams'; +export type {default as AddNewContactMethodParams} from './AddNewContactMethodParams'; +export type {default as AddPaymentCardParams} from './AddPaymentCardParams'; +export type {default as AddPersonalBankAccountParams} from './AddPersonalBankAccountParams'; +export type {default as AddSchoolPrincipalParams} from './AddSchoolPrincipalParams'; export type {default as AuthenticatePusherParams} from './AuthenticatePusherParams'; -export type {default as HandleRestrictedEventParams} from './HandleRestrictedEventParams'; -export type {default as OpenOldDotLinkParams} from './OpenOldDotLinkParams'; -export type {default as OpenReportParams} from './OpenReportParams'; -export type {default as RevealExpensifyCardDetailsParams} from './RevealExpensifyCardDetailsParams'; +export type {default as BankAccountHandlePlaidErrorParams} from './BankAccountHandlePlaidErrorParams'; +export type {default as BeginAppleSignInParams} from './BeginAppleSignInParams'; +export type {default as BeginGoogleSignInParams} from './BeginGoogleSignInParams'; +export type {default as BeginSignInParams} from './BeginSignInParams'; +export type {default as CloseAccountParams} from './CloseAccountParams'; +export type {default as ConnectBankAccountManuallyParams} from './ConnectBankAccountManuallyParams'; +export type {default as ConnectBankAccountWithPlaidParams} from './ConnectBankAccountWithPlaidParams'; +export type {default as DeleteContactMethodParams} from './DeleteContactMethodParams'; +export type {default as DeletePaymentBankAccountParams} from './DeletePaymentBankAccountParams'; +export type {default as DeletePaymentCardParams} from './DeletePaymentCardParams'; +export type {default as ExpandURLPreviewParams} from './ExpandURLPreviewParams'; export type {default as GetMissingOnyxMessagesParams} from './GetMissingOnyxMessagesParams'; +export type {default as GetNewerActionsParams} from './GetNewerActionsParams'; +export type {default as GetOlderActionsParams} from './GetOlderActionsParams'; +export type {default as GetReportPrivateNoteParams} from './GetReportPrivateNoteParams'; +export type {default as GetRouteForDraftParams} from './GetRouteForDraftParams'; +export type {default as GetRouteParams} from './GetRouteParams'; +export type {default as GetStatementPDFParams} from './GetStatementPDFParams'; +export type {default as HandleRestrictedEventParams} from './HandleRestrictedEventParams'; +export type {default as LogOutParams} from './LogOutParams'; +export type {default as MakeDefaultPaymentMethodParams} from './MakeDefaultPaymentMethodParams'; export type {default as OpenAppParams} from './OpenAppParams'; +export type {default as OpenOldDotLinkParams} from './OpenOldDotLinkParams'; +export type {default as OpenPlaidBankAccountSelectorParams} from './OpenPlaidBankAccountSelectorParams'; +export type {default as OpenPlaidBankLoginParams} from './OpenPlaidBankLoginParams'; export type {default as OpenProfileParams} from './OpenProfileParams'; -export type {default as ReconnectAppParams} from './ReconnectAppParams'; -export type {default as UpdatePreferredLocaleParams} from './UpdatePreferredLocaleParams'; -export type {default as OpenReimbursementAccountPageParams} from './OpenReimbursementAccountPageParams'; export type {default as OpenPublicProfilePageParams} from './OpenPublicProfilePageParams'; -export type {default as OpenPlaidBankLoginParams} from './OpenPlaidBankLoginParams'; -export type {default as OpenPlaidBankAccountSelectorParams} from './OpenPlaidBankAccountSelectorParams'; -export type {default as GetOlderActionsParams} from './GetOlderActionsParams'; -export type {default as GetNewerActionsParams} from './GetNewerActionsParams'; -export type {default as ExpandURLPreviewParams} from './ExpandURLPreviewParams'; -export type {default as GetReportPrivateNoteParams} from './GetReportPrivateNoteParams'; +export type {default as OpenReimbursementAccountPageParams} from './OpenReimbursementAccountPageParams'; +export type {default as OpenReportParams} from './OpenReportParams'; export type {default as OpenRoomMembersPageParams} from './OpenRoomMembersPageParams'; +export type {default as PaymentCardParams} from './PaymentCardParams'; +export type {default as ReconnectAppParams} from './ReconnectAppParams'; +export type {default as ReferTeachersUniteVolunteerParams} from './ReferTeachersUniteVolunteerParams'; +export type {default as ReportVirtualExpensifyCardFraudParams} from './ReportVirtualExpensifyCardFraudParams'; +export type {default as RequestContactMethodValidateCodeParams} from './RequestContactMethodValidateCodeParams'; +export type {default as RequestNewValidateCodeParams} from './RequestNewValidateCodeParams'; +export type {default as RequestPhysicalExpensifyCardParams} from './RequestPhysicalExpensifyCardParams'; +export type {default as RequestReplacementExpensifyCardParams} from './RequestReplacementExpensifyCardParams'; +export type {default as RequestUnlinkValidationLinkParams} from './RequestUnlinkValidationLinkParams'; +export type {default as ResendValidationLinkParams} from './ResendValidationLinkParams'; +export type {default as ResolveActionableMentionWhisperParams} from './ResolveActionableMentionWhisperParams'; +export type {default as RevealExpensifyCardDetailsParams} from './RevealExpensifyCardDetailsParams'; export type {default as SearchForReportsParams} from './SearchForReportsParams'; export type {default as SendPerformanceTimingParams} from './SendPerformanceTimingParams'; -export type {default as GetRouteParams} from './GetRouteParams'; -export type {default as GetRouteForDraftParams} from './GetRouteForDraftParams'; -export type {default as GetStatementPDFParams} from './GetStatementPDFParams'; -export type {default as BeginSignInParams} from './BeginSignInParams'; +export type {default as SetContactMethodAsDefaultParams} from './SetContactMethodAsDefaultParams'; +export type {default as SignInUserWithLinkParams} from './SignInUserWithLinkParams'; export type {default as SignInWithShortLivedAuthTokenParams} from './SignInWithShortLivedAuthTokenParams'; +export type {default as UnlinkLoginParams} from './UnlinkLoginParams'; +export type {default as UpdateAutomaticTimezoneParams} from './UpdateAutomaticTimezoneParams'; +export type {default as UpdateChatPriorityModeParams} from './UpdateChatPriorityModeParams'; +export type {default as UpdateDateOfBirthParams} from './UpdateDateOfBirthParams'; +export type {default as UpdateDisplayNameParams} from './UpdateDisplayNameParams'; +export type {default as UpdateFrequentlyUsedEmojisParams} from './UpdateFrequentlyUsedEmojisParams'; +export type {default as UpdateHomeAddressParams} from './UpdateHomeAddressParams'; +export type {default as UpdateLegalNameParams} from './UpdateLegalNameParams'; +export type {default as UpdateNewsletterSubscriptionParams} from './UpdateNewsletterSubscriptionParams'; +export type {default as UpdatePersonalInformationForBankAccountParams} from './UpdatePersonalInformationForBankAccountParams'; +export type {default as UpdatePreferredEmojiSkinToneParams} from './UpdatePreferredEmojiSkinToneParams'; +export type {default as UpdatePreferredLocaleParams} from './UpdatePreferredLocaleParams'; +export type {default as UpdatePronounsParams} from './UpdatePronounsParams'; +export type {default as UpdateSelectedTimezoneParams} from './UpdateSelectedTimezoneParams'; +export type {default as UpdateStatusParams} from './UpdateStatusParams'; +export type {default as UpdateThemeParams} from './UpdateThemeParams'; +export type {default as UpdateUserAvatarParams} from './UpdateUserAvatarParams'; +export type {default as ValidateBankAccountWithTransactionsParams} from './ValidateBankAccountWithTransactionsParams'; +export type {default as ValidateLoginParams} from './ValidateLoginParams'; +export type {default as ValidateSecondaryLoginParams} from './ValidateSecondaryLoginParams'; +export type {default as ValidateTwoFactorAuthParams} from './ValidateTwoFactorAuthParams'; +export type {default as VerifyIdentityForBankAccountParams} from './VerifyIdentityForBankAccountParams'; From fa5891dcaf28901d72e6e80e3eee7b4657259d4e Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Tue, 16 Jan 2024 20:37:23 +0100 Subject: [PATCH 256/580] Add remaining write commands values to the WriteCommandParameters mapping --- ...PersonalInformationForBankAccountParams.ts | 16 +-- src/libs/API/types.ts | 126 ++++++++++++++++++ src/libs/actions/BankAccounts.ts | 14 +- src/libs/actions/PersonalDetails.ts | 6 +- src/types/onyx/ReimbursementAccountDraft.ts | 14 +- 5 files changed, 155 insertions(+), 21 deletions(-) diff --git a/src/libs/API/parameters/UpdatePersonalInformationForBankAccountParams.ts b/src/libs/API/parameters/UpdatePersonalInformationForBankAccountParams.ts index 1555500e84ee..4de2e462fc7a 100644 --- a/src/libs/API/parameters/UpdatePersonalInformationForBankAccountParams.ts +++ b/src/libs/API/parameters/UpdatePersonalInformationForBankAccountParams.ts @@ -1,15 +1,5 @@ -type UpdatePersonalInformationForBankAccountParams = { - firstName?: string; - lastName?: string; - requestorAddressStreet?: string; - requestorAddressCity?: string; - requestorAddressState?: string; - requestorAddressZipCode?: string; - dob?: string | Date; - ssnLast4?: string; - isControllingOfficer?: boolean; - isOnfidoSetupComplete?: boolean; - onfidoData?: OnfidoData; -}; +import type {RequestorStepProps} from '@src/types/onyx/ReimbursementAccountDraft'; + +type UpdatePersonalInformationForBankAccountParams = RequestorStepProps; export default UpdatePersonalInformationForBankAccountParams; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index 4b71d67613a2..80655c6f606e 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -2,8 +2,20 @@ import type {ValueOf} from 'type-fest'; import type CONST from '@src/CONST'; import type {EmptyObject} from '@src/types/utils/EmptyObject'; import type { + ActivatePhysicalExpensifyCardParams, + AddNewContactMethodParams, + AddPaymentCardParams, + AddPersonalBankAccountParams, + AddSchoolPrincipalParams, AuthenticatePusherParams, + BankAccountHandlePlaidErrorParams, BeginSignInParams, + CloseAccountParams, + ConnectBankAccountManuallyParams, + ConnectBankAccountWithPlaidParams, + DeleteContactMethodParams, + DeletePaymentBankAccountParams, + DeletePaymentCardParams, ExpandURLPreviewParams, GetMissingOnyxMessagesParams, GetNewerActionsParams, @@ -13,6 +25,8 @@ import type { GetRouteParams, GetStatementPDFParams, HandleRestrictedEventParams, + LogOutParams, + MakeDefaultPaymentMethodParams, OpenAppParams, OpenOldDotLinkParams, OpenPlaidBankAccountSelectorParams, @@ -23,12 +37,43 @@ import type { OpenReportParams, OpenRoomMembersPageParams, ReconnectAppParams, + ReferTeachersUniteVolunteerParams, + ReportVirtualExpensifyCardFraudParams, + RequestContactMethodValidateCodeParams, + RequestNewValidateCodeParams, + RequestPhysicalExpensifyCardParams, + RequestReplacementExpensifyCardParams, + RequestUnlinkValidationLinkParams, + ResolveActionableMentionWhisperParams, RevealExpensifyCardDetailsParams, SearchForReportsParams, SendPerformanceTimingParams, + SetContactMethodAsDefaultParams, + SignInUserWithLinkParams, SignInWithShortLivedAuthTokenParams, + UnlinkLoginParams, + UpdateAutomaticTimezoneParams, + UpdateChatPriorityModeParams, + UpdateDateOfBirthParams, + UpdateDisplayNameParams, + UpdateFrequentlyUsedEmojisParams, + UpdateHomeAddressParams, + UpdateLegalNameParams, + UpdateNewsletterSubscriptionParams, + UpdatePersonalInformationForBankAccountParams, + UpdatePreferredEmojiSkinToneParams, UpdatePreferredLocaleParams, + UpdatePronounsParams, + UpdateSelectedTimezoneParams, + UpdateStatusParams, + UpdateThemeParams, + UpdateUserAvatarParams, + ValidateBankAccountWithTransactionsParams, + ValidateLoginParams, + ValidateSecondaryLoginParams, + VerifyIdentityForBankAccountParams, } from './parameters'; +import type SignInUserParams from './parameters/SignInUserParams'; type ApiRequestWithSideEffects = ValueOf; @@ -131,6 +176,87 @@ type WriteCommandParameters = { [WRITE_COMMANDS.OPEN_PROFILE]: OpenProfileParams; [WRITE_COMMANDS.HANDLE_RESTRICTED_EVENT]: HandleRestrictedEventParams; [WRITE_COMMANDS.OPEN_REPORT]: OpenReportParams; + [WRITE_COMMANDS.DELETE_PAYMENT_BANK_ACCOUNT]: DeletePaymentBankAccountParams; + [WRITE_COMMANDS.UPDATE_PERSONAL_INFORMATION_FOR_BANK_ACCOUNT]: UpdatePersonalInformationForBankAccountParams; + [WRITE_COMMANDS.VALIDATE_BANK_ACCOUNT_WITH_TRANSACTIONS]: ValidateBankAccountWithTransactionsParams; + [WRITE_COMMANDS.UPDATE_COMPANY_INFORMATION_FOR_BANK_ACCOUNT]: UpdateCompanyInformationForBankAccountParams; + [WRITE_COMMANDS.UPDATE_BENEFICIAL_OWNERS_FOR_BANK_ACCOUNT]: UpdateBeneficialOwnersForBankAccountParams; + [WRITE_COMMANDS.CONNECT_BANK_ACCOUNT_MANUALLY]: ConnectBankAccountManuallyParams; + [WRITE_COMMANDS.VERIFY_IDENTITY_FOR_BANK_ACCOUNT]: VerifyIdentityForBankAccountParams; + [WRITE_COMMANDS.BANK_ACCOUNT_HANDLE_PLAID_ERROR]: BankAccountHandlePlaidErrorParams; + [WRITE_COMMANDS.REPORT_VIRTUAL_EXPENSIFY_CARD_FRAUD]: ReportVirtualExpensifyCardFraudParams; + [WRITE_COMMANDS.REQUEST_REPLACEMENT_EXPENSIFY_CARD]: RequestReplacementExpensifyCardParams; + [WRITE_COMMANDS.ACTIVATE_PHYSICAL_EXPENSIFY_CARD]: ActivatePhysicalExpensifyCardParams; + [WRITE_COMMANDS.MAKE_DEFAULT_PAYMENT_METHOD]: MakeDefaultPaymentMethodParams; + [WRITE_COMMANDS.ADD_PAYMENT_CARD]: AddPaymentCardParams; + [WRITE_COMMANDS.DELETE_PAYMENT_CARD]: DeletePaymentCardParams; + [WRITE_COMMANDS.UPDATE_PRONOUNS]: UpdatePronounsParams; + [WRITE_COMMANDS.UPDATE_DISPLAY_NAME]: UpdateDisplayNameParams; + [WRITE_COMMANDS.UPDATE_LEGAL_NAME]: UpdateLegalNameParams; + [WRITE_COMMANDS.UPDATE_DATE_OF_BIRTH]: UpdateDateOfBirthParams; + [WRITE_COMMANDS.UPDATE_HOME_ADDRESS]: UpdateHomeAddressParams; + [WRITE_COMMANDS.UPDATE_AUTOMATIC_TIMEZONE]: UpdateAutomaticTimezoneParams; + [WRITE_COMMANDS.UPDATE_SELECTED_TIMEZONE]: UpdateSelectedTimezoneParams; + [WRITE_COMMANDS.UPDATE_USER_AVATAR]: UpdateUserAvatarParams; + [WRITE_COMMANDS.DELETE_USER_AVATAR]: EmptyObject; + [WRITE_COMMANDS.REFER_TEACHERS_UNITE_VOLUNTEER]: ReferTeachersUniteVolunteerParams; + [WRITE_COMMANDS.ADD_SCHOOL_PRINCIPAL]: AddSchoolPrincipalParams; + [WRITE_COMMANDS.CLOSE_ACCOUNT]: CloseAccountParams; + [WRITE_COMMANDS.REQUEST_CONTACT_METHOD_VALIDATE_CODE]: RequestContactMethodValidateCodeParams; + [WRITE_COMMANDS.UPDATE_NEWSLETTER_SUBSCRIPTION]: UpdateNewsletterSubscriptionParams; + [WRITE_COMMANDS.DELETE_CONTACT_METHOD]: DeleteContactMethodParams; + [WRITE_COMMANDS.ADD_NEW_CONTACT_METHOD]: AddNewContactMethodParams; + [WRITE_COMMANDS.VALIDATE_LOGIN]: ValidateLoginParams; + [WRITE_COMMANDS.VALIDATE_SECONDARY_LOGIN]: ValidateSecondaryLoginParams; + [WRITE_COMMANDS.UPDATE_PREFERRED_EMOJI_SKIN_TONE]: UpdatePreferredEmojiSkinToneParams; + [WRITE_COMMANDS.UPDATE_FREQUENTLY_USED_EMOJIS]: UpdateFrequentlyUsedEmojisParams; + [WRITE_COMMANDS.UPDATE_CHAT_PRIORITY_MODE]: UpdateChatPriorityModeParams; + [WRITE_COMMANDS.SET_CONTACT_METHOD_AS_DEFAULT]: SetContactMethodAsDefaultParams; + [WRITE_COMMANDS.UPDATE_THEME]: UpdateThemeParams; + [WRITE_COMMANDS.UPDATE_STATUS]: UpdateStatusParams; + [WRITE_COMMANDS.CLEAR_STATUS]: ClearStatusParams; + [WRITE_COMMANDS.UPDATE_PERSONAL_DETAILS_FOR_WALLET]: UpdatePersonalDetailsForWalletParams; + [WRITE_COMMANDS.VERIFY_IDENTITY]: VerifyIdentityParams; + [WRITE_COMMANDS.ACCEPT_WALLET_TERMS]: AcceptWalletTermsParams; + [WRITE_COMMANDS.ANSWER_QUESTIONS_FOR_WALLET]: AnswerQuestionsForWalletParams; + [WRITE_COMMANDS.REQUEST_PHYSICAL_EXPENSIFY_CARD]: RequestPhysicalExpensifyCardParams; + [WRITE_COMMANDS.LOG_OUT]: LogOutParams; + [WRITE_COMMANDS.REQUEST_ACCOUNT_VALIDATION_LINK]: RequestAccountValidationLinkParams; + [WRITE_COMMANDS.REQUEST_NEW_VALIDATE_CODE]: RequestNewValidateCodeParams; + [WRITE_COMMANDS.SIGN_IN_WITH_APPLE]: SignInWithAppleParams; + [WRITE_COMMANDS.SIGN_IN_WITH_GOOGLE]: SignInWithGoogleParams; + [WRITE_COMMANDS.SIGN_IN_USER]: SignInUserParams; + [WRITE_COMMANDS.SIGN_IN_USER_WITH_LINK]: SignInUserWithLinkParams; + [WRITE_COMMANDS.REQUEST_UNLINK_VALIDATION_LINK]: RequestUnlinkValidationLinkParams; + [WRITE_COMMANDS.UNLINK_LOGIN]: UnlinkLoginParams; + [WRITE_COMMANDS.ENABLE_TWO_FACTOR_AUTH]: EnableTwoFactorAuthParams; + [WRITE_COMMANDS.DISABLE_TWO_FACTOR_AUTH]: DisableTwoFactorAuthParams; + [WRITE_COMMANDS.TWO_FACTOR_AUTH_VALIDATE]: TwoFactorAuthValidateParams; + [WRITE_COMMANDS.ADD_COMMENT]: AddCommentParams; + [WRITE_COMMANDS.ADD_ATTACHMENT]: AddAttachmentParams; + [WRITE_COMMANDS.CONNECT_BANK_ACCOUNT_WITH_PLAID]: ConnectBankAccountWithPlaidParams; + [WRITE_COMMANDS.ADD_PERSONAL_BANK_ACCOUNT]: AddPersonalBankAccountParams; + [WRITE_COMMANDS.OPT_IN_TO_PUSH_NOTIFICATIONS]: OptInToPushNotificationsParams; + [WRITE_COMMANDS.OPT_OUT_OF_PUSH_NOTIFICATIONS]: OptOutOfPushNotificationsParams; + [WRITE_COMMANDS.RECONNECT_TO_REPORT]: ReconnectToReportParams; + [WRITE_COMMANDS.READ_NEWEST_ACTION]: ReadNewestActionParams; + [WRITE_COMMANDS.MARK_AS_UNREAD]: MarkAsUnreadParams; + [WRITE_COMMANDS.TOGGLE_PINNED_CHAT]: TogglePinnedChatParams; + [WRITE_COMMANDS.DELETE_COMMENT]: DeleteCommentParams; + [WRITE_COMMANDS.UPDATE_COMMENT]: UpdateCommentParams; + [WRITE_COMMANDS.UPDATE_REPORT_NOTIFICATION_PREFERENCE]: UpdateReportNotificationPreferenceParams; + [WRITE_COMMANDS.UPDATE_WELCOME_MESSAGE]: UpdateWelcomeMessageParams; + [WRITE_COMMANDS.UPDATE_REPORT_WRITE_CAPABILITY]: UpdateReportWriteCapabilityParams; + [WRITE_COMMANDS.ADD_WORKSPACE_ROOM]: AddWorkspaceRoomParams; + [WRITE_COMMANDS.UPDATE_POLICY_ROOM_NAME]: UpdatePolicyRoomNameParams; + [WRITE_COMMANDS.ADD_EMOJI_REACTION]: AddEmojiReactionParams; + [WRITE_COMMANDS.REMOVE_EMOJI_REACTION]: RemoveEmojiReactionParams; + [WRITE_COMMANDS.LEAVE_ROOM]: LeaveRoomParams; + [WRITE_COMMANDS.INVITE_TO_ROOM]: InviteToRoomParams; + [WRITE_COMMANDS.REMOVE_FROM_ROOM]: RemoveFromRoomParams; + [WRITE_COMMANDS.FLAG_COMMENT]: FlagCommentParams; + [WRITE_COMMANDS.UPDATE_REPORT_PRIVATE_NOTE]: UpdateReportPrivateNoteParams; + [WRITE_COMMANDS.RESOLVE_ACTIONABLE_MENTION_WHISPER]: ResolveActionableMentionWhisperParams; }; const READ_COMMANDS = { diff --git a/src/libs/actions/BankAccounts.ts b/src/libs/actions/BankAccounts.ts index 4361dec74da1..97a3829e74b8 100644 --- a/src/libs/actions/BankAccounts.ts +++ b/src/libs/actions/BankAccounts.ts @@ -1,6 +1,16 @@ import Onyx from 'react-native-onyx'; import * as API from '@libs/API'; -import type {OpenReimbursementAccountPageParams} from '@libs/API/parameters'; +import type { + AddPersonalBankAccountParams, + BankAccountHandlePlaidErrorParams, + ConnectBankAccountManuallyParams, + ConnectBankAccountWithPlaidParams, + DeletePaymentBankAccountParams, + OpenReimbursementAccountPageParams, + UpdatePersonalInformationForBankAccountParams, + ValidateBankAccountWithTransactionsParams, + VerifyIdentityForBankAccountParams, +} from '@libs/API/parameters'; import {READ_COMMANDS, WRITE_COMMANDS} from '@libs/API/types'; import * as ErrorUtils from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; @@ -10,7 +20,7 @@ import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type PlaidBankAccount from '@src/types/onyx/PlaidBankAccount'; import type {BankAccountStep, BankAccountSubStep} from '@src/types/onyx/ReimbursementAccount'; -import type {ACHContractStepProps, BankAccountStepProps, CompanyStepProps, OnfidoData, ReimbursementAccountProps, RequestorStepProps} from '@src/types/onyx/ReimbursementAccountDraft'; +import type {ACHContractStepProps, BankAccountStepProps, CompanyStepProps, OnfidoData, ReimbursementAccountProps} from '@src/types/onyx/ReimbursementAccountDraft'; import type {OnyxData} from '@src/types/onyx/Request'; import * as ReimbursementAccount from './ReimbursementAccount'; diff --git a/src/libs/actions/PersonalDetails.ts b/src/libs/actions/PersonalDetails.ts index de4ed0ab10fb..32a69e507fae 100644 --- a/src/libs/actions/PersonalDetails.ts +++ b/src/libs/actions/PersonalDetails.ts @@ -481,11 +481,7 @@ function deleteAvatar() { }, ]; - type DeleteUserAvatarParams = Record; - - const parameters: DeleteUserAvatarParams = {}; - - API.write(WRITE_COMMANDS.DELETE_USER_AVATAR, parameters, {optimisticData, failureData}); + API.write(WRITE_COMMANDS.DELETE_USER_AVATAR, {}, {optimisticData, failureData}); } /** diff --git a/src/types/onyx/ReimbursementAccountDraft.ts b/src/types/onyx/ReimbursementAccountDraft.ts index b86f7e9dcb62..cab1283943bc 100644 --- a/src/types/onyx/ReimbursementAccountDraft.ts +++ b/src/types/onyx/ReimbursementAccountDraft.ts @@ -23,7 +23,19 @@ type CompanyStepProps = { hasNoConnectionToCannabis?: boolean; }; - +type RequestorStepProps = { + firstName?: string; + lastName?: string; + requestorAddressStreet?: string; + requestorAddressCity?: string; + requestorAddressState?: string; + requestorAddressZipCode?: string; + dob?: string | Date; + ssnLast4?: string; + isControllingOfficer?: boolean; + isOnfidoSetupComplete?: boolean; + onfidoData?: OnfidoData; +}; type ACHContractStepProps = { ownsMoreThan25Percent?: boolean; From 9710b716dae9078aff6e36cb32624af6f7511dc2 Mon Sep 17 00:00:00 2001 From: Marc Glasser Date: Tue, 16 Jan 2024 16:15:13 -1000 Subject: [PATCH 257/580] Update comment Add some methods to check if the report is valid Fix ts stuff --- src/libs/ReportUtils.ts | 25 +++++++++++++++++++++++++ src/libs/actions/PriorityMode.ts | 13 ++++++++++++- 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index e4b82aa36c7a..317ace1d7d4e 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -4317,6 +4317,29 @@ function isGroupChat(report: OnyxEntry): boolean { ); } +/** + * Assume any report without a reportID is unusable. + */ +function isValidReport(report?: OnyxEntry): boolean { + return Boolean(!report?.reportID); +} + +/** + * Check to see if we are a participant of this report. + */ +function isReportParticipant(report?: OnyxEntry, accountID: number): boolean { + if (!accountID) { + return false; + } + + // We are not the owner or the manager or a participant + if (report?.ownerAccountID !== accountID && report?.managerID !== accountID && !(report?.participantAccountIDs ?? []).includes(accountID)) { + return false; + } + + return true; +} + function shouldUseFullTitleToDisplay(report: OnyxEntry): boolean { return isMoneyRequestReport(report) || isPolicyExpenseChat(report) || isChatRoom(report) || isChatThread(report) || isTaskReport(report); } @@ -4613,6 +4636,8 @@ export { shouldDisplayThreadReplies, shouldDisableThread, getChildReportNotificationPreference, + isReportParticipant, + isValidReport, }; export type {ExpenseOriginalMessage, OptionData, OptimisticChatReport, OptimisticCreatedReportAction}; diff --git a/src/libs/actions/PriorityMode.ts b/src/libs/actions/PriorityMode.ts index 1d38d09e08a1..1e86c8dd8f1a 100644 --- a/src/libs/actions/PriorityMode.ts +++ b/src/libs/actions/PriorityMode.ts @@ -6,6 +6,7 @@ import Log from '@libs/Log'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Report} from '@src/types/onyx'; +import * as ReportUtils from '@libs/ReportUtils'; /** * This actions file is used to automatically switch a user into #focus mode when they exceed a certain number of reports. We do this primarily for performance reasons. @@ -120,7 +121,17 @@ function tryFocusModeUpdate() { return; } - const reportCount = Object.keys(allReports ?? {}).length; + const validReports = []; + Object.keys(allReports ?? {}).forEach((key) => { + const report = allReports?.[key]; + if (ReportUtils.isValidReport(report) || !ReportUtils.isReportParticipant(currentUserAccountID ?? 0, report)) { + return; + } + + validReports.push(report); + }); + + const reportCount = validReports.length; if (reportCount < CONST.REPORT.MAX_COUNT_BEFORE_FOCUS_UPDATE) { Log.info('Not switching user to optimized focus mode as they do not have enough reports', false, {reportCount}); return; From 3c242ba0986a9e6365b7ecb1ea688a8774a37e52 Mon Sep 17 00:00:00 2001 From: Marc Glasser Date: Tue, 16 Jan 2024 16:39:06 -1000 Subject: [PATCH 258/580] Fix incorrect logic --- src/libs/ReportUtils.ts | 4 ++-- src/libs/actions/PriorityMode.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 317ace1d7d4e..659decf83bb4 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -4321,13 +4321,13 @@ function isGroupChat(report: OnyxEntry): boolean { * Assume any report without a reportID is unusable. */ function isValidReport(report?: OnyxEntry): boolean { - return Boolean(!report?.reportID); + return Boolean(report?.reportID); } /** * Check to see if we are a participant of this report. */ -function isReportParticipant(report?: OnyxEntry, accountID: number): boolean { +function isReportParticipant(accountID: number, report?: OnyxEntry): boolean { if (!accountID) { return false; } diff --git a/src/libs/actions/PriorityMode.ts b/src/libs/actions/PriorityMode.ts index 1e86c8dd8f1a..a36178c7c7c1 100644 --- a/src/libs/actions/PriorityMode.ts +++ b/src/libs/actions/PriorityMode.ts @@ -124,7 +124,7 @@ function tryFocusModeUpdate() { const validReports = []; Object.keys(allReports ?? {}).forEach((key) => { const report = allReports?.[key]; - if (ReportUtils.isValidReport(report) || !ReportUtils.isReportParticipant(currentUserAccountID ?? 0, report)) { + if (!ReportUtils.isValidReport(report) || !ReportUtils.isReportParticipant(currentUserAccountID ?? 0, report)) { return; } From fa4259948cbf008b7395b44fc2f954193fe032d3 Mon Sep 17 00:00:00 2001 From: Marc Glasser Date: Tue, 16 Jan 2024 16:39:59 -1000 Subject: [PATCH 259/580] Run prettier --- src/libs/actions/PriorityMode.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libs/actions/PriorityMode.ts b/src/libs/actions/PriorityMode.ts index a36178c7c7c1..52ec43279e66 100644 --- a/src/libs/actions/PriorityMode.ts +++ b/src/libs/actions/PriorityMode.ts @@ -3,10 +3,10 @@ import type {OnyxCollection} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import * as CollectionUtils from '@libs/CollectionUtils'; import Log from '@libs/Log'; +import * as ReportUtils from '@libs/ReportUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Report} from '@src/types/onyx'; -import * as ReportUtils from '@libs/ReportUtils'; /** * This actions file is used to automatically switch a user into #focus mode when they exceed a certain number of reports. We do this primarily for performance reasons. @@ -121,7 +121,7 @@ function tryFocusModeUpdate() { return; } - const validReports = []; + const validReports = []; Object.keys(allReports ?? {}).forEach((key) => { const report = allReports?.[key]; if (!ReportUtils.isValidReport(report) || !ReportUtils.isReportParticipant(currentUserAccountID ?? 0, report)) { From a1f468ca1176b27202d461a9c83f7ae97c92948a Mon Sep 17 00:00:00 2001 From: tienifr Date: Wed, 17 Jan 2024 12:26:33 +0700 Subject: [PATCH 260/580] fix hasOutstandingChildRequest --- src/libs/actions/IOU.js | 39 +++++++++++++++++++++++++++------------ 1 file changed, 27 insertions(+), 12 deletions(-) diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index 06adaab3c28f..93fd4d5827ad 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -311,6 +311,27 @@ function getReceiptError(receipt, filename, isScanRequest = true) { : ErrorUtils.getMicroSecondOnyxErrorObject({error: CONST.IOU.RECEIPT_ERROR, source: receipt.source, filename}); } +/** + * @param {Object} [policy] + * @param {Boolean} needsToBeManuallySubmitted + * @returns {Object} + */ +function getOutstandingChildRequest(policy, needsToBeManuallySubmitted) { + if (!needsToBeManuallySubmitted) { + return { + hasOutstandingChildRequest: false, + }; + } + + if (PolicyUtils.isPolicyAdmin(policy)) { + return { + hasOutstandingChildRequest: true, + }; + } + + return {}; +} + /** * Builds the Onyx data for a money request. * @@ -329,7 +350,7 @@ function getReceiptError(receipt, filename, isScanRequest = true) { * @param {Object} policy - May be undefined, an empty object, or an object matching the Policy type (src/types/onyx/Policy.ts) * @param {Array} policyTags * @param {Array} policyCategories - * @param {Boolean} hasOutstandingChildRequest + * @param {Boolean} needsToBeManuallySubmitted * @returns {Array} - An array containing the optimistic data, success data, and failure data. */ function buildOnyxDataForMoneyRequest( @@ -348,11 +369,10 @@ function buildOnyxDataForMoneyRequest( policy, policyTags, policyCategories, - hasOutstandingChildRequest = false, + needsToBeManuallySubmitted = true, ) { - const isPolicyAdmin = PolicyUtils.isPolicyAdmin(policy); - const isScanRequest = TransactionUtils.isScanRequest(transaction); + const hasOutstandingChildRequest = getOutstandingChildRequest(needsToBeManuallySubmitted, policy); const optimisticData = [ { // Use SET for new reports because it doesn't exist yet, is faster and we need the data to be available when we navigate to the chat page @@ -363,7 +383,6 @@ function buildOnyxDataForMoneyRequest( lastReadTime: DateUtils.getDBTime(), lastMessageTranslationKey: '', iouReportID: iouReport.reportID, - ...(isPolicyAdmin ? {hasOutstandingChildRequest: true} : {}), hasOutstandingChildRequest, ...(isNewChatReport ? {pendingFields: {createChat: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD}} : {}), }, @@ -509,7 +528,7 @@ function buildOnyxDataForMoneyRequest( iouReportID: chatReport.iouReportID, lastReadTime: chatReport.lastReadTime, pendingFields: null, - ...(isPolicyAdmin ? {hasOutstandingChildRequest: chatReport.hasOutstandingChildRequest} : {}), + hasOutstandingChildRequest: chatReport.hasOutstandingChildRequest, ...(isNewChatReport ? { errorFields: { @@ -691,7 +710,7 @@ function getMoneyRequestInformation( let iouReport = isNewIOUReport ? null : allReports[`${ONYXKEYS.COLLECTION.REPORT}${chatReport.iouReportID}`]; // Check if the Scheduled Submit is enabled in case of expense report - let needsToBeManuallySubmitted = false; + let needsToBeManuallySubmitted = true; let isFromPaidPolicy = false; if (isPolicyExpenseChat) { isFromPaidPolicy = PolicyUtils.isPaidGroupPolicy(policy); @@ -810,10 +829,6 @@ function getMoneyRequestInformation( } : undefined; - // The policy expense chat should have the GBR only when its a paid policy and the scheduled submit is turned off - // so the employee has to submit to their manager manually. - const hasOutstandingChildRequest = isPolicyExpenseChat && needsToBeManuallySubmitted; - // STEP 5: Build Onyx Data const [optimisticData, successData, failureData] = buildOnyxDataForMoneyRequest( chatReport, @@ -831,7 +846,7 @@ function getMoneyRequestInformation( policy, policyTags, policyCategories, - hasOutstandingChildRequest, + needsToBeManuallySubmitted, ); return { From faca563b1f90300f0186bfad56ca5cebd8b57b5f Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Wed, 17 Jan 2024 14:17:17 +0700 Subject: [PATCH 261/580] disable reply in thread for report preview when delete IOU report in offline --- 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 430d88b98569..82be73ce667b 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -2568,7 +2568,7 @@ function deleteMoneyRequest(transactionID, reportAction, isSingleTransactionView amount: CurrencyUtils.convertToDisplayString(updatedIOUReport.total, updatedIOUReport.currency), }); updatedReportPreviewAction.message[0].text = messageText; - updatedReportPreviewAction.message[0].html = messageText; + updatedReportPreviewAction.message[0].html = shouldDeleteIOUReport ? '' : messageText; if (reportPreviewAction.childMoneyRequestCount > 0) { updatedReportPreviewAction.childMoneyRequestCount = reportPreviewAction.childMoneyRequestCount - 1; From 23c94b6cd3becc7177c112f4eed879b9b52d1ded Mon Sep 17 00:00:00 2001 From: VickyStash Date: Wed, 17 Jan 2024 08:55:47 +0100 Subject: [PATCH 262/580] Use ContextMenuAnchor type for anchor typing --- src/components/ReportActionItem/ReportPreview.tsx | 1 - src/components/ShowContextMenuContext.ts | 5 +++-- .../report/ContextMenu/PopoverReportActionContextMenu.tsx | 6 +----- .../home/report/ContextMenu/ReportActionContextMenu.ts | 2 +- 4 files changed, 5 insertions(+), 9 deletions(-) diff --git a/src/components/ReportActionItem/ReportPreview.tsx b/src/components/ReportActionItem/ReportPreview.tsx index 1378e3b169f0..57e93bc3c172 100644 --- a/src/components/ReportActionItem/ReportPreview.tsx +++ b/src/components/ReportActionItem/ReportPreview.tsx @@ -228,7 +228,6 @@ function ReportPreview({ }} onPressIn={() => DeviceCapabilities.canUseTouchScreen() && ControlSelection.block()} onPressOut={() => ControlSelection.unblock()} - // @ts-expect-error TODO: Remove this once ShowContextMenuContext (https://github.com/Expensify/App/issues/24980) is migrated to TypeScript. onLongPress={(event) => showContextMenuForReport(event, contextMenuAnchor, chatReportID, action, checkIfContextMenuActive)} style={[styles.flexRow, styles.justifyContentBetween, styles.reportPreviewBox]} role="button" diff --git a/src/components/ShowContextMenuContext.ts b/src/components/ShowContextMenuContext.ts index 17557051bef9..41636aadfd91 100644 --- a/src/components/ShowContextMenuContext.ts +++ b/src/components/ShowContextMenuContext.ts @@ -5,11 +5,12 @@ import type {OnyxEntry} from 'react-native-onyx'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import * as ReportUtils from '@libs/ReportUtils'; import * as ReportActionContextMenu from '@pages/home/report/ContextMenu/ReportActionContextMenu'; +import type {ContextMenuAnchor} from '@pages/home/report/ContextMenu/ReportActionContextMenu'; import CONST from '@src/CONST'; import type {Report, ReportAction} from '@src/types/onyx'; type ShowContextMenuContextProps = { - anchor: RNText | null; + anchor: ContextMenuAnchor; report: OnyxEntry; action: OnyxEntry; checkIfContextMenuActive: () => void; @@ -36,7 +37,7 @@ ShowContextMenuContext.displayName = 'ShowContextMenuContext'; */ function showContextMenuForReport( event: GestureResponderEvent | MouseEvent, - anchor: RNText | null, + anchor: ContextMenuAnchor, reportID: string, action: OnyxEntry, checkIfContextMenuActive: () => void, diff --git a/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx b/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx index 46b783bca3f9..bbd048b0d82c 100644 --- a/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx +++ b/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx @@ -12,11 +12,7 @@ import * as Report from '@userActions/Report'; import CONST from '@src/CONST'; import type {ReportAction} from '@src/types/onyx'; import BaseReportActionContextMenu from './BaseReportActionContextMenu'; -import type {ContextMenuType, ReportActionContextMenu} from './ReportActionContextMenu'; - -type ContextMenuAnchorCallback = (x: number, y: number) => void; - -type ContextMenuAnchor = {measureInWindow: (callback: ContextMenuAnchorCallback) => void}; +import type {ContextMenuAnchor, ContextMenuType, ReportActionContextMenu} from './ReportActionContextMenu'; type Location = { x: number; diff --git a/src/pages/home/report/ContextMenu/ReportActionContextMenu.ts b/src/pages/home/report/ContextMenu/ReportActionContextMenu.ts index 19d46c1fdc4a..7080c02775ce 100644 --- a/src/pages/home/report/ContextMenu/ReportActionContextMenu.ts +++ b/src/pages/home/report/ContextMenu/ReportActionContextMenu.ts @@ -15,7 +15,7 @@ type OnCancel = () => void; type ContextMenuType = ValueOf; -type ContextMenuAnchor = View | RNText | null; +type ContextMenuAnchor = View | RNText | null | undefined; type ShowContextMenu = ( type: ContextMenuType, From b2ca9a6fd7970bde6b938b196b9f35b176d53ac2 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Wed, 17 Jan 2024 09:12:49 +0100 Subject: [PATCH 263/580] Fix lint error --- src/components/ShowContextMenuContext.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ShowContextMenuContext.ts b/src/components/ShowContextMenuContext.ts index 41636aadfd91..374ca8a2f1e5 100644 --- a/src/components/ShowContextMenuContext.ts +++ b/src/components/ShowContextMenuContext.ts @@ -1,6 +1,6 @@ import {createContext} from 'react'; // eslint-disable-next-line no-restricted-imports -import type {GestureResponderEvent, Text as RNText} from 'react-native'; +import type {GestureResponderEvent} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import * as ReportUtils from '@libs/ReportUtils'; From a53da9d6b5071c0a52d50569c368801d3d377c71 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Wed, 17 Jan 2024 09:22:18 +0100 Subject: [PATCH 264/580] Fix lint error in import --- src/libs/ReportUtils.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 71f1ba3927c1..e7750dff6368 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -16,7 +16,14 @@ import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type {Beta, Login, PersonalDetails, PersonalDetailsList, Policy, PolicyReportField, Report, ReportAction, ReportMetadata, Session, Transaction} from '@src/types/onyx'; import type {Errors, Icon, PendingAction} from '@src/types/onyx/OnyxCommon'; -import type {IOUMessage, OriginalMessageActionName, OriginalMessageCreated, OriginalMessageReimbursementDequeued, PaymentMethodType, ReimbursementDeQueuedMessage} from '@src/types/onyx/OriginalMessage'; +import type { + IOUMessage, + OriginalMessageActionName, + OriginalMessageCreated, + OriginalMessageReimbursementDequeued, + PaymentMethodType, + ReimbursementDeQueuedMessage, +} from '@src/types/onyx/OriginalMessage'; import type {Status} from '@src/types/onyx/PersonalDetails'; import type {NotificationPreference} from '@src/types/onyx/Report'; import type {Message, ReportActionBase, ReportActions} from '@src/types/onyx/ReportAction'; From 2d65ac513755b23727e8c1f88c8e77505c4992ef Mon Sep 17 00:00:00 2001 From: Pedro Guerreiro Date: Wed, 17 Jan 2024 10:53:42 +0000 Subject: [PATCH 265/580] refactor(typescript): apply pull request feedback --- src/pages/ValidateLoginPage/index.tsx | 2 +- src/pages/ValidateLoginPage/index.website.tsx | 15 ++++++++++----- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/pages/ValidateLoginPage/index.tsx b/src/pages/ValidateLoginPage/index.tsx index 1af5000ed801..7e79216a129d 100644 --- a/src/pages/ValidateLoginPage/index.tsx +++ b/src/pages/ValidateLoginPage/index.tsx @@ -8,7 +8,7 @@ import type {ValidateLoginPageOnyxNativeProps, ValidateLoginPageProps} from './t function ValidateLoginPage({ route: { - params: {accountID = '', validateCode = ''}, + params: {accountID, validateCode}, }, session, }: ValidateLoginPageProps) { diff --git a/src/pages/ValidateLoginPage/index.website.tsx b/src/pages/ValidateLoginPage/index.website.tsx index 3fe994e20644..08f0a64d1a0d 100644 --- a/src/pages/ValidateLoginPage/index.website.tsx +++ b/src/pages/ValidateLoginPage/index.website.tsx @@ -10,11 +10,16 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {ValidateLoginPageOnyxProps, ValidateLoginPageProps} from './types'; -function ValidateLoginPage({account, credentials, route, session}: ValidateLoginPageProps) { +function ValidateLoginPage({ + account, + credentials, + route: { + params: {accountID, validateCode}, + }, + session, +}: ValidateLoginPageProps) { const login = credentials?.login; const autoAuthState = session?.autoAuthState ?? CONST.AUTO_AUTH_STATE.NOT_STARTED; - const accountID = Number(route?.params.accountID) ?? -1; - const validateCode = route.params.validateCode ?? ''; const isSignedIn = !!session?.authToken; const is2FARequired = !!account?.requiresTwoFactorAuth; const cachedAccountID = credentials?.accountID; @@ -32,7 +37,7 @@ function ValidateLoginPage({account, credentials, route, session}: ValidateLogin } // The user has initiated the sign in process on the same browser, in another tab. - Session.signInWithValidateCode(accountID, validateCode); + Session.signInWithValidateCode(Number(accountID), validateCode); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); @@ -52,7 +57,7 @@ function ValidateLoginPage({account, credentials, route, session}: ValidateLogin {autoAuthState === CONST.AUTO_AUTH_STATE.JUST_SIGNED_IN && isSignedIn && } {autoAuthState === CONST.AUTO_AUTH_STATE.NOT_STARTED && ( )} From fb68ea99b2847393a4e3b11553b5e17b96f0920c Mon Sep 17 00:00:00 2001 From: Pedro Guerreiro Date: Wed, 17 Jan 2024 11:24:32 +0000 Subject: [PATCH 266/580] refactor(typescript): apply pull request feedback --- src/pages/settings/AboutPage/AboutPage.tsx | 23 +++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/src/pages/settings/AboutPage/AboutPage.tsx b/src/pages/settings/AboutPage/AboutPage.tsx index 454d3e06e82e..e6211e78ade1 100644 --- a/src/pages/settings/AboutPage/AboutPage.tsx +++ b/src/pages/settings/AboutPage/AboutPage.tsx @@ -9,7 +9,6 @@ import LottieAnimations from '@components/LottieAnimations'; import MenuItemList from '@components/MenuItemList'; import Text from '@components/Text'; import TextLink from '@components/TextLink'; -import withWindowDimensions, {windowDimensionsPropTypes} from '@components/withWindowDimensions'; import useLocalize from '@hooks/useLocalize'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -26,11 +25,7 @@ import SCREENS from '@src/SCREENS'; import type IconAsset from '@src/types/utils/IconAsset'; import pkg from '../../../../package.json'; -const propTypes = { - ...windowDimensionsPropTypes, -}; - -function getFlavor() { +function getFlavor(): string { const bundleId = DeviceInfo.getBundleId(); if (bundleId.includes('dev')) { return ' Develop'; @@ -41,7 +36,13 @@ function getFlavor() { return ''; } -type MenuItem = {translationKey: TranslationPaths; icon: IconAsset; iconRight?: IconAsset; action: () => Promise; link?: string}; +type MenuItem = { + translationKey: TranslationPaths; + icon: IconAsset; + iconRight?: IconAsset; + action: () => Promise; + link?: string; +}; function AboutPage() { const {translate} = useLocalize(); @@ -88,6 +89,7 @@ function AboutPage() { action: waitForNavigate(Report.navigateToConciergeChat), }, ]; + return baseMenuItems.map(({translationKey, icon, iconRight, action, link}: MenuItem) => ({ key: translationKey, title: translate(translationKey), @@ -96,7 +98,7 @@ function AboutPage() { onPress: action, shouldShowRightIcon: true, onSecondaryInteraction: link - ? (e: GestureResponderEvent | MouseEvent) => ReportActionContextMenu.showContextMenu(CONST.CONTEXT_MENU_TYPES.LINK, e, link, popoverAnchor.current) + ? (event: GestureResponderEvent | MouseEvent) => ReportActionContextMenu.showContextMenu(CONST.CONTEXT_MENU_TYPES.LINK, event, link, popoverAnchor.current) : undefined, ref: popoverAnchor, shouldBlockSelection: !!link, @@ -129,7 +131,7 @@ function AboutPage() { > {translate('footer.aboutExpensify')} - {translate('initialSettingsPage.aboutPage.description')} + {translate('initialSettingsPage.aboutPage.description')} Date: Wed, 17 Jan 2024 12:28:23 +0100 Subject: [PATCH 267/580] fix: problems --- .../Pager/AttachmentCarouselPagerContext.ts | 5 ++-- .../AttachmentCarousel/Pager/index.tsx | 6 +++-- .../BaseAttachmentViewPdf.js | 6 ++--- .../AttachmentViewPdf/index.android.js | 8 +++---- src/components/Lightbox.tsx | 24 ++++++++++++------- src/components/MultiGestureCanvas/types.ts | 2 +- 6 files changed, 30 insertions(+), 21 deletions(-) diff --git a/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts b/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts index f23153f5e2e2..481c11ee0397 100644 --- a/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts +++ b/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts @@ -4,10 +4,11 @@ import type PagerView from 'react-native-pager-view'; import type {SharedValue} from 'react-native-reanimated'; type AttachmentCarouselPagerContextValue = { + pagerRef: ForwardedRef; + isPagerSwiping: SharedValue; + isPdfZooming: SharedValue; onTap: () => void; onScaleChanged: (scale: number) => void; - pagerRef: ForwardedRef; // - isPagerSwiping: SharedValue; }; const AttachmentCarouselPagerContext = createContext(null); diff --git a/src/components/Attachments/AttachmentCarousel/Pager/index.tsx b/src/components/Attachments/AttachmentCarousel/Pager/index.tsx index 0331fc4044ce..cf95382ccc30 100644 --- a/src/components/Attachments/AttachmentCarousel/Pager/index.tsx +++ b/src/components/Attachments/AttachmentCarousel/Pager/index.tsx @@ -42,6 +42,7 @@ function AttachmentCarouselPager( const styles = useThemeStyles(); const pagerRef = useRef(null); + const isPdfZooming = useSharedValue(false); const isPagerSwiping = useSharedValue(false); const activePage = useSharedValue(initialIndex); const [activePageState, setActivePageState] = useState(initialIndex); @@ -67,10 +68,11 @@ function AttachmentCarouselPager( () => ({ pagerRef, isPagerSwiping, + isPdfZooming, onTap, onScaleChanged, }), - [isPagerSwiping, onTap, onScaleChanged], + [isPagerSwiping, isPdfZooming, onTap, onScaleChanged], ); useImperativeHandle( @@ -88,7 +90,7 @@ function AttachmentCarouselPager( { - if (offsetX.value !== 0 && offsetY.value !== 0 && shouldPagerScroll) { + if (offsetX.value !== 0 && offsetY.value !== 0 && isPdfZooming) { // if the value of X is greater than Y and the pdf is not zoomed in, // enable the pager scroll so that the user // can swipe to the next attachment otherwise disable it. if (Math.abs(evt.allTouches[0].absoluteX - offsetX.value) > Math.abs(evt.allTouches[0].absoluteY - offsetY.value) && scaleRef.value === 1) { - shouldPagerScroll.value = true; + isPdfZooming.value = true; } else { - shouldPagerScroll.value = false; + isPdfZooming.value = false; } } offsetX.value = evt.allTouches[0].absoluteX; diff --git a/src/components/Lightbox.tsx b/src/components/Lightbox.tsx index 3c092b5b4840..d814e34933c0 100644 --- a/src/components/Lightbox.tsx +++ b/src/components/Lightbox.tsx @@ -4,7 +4,7 @@ import {ActivityIndicator, PixelRatio, StyleSheet, View} from 'react-native'; import useStyleUtils from '@hooks/useStyleUtils'; import Image from './Image'; import MultiGestureCanvas, {DEFAULT_ZOOM_RANGE} from './MultiGestureCanvas'; -import type {ContentSize, OnScaleChangedCallback, ZoomRange} from './MultiGestureCanvas/types'; +import type {CanvasSize, ContentSize, OnScaleChangedCallback, ZoomRange} from './MultiGestureCanvas/types'; import {getCanvasFitScale} from './MultiGestureCanvas/utils'; // Increase/decrease this number to change the number of concurrent lightboxes @@ -63,8 +63,8 @@ function Lightbox({ }: LightboxProps) { const StyleUtils = useStyleUtils(); - const [canvasSize, setCanvasSize] = useState({width: 0, height: 0}); - const isCanvasLoaded = canvasSize.width !== 0 && canvasSize.height !== 0; + const [canvasSize, setCanvasSize] = useState(); + const isCanvasLoaded = canvasSize !== undefined; const updateCanvasSize = useCallback( ({ nativeEvent: { @@ -75,6 +75,7 @@ function Lightbox({ ); const [contentSize, setInternalContentSize] = useState(() => cachedImageDimensions.get(uri)); + const isContentLoaded = contentSize !== undefined; const setContentSize = useCallback( (newDimensions: ContentSize | undefined) => { setInternalContentSize(newDimensions); @@ -83,10 +84,15 @@ function Lightbox({ [uri], ); const updateContentSize = useCallback( - ({nativeEvent: {width, height}}: ImageOnLoadEvent) => setContentSize({width: width * PixelRatio.get(), height: height * PixelRatio.get()}), - [setContentSize], + ({nativeEvent: {width, height}}: ImageOnLoadEvent) => { + if (contentSize !== undefined) { + return; + } + + setContentSize({width: width * PixelRatio.get(), height: height * PixelRatio.get()}); + }, + [contentSize, setContentSize], ); - const contentLoaded = contentSize != null; const isItemActive = index === activeIndex; const [isActive, setActive] = useState(isItemActive); @@ -125,7 +131,7 @@ function Lightbox({ // because it's only going to be rendered after the fallback image is hidden. const shouldShowLightbox = !hasSiblingCarouselItems || !isFallbackVisible; - const isLoading = isActive && (!isCanvasLoaded || !contentLoaded); + const isLoading = isActive && (!isCanvasLoaded || !isContentLoaded); // We delay setting a page to active state by a (few) millisecond(s), // to prevent the image transformer from flashing while still rendering @@ -151,7 +157,7 @@ function Lightbox({ } if (isActive) { - if (contentLoaded && isFallbackVisible) { + if (isContentLoaded && isFallbackVisible) { // We delay hiding the fallback image while image transformer is still rendering setTimeout(() => { setFallbackVisible(false); @@ -166,7 +172,7 @@ function Lightbox({ // Show fallback when the image goes out of focus or when the image is loading setFallbackVisible(true); } - }, [contentLoaded, hasSiblingCarouselItems, isActive, isFallbackVisible, isLightboxImageLoaded, isLightboxVisible]); + }, [isContentLoaded, hasSiblingCarouselItems, isActive, isFallbackVisible, isLightboxImageLoaded, isLightboxVisible]); return ( void; reset: (animated: boolean, callbackProp: () => void) => void; onTap: OnTapCallback | undefined; - onScaleChanged: OnScaleChangedCallback | undefined; + onScaleChanged?: OnScaleChangedCallback; }; export type {CanvasSize, ContentSize, ZoomRange, OnScaleChangedCallback, MultiGestureCanvasVariables}; From c06219a96e83ec87adcfdbca0eec1fdb28eaaf94 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Wed, 17 Jan 2024 12:40:12 +0100 Subject: [PATCH 268/580] fix: arrows not showable --- .../AttachmentViewPdf/BaseAttachmentViewPdf.js | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.js b/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.js index 177927edaeac..6d2e8ec6242a 100644 --- a/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.js +++ b/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.js @@ -8,7 +8,7 @@ function BaseAttachmentViewPdf({ encryptedSourceUrl, isFocused, isUsedInCarousel, - onPress, + onPress: onPressProp, onScaleChanged: onScaleChangedProp, onToggleKeyboard, onLoadComplete, @@ -45,6 +45,18 @@ function BaseAttachmentViewPdf({ [attachmentCarouselPagerContext, isUsedInCarousel, onScaleChangedProp], ); + const onPress = useCallback( + (e) => { + if (onPressProp !== undefined) { + onPressProp(e); + } + if (attachmentCarouselPagerContext !== null && attachmentCarouselPagerContext.onTap !== null) { + attachmentCarouselPagerContext.onTap(e); + } + }, + [attachmentCarouselPagerContext], + ); + return ( Date: Wed, 17 Jan 2024 10:10:18 -0300 Subject: [PATCH 269/580] 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 9298809b24b2d5a0675b0ba08cafd74e089bcc8e Mon Sep 17 00:00:00 2001 From: Kamil Owczarz Date: Wed, 17 Jan 2024 17:00:43 +0100 Subject: [PATCH 270/580] 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 64ead2bb48c1df23e1f972c834262980fbf21c37 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Wed, 17 Jan 2024 18:42:47 +0100 Subject: [PATCH 271/580] 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 272/580] 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 b996ddaf272d07c9638ddd3dec630c36504cd410 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Wed, 17 Jan 2024 20:34:41 +0100 Subject: [PATCH 273/580] Correct some of the files manually --- .../RequestAccountValidationLinkParams.ts | 5 + .../RequestReplacementExpensifyCardParams.ts | 3 +- .../parameters/ResendValidationLinkParams.ts | 5 - src/libs/API/parameters/index.ts | 2 +- src/libs/API/types.ts | 14 +- src/libs/actions/PaymentMethods.ts | 1 + src/libs/actions/PersonalDetails.ts | 2 +- src/libs/actions/Report.ts | 152 +++--------------- src/libs/actions/Session/index.ts | 21 ++- src/libs/actions/TeachersUnite.ts | 1 + src/libs/actions/User.ts | 19 ++- src/libs/actions/Wallet.ts | 8 +- 12 files changed, 75 insertions(+), 158 deletions(-) create mode 100644 src/libs/API/parameters/RequestAccountValidationLinkParams.ts delete mode 100644 src/libs/API/parameters/ResendValidationLinkParams.ts diff --git a/src/libs/API/parameters/RequestAccountValidationLinkParams.ts b/src/libs/API/parameters/RequestAccountValidationLinkParams.ts new file mode 100644 index 000000000000..be33c5648685 --- /dev/null +++ b/src/libs/API/parameters/RequestAccountValidationLinkParams.ts @@ -0,0 +1,5 @@ +type RequestAccountValidationLinkParams = { + email?: string; +}; + +export default RequestAccountValidationLinkParams; diff --git a/src/libs/API/parameters/RequestReplacementExpensifyCardParams.ts b/src/libs/API/parameters/RequestReplacementExpensifyCardParams.ts index f136086338f4..bc86923a83a4 100644 --- a/src/libs/API/parameters/RequestReplacementExpensifyCardParams.ts +++ b/src/libs/API/parameters/RequestReplacementExpensifyCardParams.ts @@ -1,5 +1,6 @@ type RequestReplacementExpensifyCardParams = { - cardId: number; + cardID: number; reason: string; }; + export default RequestReplacementExpensifyCardParams; diff --git a/src/libs/API/parameters/ResendValidationLinkParams.ts b/src/libs/API/parameters/ResendValidationLinkParams.ts deleted file mode 100644 index 663ccc33f1e5..000000000000 --- a/src/libs/API/parameters/ResendValidationLinkParams.ts +++ /dev/null @@ -1,5 +0,0 @@ -type ResendValidationLinkParams = { - email?: string; -}; - -export default ResendValidationLinkParams; diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index e503ba29c5bb..598b29bab53f 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -43,7 +43,7 @@ export type {default as RequestNewValidateCodeParams} from './RequestNewValidate export type {default as RequestPhysicalExpensifyCardParams} from './RequestPhysicalExpensifyCardParams'; export type {default as RequestReplacementExpensifyCardParams} from './RequestReplacementExpensifyCardParams'; export type {default as RequestUnlinkValidationLinkParams} from './RequestUnlinkValidationLinkParams'; -export type {default as ResendValidationLinkParams} from './ResendValidationLinkParams'; +export type {default as RequestAccountValidationLinkParams} from './RequestAccountValidationLinkParams'; export type {default as ResolveActionableMentionWhisperParams} from './ResolveActionableMentionWhisperParams'; export type {default as RevealExpensifyCardDetailsParams} from './RevealExpensifyCardDetailsParams'; export type {default as SearchForReportsParams} from './SearchForReportsParams'; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index 80655c6f606e..8dce41a41fdf 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -39,6 +39,7 @@ import type { ReconnectAppParams, ReferTeachersUniteVolunteerParams, ReportVirtualExpensifyCardFraudParams, + RequestAccountValidationLinkParams, RequestContactMethodValidateCodeParams, RequestNewValidateCodeParams, RequestPhysicalExpensifyCardParams, @@ -71,6 +72,7 @@ import type { ValidateBankAccountWithTransactionsParams, ValidateLoginParams, ValidateSecondaryLoginParams, + ValidateTwoFactorAuthParams, VerifyIdentityForBankAccountParams, } from './parameters'; import type SignInUserParams from './parameters/SignInUserParams'; @@ -214,7 +216,7 @@ type WriteCommandParameters = { [WRITE_COMMANDS.SET_CONTACT_METHOD_AS_DEFAULT]: SetContactMethodAsDefaultParams; [WRITE_COMMANDS.UPDATE_THEME]: UpdateThemeParams; [WRITE_COMMANDS.UPDATE_STATUS]: UpdateStatusParams; - [WRITE_COMMANDS.CLEAR_STATUS]: ClearStatusParams; + [WRITE_COMMANDS.CLEAR_STATUS]: EmptyObject; [WRITE_COMMANDS.UPDATE_PERSONAL_DETAILS_FOR_WALLET]: UpdatePersonalDetailsForWalletParams; [WRITE_COMMANDS.VERIFY_IDENTITY]: VerifyIdentityParams; [WRITE_COMMANDS.ACCEPT_WALLET_TERMS]: AcceptWalletTermsParams; @@ -229,11 +231,11 @@ type WriteCommandParameters = { [WRITE_COMMANDS.SIGN_IN_USER_WITH_LINK]: SignInUserWithLinkParams; [WRITE_COMMANDS.REQUEST_UNLINK_VALIDATION_LINK]: RequestUnlinkValidationLinkParams; [WRITE_COMMANDS.UNLINK_LOGIN]: UnlinkLoginParams; - [WRITE_COMMANDS.ENABLE_TWO_FACTOR_AUTH]: EnableTwoFactorAuthParams; - [WRITE_COMMANDS.DISABLE_TWO_FACTOR_AUTH]: DisableTwoFactorAuthParams; - [WRITE_COMMANDS.TWO_FACTOR_AUTH_VALIDATE]: TwoFactorAuthValidateParams; - [WRITE_COMMANDS.ADD_COMMENT]: AddCommentParams; - [WRITE_COMMANDS.ADD_ATTACHMENT]: AddAttachmentParams; + [WRITE_COMMANDS.ENABLE_TWO_FACTOR_AUTH]: EmptyObject; + [WRITE_COMMANDS.DISABLE_TWO_FACTOR_AUTH]: EmptyObject; + [WRITE_COMMANDS.TWO_FACTOR_AUTH_VALIDATE]: ValidateTwoFactorAuthParams; + [WRITE_COMMANDS.ADD_COMMENT]: AddCommentOrAttachementParams; + [WRITE_COMMANDS.ADD_ATTACHMENT]: AddCommentOrAttachementParams; [WRITE_COMMANDS.CONNECT_BANK_ACCOUNT_WITH_PLAID]: ConnectBankAccountWithPlaidParams; [WRITE_COMMANDS.ADD_PERSONAL_BANK_ACCOUNT]: AddPersonalBankAccountParams; [WRITE_COMMANDS.OPT_IN_TO_PUSH_NOTIFICATIONS]: OptInToPushNotificationsParams; diff --git a/src/libs/actions/PaymentMethods.ts b/src/libs/actions/PaymentMethods.ts index 09ce026395c1..4e5190929647 100644 --- a/src/libs/actions/PaymentMethods.ts +++ b/src/libs/actions/PaymentMethods.ts @@ -7,6 +7,7 @@ import type {OnyxEntry} from 'react-native-onyx/lib/types'; import type {ValueOf} from 'type-fest'; import type {TransferMethod} from '@components/KYCWall/types'; import * as API from '@libs/API'; +import type {AddPaymentCardParams, DeletePaymentCardParams, MakeDefaultPaymentMethodParams, PaymentCardParams} from '@libs/API/parameters'; import {READ_COMMANDS, WRITE_COMMANDS} from '@libs/API/types'; import * as CardUtils from '@libs/CardUtils'; import Navigation from '@libs/Navigation/Navigation'; diff --git a/src/libs/actions/PersonalDetails.ts b/src/libs/actions/PersonalDetails.ts index 32a69e507fae..d831adfb5e61 100644 --- a/src/libs/actions/PersonalDetails.ts +++ b/src/libs/actions/PersonalDetails.ts @@ -2,7 +2,7 @@ import Str from 'expensify-common/lib/str'; import type {OnyxEntry, OnyxUpdate} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import * as API from '@libs/API'; -import type {OpenPublicProfilePageParams} from '@libs/API/parameters'; +import type {OpenPublicProfilePageParams, UpdateAutomaticTimezoneParams, UpdateDateOfBirthParams, UpdateDisplayNameParams, UpdateHomeAddressParams, UpdateLegalNameParams, UpdatePronounsParams, UpdateSelectedTimezoneParams, UpdateUserAvatarParams} from '@libs/API/parameters'; import {READ_COMMANDS, WRITE_COMMANDS} from '@libs/API/types'; import type {CustomRNImageManipulatorResult} from '@libs/cropOrRotateImage/types'; import DateUtils from '@libs/DateUtils'; diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 17c397db0337..79ffcd05ddf1 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -17,6 +17,7 @@ import type { GetReportPrivateNoteParams, OpenReportParams, OpenRoomMembersPageParams, + ResolveActionableMentionWhisperParams, SearchForReportsParams, } from '@libs/API/parameters'; import {READ_COMMANDS, SIDE_EFFECT_REQUEST_COMMANDS, WRITE_COMMANDS} from '@libs/API/types'; @@ -352,19 +353,7 @@ function addActions(reportID: string, text = '', file?: File) { optimisticReportActions[attachmentAction.reportActionID] = attachmentAction; } - type AddCommentOrAttachementParameters = { - reportID: string; - reportActionID?: string; - commentReportActionID?: string | null; - reportComment?: string; - file?: File; - timezone?: string; - shouldAllowActionableMentionWhispers?: boolean; - clientCreatedTime?: string; - isOldDotConciergeChat?: boolean; - }; - - const parameters: AddCommentOrAttachementParameters = { + const parameters: AddCommentOrAttachementParams = { reportID, reportActionID: file ? attachmentAction?.reportActionID : reportCommentAction?.reportActionID, commentReportActionID: file && reportCommentAction ? reportCommentAction.reportActionID : null, @@ -803,11 +792,7 @@ function reconnect(reportID: string) { }, ]; - type ReconnectToReportParameters = { - reportID: string; - }; - - const parameters: ReconnectToReportParameters = { + const parameters: ReconnectToReportParams = { reportID, }; @@ -926,12 +911,7 @@ function readNewestAction(reportID: string) { }, ]; - type ReadNewestActionParameters = { - reportID: string; - lastReadTime: string; - }; - - const parameters: ReadNewestActionParameters = { + const parameters: ReadNewestActionParams = { reportID, lastReadTime, }; @@ -972,12 +952,7 @@ function markCommentAsUnread(reportID: string, reportActionCreated: string) { }, ]; - type MarkAsUnreadParameters = { - reportID: string; - lastReadTime: string; - }; - - const parameters: MarkAsUnreadParameters = { + const parameters: MarkAsUnreadParams = { reportID, lastReadTime, }; @@ -999,12 +974,7 @@ function togglePinnedState(reportID: string, isPinnedChat: boolean) { }, ]; - type TogglePinnedChatParameters = { - reportID: string; - pinnedValue: boolean; - }; - - const parameters: TogglePinnedChatParameters = { + const parameters: TogglePinnedChatParams = { reportID, pinnedValue, }; @@ -1192,12 +1162,7 @@ function deleteReportComment(reportID: string, reportAction: ReportAction) { } } - type DeleteCommentParameters = { - reportID: string; - reportActionID: string; - }; - - const parameters: DeleteCommentParameters = { + const parameters: DeleteCommentParams = { reportID: originalReportID, reportActionID, }; @@ -1345,13 +1310,7 @@ function editReportComment(reportID: string, originalReportAction: OnyxEntry; - writeCapability?: WriteCapability; - welcomeMessage?: string; - }; - - const parameters: AddWorkspaceRoomParameters = { + const parameters: AddWorkspaceRoomParams = { policyID: policyReport.policyID, reportName: policyReport.reportName, visibility: policyReport.visibility, @@ -1749,12 +1683,7 @@ function updatePolicyRoomNameAndNavigate(policyRoomReport: Report, policyRoomNam }, ]; - type UpdatePolicyRoomNameParameters = { - reportID: string; - policyRoomName: string; - }; - - const parameters: UpdatePolicyRoomNameParameters = {reportID, policyRoomName}; + const parameters: UpdatePolicyRoomNameParams = {reportID, policyRoomName}; API.write(WRITE_COMMANDS.UPDATE_POLICY_ROOM_NAME, parameters, {optimisticData, successData, failureData}); Navigation.goBack(ROUTES.REPORT_SETTINGS.getRoute(reportID)); @@ -1916,16 +1845,7 @@ function addEmojiReaction(reportID: string, reportActionID: string, emoji: Emoji }, ]; - type AddEmojiReactionParameters = { - reportID: string; - skinTone: string | number; - emojiCode: string; - reportActionID: string; - createdAt: string; - useEmojiReactions: boolean; - }; - - const parameters: AddEmojiReactionParameters = { + const parameters: AddEmojiReactionParams = { reportID, skinTone, emojiCode: emoji.name, @@ -1957,14 +1877,7 @@ function removeEmojiReaction(reportID: string, reportActionID: string, emoji: Em }, ]; - type RemoveEmojiReactionParameters = { - reportID: string; - reportActionID: string; - emojiCode: string; - useEmojiReactions: boolean; - }; - - const parameters: RemoveEmojiReactionParameters = { + const parameters: RemoveEmojiReactionParams = { reportID, reportActionID, emojiCode: emoji.name, @@ -2136,11 +2049,7 @@ function leaveRoom(reportID: string, isWorkspaceMemberLeavingWorkspaceRoom = fal }); } - type LeaveRoomParameters = { - reportID: string; - }; - - const parameters: LeaveRoomParameters = { + const parameters: LeaveRoomParams = { reportID, }; @@ -2209,12 +2118,7 @@ function inviteToRoom(reportID: string, inviteeEmailsToAccountIDs: Record, se }, ]; - type FlagCommentParameters = { - severity: string; - reportActionID: string; - isDevRequest: boolean; - }; - - const parameters: FlagCommentParameters = { + const parameters: FlagCommentParams = { severity, reportActionID, // This check is to prevent flooding Concierge with test flags @@ -2426,12 +2319,7 @@ const updatePrivateNotes = (reportID: string, accountID: number, note: string) = }, ]; - type UpdateReportPrivateNoteParameters = { - reportID: string; - privateNotes: string; - }; - - const parameters: UpdateReportPrivateNoteParameters = {reportID, privateNotes: note}; + const parameters: UpdateReportPrivateNoteParams = {reportID, privateNotes: note}; API.write(WRITE_COMMANDS.UPDATE_REPORT_PRIVATE_NOTE, parameters, {optimisticData, successData, failureData}); }; diff --git a/src/libs/actions/Session/index.ts b/src/libs/actions/Session/index.ts index 328c6d7698bb..6a4bbffd9df2 100644 --- a/src/libs/actions/Session/index.ts +++ b/src/libs/actions/Session/index.ts @@ -7,7 +7,20 @@ import Onyx from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import * as PersistedRequests from '@libs/actions/PersistedRequests'; import * as API from '@libs/API'; -import type {AuthenticatePusherParams, BeginSignInParams, SignInWithShortLivedAuthTokenParams} from '@libs/API/parameters'; +import type { + AuthenticatePusherParams, + BeginAppleSignInParams, + BeginGoogleSignInParams, + BeginSignInParams, + LogOutParams, + RequestAccountValidationLinkParams, + RequestNewValidateCodeParams, + RequestUnlinkValidationLinkParams, + SignInUserWithLinkParams, + SignInWithShortLivedAuthTokenParams, + UnlinkLoginParams, + ValidateTwoFactorAuthParams, +} from '@libs/API/parameters'; import {READ_COMMANDS, SIDE_EFFECT_REQUEST_COMMANDS, WRITE_COMMANDS} from '@libs/API/types'; import * as Authentication from '@libs/Authentication'; import * as ErrorUtils from '@libs/ErrorUtils'; @@ -171,7 +184,7 @@ function resendValidationLink(login = credentials.login) { }, ]; - const params: ResendValidationLinkParams = {email: login}; + const params: RequestAccountValidationLinkParams = {email: login}; API.write(WRITE_COMMANDS.REQUEST_ACCOUNT_VALIDATION_LINK, params, {optimisticData, successData, failureData}); } @@ -779,7 +792,7 @@ function toggleTwoFactorAuth(enable: boolean) { }, ]; - API.write(enable ? 'EnableTwoFactorAuth' : 'DisableTwoFactorAuth', {}, {optimisticData, successData, failureData}); + API.write(enable ? WRITE_COMMANDS.ENABLE_TWO_FACTOR_AUTH : WRITE_COMMANDS.DISABLE_TWO_FACTOR_AUTH, {}, {optimisticData, successData, failureData}); } function validateTwoFactorAuth(twoFactorAuthCode: string) { @@ -815,7 +828,7 @@ function validateTwoFactorAuth(twoFactorAuthCode: string) { const params: ValidateTwoFactorAuthParams = {twoFactorAuthCode}; - API.write('TwoFactorAuth_Validate', params, {optimisticData, successData, failureData}); + API.write(WRITE_COMMANDS.TWO_FACTOR_AUTH_VALIDATE, params, {optimisticData, successData, failureData}); } /** diff --git a/src/libs/actions/TeachersUnite.ts b/src/libs/actions/TeachersUnite.ts index cc782f91f751..bea9e24a85cc 100644 --- a/src/libs/actions/TeachersUnite.ts +++ b/src/libs/actions/TeachersUnite.ts @@ -9,6 +9,7 @@ import type {OptimisticCreatedReportAction} from '@libs/ReportUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {PersonalDetailsList} from '@src/types/onyx'; +import { AddSchoolPrincipalParams, ReferTeachersUniteVolunteerParams } from '@libs/API/parameters'; type CreationData = { reportID: string; diff --git a/src/libs/actions/User.ts b/src/libs/actions/User.ts index 5d9b889acfd1..e16a9ec30857 100644 --- a/src/libs/actions/User.ts +++ b/src/libs/actions/User.ts @@ -4,7 +4,22 @@ import Onyx from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx/lib/types'; import type {ValueOf} from 'type-fest'; import * as API from '@libs/API'; -import type {GetStatementPDFParams} from '@libs/API/parameters'; +import type { + AddNewContactMethodParams, + CloseAccountParams, + DeleteContactMethodParams, + GetStatementPDFParams, + RequestContactMethodValidateCodeParams, + SetContactMethodAsDefaultParams, + UpdateChatPriorityModeParams, + UpdateFrequentlyUsedEmojisParams, + UpdateNewsletterSubscriptionParams, + UpdatePreferredEmojiSkinToneParams, + UpdateStatusParams, + UpdateThemeParams, + ValidateLoginParams, + ValidateSecondaryLoginParams, +} from '@libs/API/parameters'; import {READ_COMMANDS, WRITE_COMMANDS} from '@libs/API/types'; import * as ErrorUtils from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; @@ -806,7 +821,7 @@ function clearCustomStatus() { }, }, ]; - API.write(WRITE_COMMANDS.CLEAR_STATUS, undefined, { + API.write(WRITE_COMMANDS.CLEAR_STATUS, {}, { optimisticData, }); } diff --git a/src/libs/actions/Wallet.ts b/src/libs/actions/Wallet.ts index 32f97d9d7d30..717b6df2680d 100644 --- a/src/libs/actions/Wallet.ts +++ b/src/libs/actions/Wallet.ts @@ -2,6 +2,7 @@ import type {OnyxUpdate} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import * as API from '@libs/API'; +import type {RequestPhysicalExpensifyCardParams} from '@libs/API/parameters'; import {READ_COMMANDS, WRITE_COMMANDS} from '@libs/API/types'; import type {PrivatePersonalDetails} from '@libs/GetPhysicalCardUtils'; import type CONST from '@src/CONST'; @@ -263,12 +264,7 @@ function answerQuestionsForWallet(answers: WalletQuestionAnswer[], idNumber: str }, ]; - type AnswerQuestionsForWallet = { - idologyAnswers: string; - idNumber: string; - }; - - const requestParams: AnswerQuestionsForWallet = { + const requestParams: AnswerQuestionsForWalletParams = { idologyAnswers, idNumber, }; From 7dd5f0b1b82a7bc6995a10d9e1977c9bea861f5f Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Wed, 17 Jan 2024 20:51:43 +0100 Subject: [PATCH 274/580] Add remaining write params --- .../API/parameters/AcceptWalletTermsParams.ts | 6 + .../AddCommentOrAttachementParams.ts | 13 + .../API/parameters/AddEmojiReactionParams.ts | 10 + .../API/parameters/AddWorkspaceRoomParams.ts | 15 + .../AnswerQuestionsForWalletParams.ts | 6 + .../API/parameters/DeleteCommentParams.ts | 6 + src/libs/API/parameters/FlagCommentParams.ts | 7 + src/libs/API/parameters/InviteToRoomParams.ts | 6 + src/libs/API/parameters/LeaveRoomParams.ts | 5 + src/libs/API/parameters/MarkAsUnreadParams.ts | 6 + .../OptInOutToPushNotificationsParams.ts | 5 + .../API/parameters/ReadNewestActionParams.ts | 6 + .../API/parameters/ReconnectToReportParams.ts | 5 + .../parameters/RemoveEmojiReactionParams.ts | 8 + .../API/parameters/RemoveFromRoomParams.ts | 6 + .../API/parameters/TogglePinnedChatParams.ts | 6 + ...ateBeneficialOwnersForBankAccountParams.ts | 5 + .../API/parameters/UpdateCommentParams.ts | 7 + ...eCompanyInformationForBankAccountParams.ts | 8 + .../UpdatePersonalDetailsForWalletParams.ts | 13 + .../parameters/UpdatePolicyRoomNameParams.ts | 6 + ...pdateReportNotificationPreferenceParams.ts | 8 + .../UpdateReportPrivateNoteParams.ts | 6 + .../UpdateReportWriteCapabilityParams.ts | 8 + .../parameters/UpdateWelcomeMessageParams.ts | 6 + .../API/parameters/VerifyIdentityParams.ts | 5 + src/libs/API/parameters/index.ts | 25 ++ src/libs/API/types.ts | 282 +++++++----------- src/libs/actions/BankAccounts.ts | 11 +- src/libs/actions/PersonalDetails.ts | 12 +- src/libs/actions/Report.ts | 19 ++ src/libs/actions/Session/index.ts | 1 + src/libs/actions/TeachersUnite.ts | 2 +- src/libs/actions/User.ts | 4 +- src/libs/actions/Wallet.ts | 37 +-- 35 files changed, 367 insertions(+), 214 deletions(-) create mode 100644 src/libs/API/parameters/AcceptWalletTermsParams.ts create mode 100644 src/libs/API/parameters/AddCommentOrAttachementParams.ts create mode 100644 src/libs/API/parameters/AddEmojiReactionParams.ts create mode 100644 src/libs/API/parameters/AddWorkspaceRoomParams.ts create mode 100644 src/libs/API/parameters/AnswerQuestionsForWalletParams.ts create mode 100644 src/libs/API/parameters/DeleteCommentParams.ts create mode 100644 src/libs/API/parameters/FlagCommentParams.ts create mode 100644 src/libs/API/parameters/InviteToRoomParams.ts create mode 100644 src/libs/API/parameters/LeaveRoomParams.ts create mode 100644 src/libs/API/parameters/MarkAsUnreadParams.ts create mode 100644 src/libs/API/parameters/OptInOutToPushNotificationsParams.ts create mode 100644 src/libs/API/parameters/ReadNewestActionParams.ts create mode 100644 src/libs/API/parameters/ReconnectToReportParams.ts create mode 100644 src/libs/API/parameters/RemoveEmojiReactionParams.ts create mode 100644 src/libs/API/parameters/RemoveFromRoomParams.ts create mode 100644 src/libs/API/parameters/TogglePinnedChatParams.ts create mode 100644 src/libs/API/parameters/UpdateBeneficialOwnersForBankAccountParams.ts create mode 100644 src/libs/API/parameters/UpdateCommentParams.ts create mode 100644 src/libs/API/parameters/UpdateCompanyInformationForBankAccountParams.ts create mode 100644 src/libs/API/parameters/UpdatePersonalDetailsForWalletParams.ts create mode 100644 src/libs/API/parameters/UpdatePolicyRoomNameParams.ts create mode 100644 src/libs/API/parameters/UpdateReportNotificationPreferenceParams.ts create mode 100644 src/libs/API/parameters/UpdateReportPrivateNoteParams.ts create mode 100644 src/libs/API/parameters/UpdateReportWriteCapabilityParams.ts create mode 100644 src/libs/API/parameters/UpdateWelcomeMessageParams.ts create mode 100644 src/libs/API/parameters/VerifyIdentityParams.ts diff --git a/src/libs/API/parameters/AcceptWalletTermsParams.ts b/src/libs/API/parameters/AcceptWalletTermsParams.ts new file mode 100644 index 000000000000..897f002eb77a --- /dev/null +++ b/src/libs/API/parameters/AcceptWalletTermsParams.ts @@ -0,0 +1,6 @@ +type AcceptWalletTermsParams = { + hasAcceptedTerms: boolean; + reportID: string; +}; + +export default AcceptWalletTermsParams; diff --git a/src/libs/API/parameters/AddCommentOrAttachementParams.ts b/src/libs/API/parameters/AddCommentOrAttachementParams.ts new file mode 100644 index 000000000000..58faf9fdfc9c --- /dev/null +++ b/src/libs/API/parameters/AddCommentOrAttachementParams.ts @@ -0,0 +1,13 @@ +type AddCommentOrAttachementParams = { + reportID: string; + reportActionID?: string; + commentReportActionID?: string | null; + reportComment?: string; + file?: File; + timezone?: string; + shouldAllowActionableMentionWhispers?: boolean; + clientCreatedTime?: string; + isOldDotConciergeChat?: boolean; +}; + +export default AddCommentOrAttachementParams; diff --git a/src/libs/API/parameters/AddEmojiReactionParams.ts b/src/libs/API/parameters/AddEmojiReactionParams.ts new file mode 100644 index 000000000000..fa31da9538ad --- /dev/null +++ b/src/libs/API/parameters/AddEmojiReactionParams.ts @@ -0,0 +1,10 @@ +type AddEmojiReactionParams = { + reportID: string; + skinTone: string | number; + emojiCode: string; + reportActionID: string; + createdAt: string; + useEmojiReactions: boolean; +}; + +export default AddEmojiReactionParams; diff --git a/src/libs/API/parameters/AddWorkspaceRoomParams.ts b/src/libs/API/parameters/AddWorkspaceRoomParams.ts new file mode 100644 index 000000000000..f7cbff9565ef --- /dev/null +++ b/src/libs/API/parameters/AddWorkspaceRoomParams.ts @@ -0,0 +1,15 @@ +import type {ValueOf} from 'type-fest'; +import type CONST from '@src/CONST'; +import type {WriteCapability} from '@src/types/onyx/Report'; + +type AddWorkspaceRoomParams = { + reportID: string; + createdReportActionID: string; + policyID?: string; + reportName?: string; + visibility?: ValueOf; + writeCapability?: WriteCapability; + welcomeMessage?: string; +}; + +export default AddWorkspaceRoomParams; diff --git a/src/libs/API/parameters/AnswerQuestionsForWalletParams.ts b/src/libs/API/parameters/AnswerQuestionsForWalletParams.ts new file mode 100644 index 000000000000..34a08d7c54ee --- /dev/null +++ b/src/libs/API/parameters/AnswerQuestionsForWalletParams.ts @@ -0,0 +1,6 @@ +type AnswerQuestionsForWalletParams = { + idologyAnswers: string; + idNumber: string; +}; + +export default AnswerQuestionsForWalletParams; diff --git a/src/libs/API/parameters/DeleteCommentParams.ts b/src/libs/API/parameters/DeleteCommentParams.ts new file mode 100644 index 000000000000..d51546eec86f --- /dev/null +++ b/src/libs/API/parameters/DeleteCommentParams.ts @@ -0,0 +1,6 @@ +type DeleteCommentParams = { + reportID: string; + reportActionID: string; +}; + +export default DeleteCommentParams; diff --git a/src/libs/API/parameters/FlagCommentParams.ts b/src/libs/API/parameters/FlagCommentParams.ts new file mode 100644 index 000000000000..1789ffb16c8c --- /dev/null +++ b/src/libs/API/parameters/FlagCommentParams.ts @@ -0,0 +1,7 @@ +type FlagCommentParams = { + severity: string; + reportActionID: string; + isDevRequest: boolean; +}; + +export default FlagCommentParams; diff --git a/src/libs/API/parameters/InviteToRoomParams.ts b/src/libs/API/parameters/InviteToRoomParams.ts new file mode 100644 index 000000000000..b1af3a4fc3df --- /dev/null +++ b/src/libs/API/parameters/InviteToRoomParams.ts @@ -0,0 +1,6 @@ +type InviteToRoomParams = { + reportID: string; + inviteeEmails: string[]; +}; + +export default InviteToRoomParams; diff --git a/src/libs/API/parameters/LeaveRoomParams.ts b/src/libs/API/parameters/LeaveRoomParams.ts new file mode 100644 index 000000000000..0d0483eca88a --- /dev/null +++ b/src/libs/API/parameters/LeaveRoomParams.ts @@ -0,0 +1,5 @@ +type LeaveRoomParams = { + reportID: string; +}; + +export default LeaveRoomParams; diff --git a/src/libs/API/parameters/MarkAsUnreadParams.ts b/src/libs/API/parameters/MarkAsUnreadParams.ts new file mode 100644 index 000000000000..0a4a0d98c18c --- /dev/null +++ b/src/libs/API/parameters/MarkAsUnreadParams.ts @@ -0,0 +1,6 @@ +type MarkAsUnreadParams = { + reportID: string; + lastReadTime: string; +}; + +export default MarkAsUnreadParams; diff --git a/src/libs/API/parameters/OptInOutToPushNotificationsParams.ts b/src/libs/API/parameters/OptInOutToPushNotificationsParams.ts new file mode 100644 index 000000000000..758152abc2af --- /dev/null +++ b/src/libs/API/parameters/OptInOutToPushNotificationsParams.ts @@ -0,0 +1,5 @@ +type OptInOutToPushNotificationsParams = { + deviceID: string | null; +}; + +export default OptInOutToPushNotificationsParams; diff --git a/src/libs/API/parameters/ReadNewestActionParams.ts b/src/libs/API/parameters/ReadNewestActionParams.ts new file mode 100644 index 000000000000..590dfce25a4e --- /dev/null +++ b/src/libs/API/parameters/ReadNewestActionParams.ts @@ -0,0 +1,6 @@ +type ReadNewestActionParams = { + reportID: string; + lastReadTime: string; +}; + +export default ReadNewestActionParams; diff --git a/src/libs/API/parameters/ReconnectToReportParams.ts b/src/libs/API/parameters/ReconnectToReportParams.ts new file mode 100644 index 000000000000..e7701cd36ca9 --- /dev/null +++ b/src/libs/API/parameters/ReconnectToReportParams.ts @@ -0,0 +1,5 @@ +type ReconnectToReportParams = { + reportID: string; +}; + +export default ReconnectToReportParams; diff --git a/src/libs/API/parameters/RemoveEmojiReactionParams.ts b/src/libs/API/parameters/RemoveEmojiReactionParams.ts new file mode 100644 index 000000000000..5d474dff713b --- /dev/null +++ b/src/libs/API/parameters/RemoveEmojiReactionParams.ts @@ -0,0 +1,8 @@ +type RemoveEmojiReactionParams = { + reportID: string; + reportActionID: string; + emojiCode: string; + useEmojiReactions: boolean; +}; + +export default RemoveEmojiReactionParams; diff --git a/src/libs/API/parameters/RemoveFromRoomParams.ts b/src/libs/API/parameters/RemoveFromRoomParams.ts new file mode 100644 index 000000000000..6bf94a534dbd --- /dev/null +++ b/src/libs/API/parameters/RemoveFromRoomParams.ts @@ -0,0 +1,6 @@ +type RemoveFromRoomParams = { + reportID: string; + targetAccountIDs: number[]; +}; + +export default RemoveFromRoomParams; diff --git a/src/libs/API/parameters/TogglePinnedChatParams.ts b/src/libs/API/parameters/TogglePinnedChatParams.ts new file mode 100644 index 000000000000..338a77172dd6 --- /dev/null +++ b/src/libs/API/parameters/TogglePinnedChatParams.ts @@ -0,0 +1,6 @@ +type TogglePinnedChatParams = { + reportID: string; + pinnedValue: boolean; +}; + +export default TogglePinnedChatParams; diff --git a/src/libs/API/parameters/UpdateBeneficialOwnersForBankAccountParams.ts b/src/libs/API/parameters/UpdateBeneficialOwnersForBankAccountParams.ts new file mode 100644 index 000000000000..414c87ee8989 --- /dev/null +++ b/src/libs/API/parameters/UpdateBeneficialOwnersForBankAccountParams.ts @@ -0,0 +1,5 @@ +import type {ACHContractStepProps} from '@src/types/onyx/ReimbursementAccountDraft'; + +type UpdateBeneficialOwnersForBankAccountParams = ACHContractStepProps; + +export default UpdateBeneficialOwnersForBankAccountParams; diff --git a/src/libs/API/parameters/UpdateCommentParams.ts b/src/libs/API/parameters/UpdateCommentParams.ts new file mode 100644 index 000000000000..e4ba9391ccd4 --- /dev/null +++ b/src/libs/API/parameters/UpdateCommentParams.ts @@ -0,0 +1,7 @@ +type UpdateCommentParams = { + reportID: string; + reportComment: string; + reportActionID: string; +}; + +export default UpdateCommentParams; diff --git a/src/libs/API/parameters/UpdateCompanyInformationForBankAccountParams.ts b/src/libs/API/parameters/UpdateCompanyInformationForBankAccountParams.ts new file mode 100644 index 000000000000..7588039a9abf --- /dev/null +++ b/src/libs/API/parameters/UpdateCompanyInformationForBankAccountParams.ts @@ -0,0 +1,8 @@ +import type {BankAccountStepProps, CompanyStepProps, ReimbursementAccountProps} from '@src/types/onyx/ReimbursementAccountDraft'; + +type BankAccountCompanyInformation = BankAccountStepProps & CompanyStepProps & ReimbursementAccountProps; + +type UpdateCompanyInformationForBankAccountParams = BankAccountCompanyInformation & {policyID: string}; + +export default UpdateCompanyInformationForBankAccountParams; +export type {BankAccountCompanyInformation}; diff --git a/src/libs/API/parameters/UpdatePersonalDetailsForWalletParams.ts b/src/libs/API/parameters/UpdatePersonalDetailsForWalletParams.ts new file mode 100644 index 000000000000..d874dced4a92 --- /dev/null +++ b/src/libs/API/parameters/UpdatePersonalDetailsForWalletParams.ts @@ -0,0 +1,13 @@ +type UpdatePersonalDetailsForWalletParams = { + phoneNumber: string; + legalFirstName: string; + legalLastName: string; + addressStreet: string; + addressCity: string; + addressState: string; + addressZip: string; + dob: string; + ssn: string; +}; + +export default UpdatePersonalDetailsForWalletParams; diff --git a/src/libs/API/parameters/UpdatePolicyRoomNameParams.ts b/src/libs/API/parameters/UpdatePolicyRoomNameParams.ts new file mode 100644 index 000000000000..65b858b7c20f --- /dev/null +++ b/src/libs/API/parameters/UpdatePolicyRoomNameParams.ts @@ -0,0 +1,6 @@ +type UpdatePolicyRoomNameParams = { + reportID: string; + policyRoomName: string; +}; + +export default UpdatePolicyRoomNameParams; diff --git a/src/libs/API/parameters/UpdateReportNotificationPreferenceParams.ts b/src/libs/API/parameters/UpdateReportNotificationPreferenceParams.ts new file mode 100644 index 000000000000..c58746b316fe --- /dev/null +++ b/src/libs/API/parameters/UpdateReportNotificationPreferenceParams.ts @@ -0,0 +1,8 @@ +import type {NotificationPreference} from '@src/types/onyx/Report'; + +type UpdateReportNotificationPreferenceParams = { + reportID: string; + notificationPreference: NotificationPreference; +}; + +export default UpdateReportNotificationPreferenceParams; diff --git a/src/libs/API/parameters/UpdateReportPrivateNoteParams.ts b/src/libs/API/parameters/UpdateReportPrivateNoteParams.ts new file mode 100644 index 000000000000..30fad3bec3ab --- /dev/null +++ b/src/libs/API/parameters/UpdateReportPrivateNoteParams.ts @@ -0,0 +1,6 @@ +type UpdateReportPrivateNoteParams = { + reportID: string; + privateNotes: string; +}; + +export default UpdateReportPrivateNoteParams; diff --git a/src/libs/API/parameters/UpdateReportWriteCapabilityParams.ts b/src/libs/API/parameters/UpdateReportWriteCapabilityParams.ts new file mode 100644 index 000000000000..30b85b15aa3b --- /dev/null +++ b/src/libs/API/parameters/UpdateReportWriteCapabilityParams.ts @@ -0,0 +1,8 @@ +import type {WriteCapability} from '@src/types/onyx/Report'; + +type UpdateReportWriteCapabilityParams = { + reportID: string; + writeCapability: WriteCapability; +}; + +export default UpdateReportWriteCapabilityParams; diff --git a/src/libs/API/parameters/UpdateWelcomeMessageParams.ts b/src/libs/API/parameters/UpdateWelcomeMessageParams.ts new file mode 100644 index 000000000000..a2d3b59fe3fa --- /dev/null +++ b/src/libs/API/parameters/UpdateWelcomeMessageParams.ts @@ -0,0 +1,6 @@ +type UpdateWelcomeMessageParams = { + reportID: string; + welcomeMessage: string; +}; + +export default UpdateWelcomeMessageParams; diff --git a/src/libs/API/parameters/VerifyIdentityParams.ts b/src/libs/API/parameters/VerifyIdentityParams.ts new file mode 100644 index 000000000000..04ddf17bed87 --- /dev/null +++ b/src/libs/API/parameters/VerifyIdentityParams.ts @@ -0,0 +1,5 @@ +type VerifyIdentityParams = { + onfidoData: string; +}; + +export default VerifyIdentityParams; diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index 598b29bab53f..0e3e521d1441 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -73,3 +73,28 @@ export type {default as ValidateLoginParams} from './ValidateLoginParams'; export type {default as ValidateSecondaryLoginParams} from './ValidateSecondaryLoginParams'; export type {default as ValidateTwoFactorAuthParams} from './ValidateTwoFactorAuthParams'; export type {default as VerifyIdentityForBankAccountParams} from './VerifyIdentityForBankAccountParams'; +export type {default as AnswerQuestionsForWalletParams} from './AnswerQuestionsForWalletParams'; +export type {default as AddCommentOrAttachementParams} from './AddCommentOrAttachementParams'; +export type {default as OptInOutToPushNotificationsParams} from './OptInOutToPushNotificationsParams'; +export type {default as ReconnectToReportParams} from './ReconnectToReportParams'; +export type {default as ReadNewestActionParams} from './ReadNewestActionParams'; +export type {default as MarkAsUnreadParams} from './MarkAsUnreadParams'; +export type {default as TogglePinnedChatParams} from './TogglePinnedChatParams'; +export type {default as DeleteCommentParams} from './DeleteCommentParams'; +export type {default as UpdateCommentParams} from './UpdateCommentParams'; +export type {default as UpdateReportNotificationPreferenceParams} from './UpdateReportNotificationPreferenceParams'; +export type {default as UpdateWelcomeMessageParams} from './UpdateWelcomeMessageParams'; +export type {default as UpdateReportWriteCapabilityParams} from './UpdateReportWriteCapabilityParams'; +export type {default as AddWorkspaceRoomParams} from './AddWorkspaceRoomParams'; +export type {default as UpdatePolicyRoomNameParams} from './UpdatePolicyRoomNameParams'; +export type {default as AddEmojiReactionParams} from './AddEmojiReactionParams'; +export type {default as RemoveEmojiReactionParams} from './RemoveEmojiReactionParams'; +export type {default as LeaveRoomParams} from './LeaveRoomParams'; +export type {default as InviteToRoomParams} from './InviteToRoomParams'; +export type {default as RemoveFromRoomParams} from './RemoveFromRoomParams'; +export type {default as FlagCommentParams} from './FlagCommentParams'; +export type {default as UpdateReportPrivateNoteParams} from './UpdateReportPrivateNoteParams'; +export type {default as UpdateCompanyInformationForBankAccountParams} from './UpdateCompanyInformationForBankAccountParams'; +export type {default as UpdatePersonalDetailsForWalletParams} from './UpdatePersonalDetailsForWalletParams'; +export type {default as VerifyIdentityParams} from './VerifyIdentityParams'; +export type {default as AcceptWalletTermsParams} from './AcceptWalletTermsParams'; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index 8dce41a41fdf..593470507978 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -1,81 +1,9 @@ import type {ValueOf} from 'type-fest'; import type CONST from '@src/CONST'; import type {EmptyObject} from '@src/types/utils/EmptyObject'; -import type { - ActivatePhysicalExpensifyCardParams, - AddNewContactMethodParams, - AddPaymentCardParams, - AddPersonalBankAccountParams, - AddSchoolPrincipalParams, - AuthenticatePusherParams, - BankAccountHandlePlaidErrorParams, - BeginSignInParams, - CloseAccountParams, - ConnectBankAccountManuallyParams, - ConnectBankAccountWithPlaidParams, - DeleteContactMethodParams, - DeletePaymentBankAccountParams, - DeletePaymentCardParams, - ExpandURLPreviewParams, - GetMissingOnyxMessagesParams, - GetNewerActionsParams, - GetOlderActionsParams, - GetReportPrivateNoteParams, - GetRouteForDraftParams, - GetRouteParams, - GetStatementPDFParams, - HandleRestrictedEventParams, - LogOutParams, - MakeDefaultPaymentMethodParams, - OpenAppParams, - OpenOldDotLinkParams, - OpenPlaidBankAccountSelectorParams, - OpenPlaidBankLoginParams, - OpenProfileParams, - OpenPublicProfilePageParams, - OpenReimbursementAccountPageParams, - OpenReportParams, - OpenRoomMembersPageParams, - ReconnectAppParams, - ReferTeachersUniteVolunteerParams, - ReportVirtualExpensifyCardFraudParams, - RequestAccountValidationLinkParams, - RequestContactMethodValidateCodeParams, - RequestNewValidateCodeParams, - RequestPhysicalExpensifyCardParams, - RequestReplacementExpensifyCardParams, - RequestUnlinkValidationLinkParams, - ResolveActionableMentionWhisperParams, - RevealExpensifyCardDetailsParams, - SearchForReportsParams, - SendPerformanceTimingParams, - SetContactMethodAsDefaultParams, - SignInUserWithLinkParams, - SignInWithShortLivedAuthTokenParams, - UnlinkLoginParams, - UpdateAutomaticTimezoneParams, - UpdateChatPriorityModeParams, - UpdateDateOfBirthParams, - UpdateDisplayNameParams, - UpdateFrequentlyUsedEmojisParams, - UpdateHomeAddressParams, - UpdateLegalNameParams, - UpdateNewsletterSubscriptionParams, - UpdatePersonalInformationForBankAccountParams, - UpdatePreferredEmojiSkinToneParams, - UpdatePreferredLocaleParams, - UpdatePronounsParams, - UpdateSelectedTimezoneParams, - UpdateStatusParams, - UpdateThemeParams, - UpdateUserAvatarParams, - ValidateBankAccountWithTransactionsParams, - ValidateLoginParams, - ValidateSecondaryLoginParams, - ValidateTwoFactorAuthParams, - VerifyIdentityForBankAccountParams, -} from './parameters'; +import type * as Parameters from './parameters'; import type SignInUserParams from './parameters/SignInUserParams'; +import type UpdateBeneficialOwnersForBankAccountParams from './parameters/UpdateBeneficialOwnersForBankAccountParams'; type ApiRequestWithSideEffects = ValueOf; @@ -173,92 +101,92 @@ const WRITE_COMMANDS = { type WriteCommand = ValueOf; type WriteCommandParameters = { - [WRITE_COMMANDS.UPDATE_PREFERRED_LOCALE]: UpdatePreferredLocaleParams; - [WRITE_COMMANDS.RECONNECT_APP]: ReconnectAppParams; - [WRITE_COMMANDS.OPEN_PROFILE]: OpenProfileParams; - [WRITE_COMMANDS.HANDLE_RESTRICTED_EVENT]: HandleRestrictedEventParams; - [WRITE_COMMANDS.OPEN_REPORT]: OpenReportParams; - [WRITE_COMMANDS.DELETE_PAYMENT_BANK_ACCOUNT]: DeletePaymentBankAccountParams; - [WRITE_COMMANDS.UPDATE_PERSONAL_INFORMATION_FOR_BANK_ACCOUNT]: UpdatePersonalInformationForBankAccountParams; - [WRITE_COMMANDS.VALIDATE_BANK_ACCOUNT_WITH_TRANSACTIONS]: ValidateBankAccountWithTransactionsParams; - [WRITE_COMMANDS.UPDATE_COMPANY_INFORMATION_FOR_BANK_ACCOUNT]: UpdateCompanyInformationForBankAccountParams; + [WRITE_COMMANDS.UPDATE_PREFERRED_LOCALE]: Parameters.UpdatePreferredLocaleParams; + [WRITE_COMMANDS.RECONNECT_APP]: Parameters.ReconnectAppParams; + [WRITE_COMMANDS.OPEN_PROFILE]: Parameters.OpenProfileParams; + [WRITE_COMMANDS.HANDLE_RESTRICTED_EVENT]: Parameters.HandleRestrictedEventParams; + [WRITE_COMMANDS.OPEN_REPORT]: Parameters.OpenReportParams; + [WRITE_COMMANDS.DELETE_PAYMENT_BANK_ACCOUNT]: Parameters.DeletePaymentBankAccountParams; + [WRITE_COMMANDS.UPDATE_PERSONAL_INFORMATION_FOR_BANK_ACCOUNT]: Parameters.UpdatePersonalInformationForBankAccountParams; + [WRITE_COMMANDS.VALIDATE_BANK_ACCOUNT_WITH_TRANSACTIONS]: Parameters.ValidateBankAccountWithTransactionsParams; + [WRITE_COMMANDS.UPDATE_COMPANY_INFORMATION_FOR_BANK_ACCOUNT]: Parameters.UpdateCompanyInformationForBankAccountParams; [WRITE_COMMANDS.UPDATE_BENEFICIAL_OWNERS_FOR_BANK_ACCOUNT]: UpdateBeneficialOwnersForBankAccountParams; - [WRITE_COMMANDS.CONNECT_BANK_ACCOUNT_MANUALLY]: ConnectBankAccountManuallyParams; - [WRITE_COMMANDS.VERIFY_IDENTITY_FOR_BANK_ACCOUNT]: VerifyIdentityForBankAccountParams; - [WRITE_COMMANDS.BANK_ACCOUNT_HANDLE_PLAID_ERROR]: BankAccountHandlePlaidErrorParams; - [WRITE_COMMANDS.REPORT_VIRTUAL_EXPENSIFY_CARD_FRAUD]: ReportVirtualExpensifyCardFraudParams; - [WRITE_COMMANDS.REQUEST_REPLACEMENT_EXPENSIFY_CARD]: RequestReplacementExpensifyCardParams; - [WRITE_COMMANDS.ACTIVATE_PHYSICAL_EXPENSIFY_CARD]: ActivatePhysicalExpensifyCardParams; - [WRITE_COMMANDS.MAKE_DEFAULT_PAYMENT_METHOD]: MakeDefaultPaymentMethodParams; - [WRITE_COMMANDS.ADD_PAYMENT_CARD]: AddPaymentCardParams; - [WRITE_COMMANDS.DELETE_PAYMENT_CARD]: DeletePaymentCardParams; - [WRITE_COMMANDS.UPDATE_PRONOUNS]: UpdatePronounsParams; - [WRITE_COMMANDS.UPDATE_DISPLAY_NAME]: UpdateDisplayNameParams; - [WRITE_COMMANDS.UPDATE_LEGAL_NAME]: UpdateLegalNameParams; - [WRITE_COMMANDS.UPDATE_DATE_OF_BIRTH]: UpdateDateOfBirthParams; - [WRITE_COMMANDS.UPDATE_HOME_ADDRESS]: UpdateHomeAddressParams; - [WRITE_COMMANDS.UPDATE_AUTOMATIC_TIMEZONE]: UpdateAutomaticTimezoneParams; - [WRITE_COMMANDS.UPDATE_SELECTED_TIMEZONE]: UpdateSelectedTimezoneParams; - [WRITE_COMMANDS.UPDATE_USER_AVATAR]: UpdateUserAvatarParams; + [WRITE_COMMANDS.CONNECT_BANK_ACCOUNT_MANUALLY]: Parameters.ConnectBankAccountManuallyParams; + [WRITE_COMMANDS.VERIFY_IDENTITY_FOR_BANK_ACCOUNT]: Parameters.VerifyIdentityForBankAccountParams; + [WRITE_COMMANDS.BANK_ACCOUNT_HANDLE_PLAID_ERROR]: Parameters.BankAccountHandlePlaidErrorParams; + [WRITE_COMMANDS.REPORT_VIRTUAL_EXPENSIFY_CARD_FRAUD]: Parameters.ReportVirtualExpensifyCardFraudParams; + [WRITE_COMMANDS.REQUEST_REPLACEMENT_EXPENSIFY_CARD]: Parameters.RequestReplacementExpensifyCardParams; + [WRITE_COMMANDS.ACTIVATE_PHYSICAL_EXPENSIFY_CARD]: Parameters.ActivatePhysicalExpensifyCardParams; + [WRITE_COMMANDS.MAKE_DEFAULT_PAYMENT_METHOD]: Parameters.MakeDefaultPaymentMethodParams; + [WRITE_COMMANDS.ADD_PAYMENT_CARD]: Parameters.AddPaymentCardParams; + [WRITE_COMMANDS.DELETE_PAYMENT_CARD]: Parameters.DeletePaymentCardParams; + [WRITE_COMMANDS.UPDATE_PRONOUNS]: Parameters.UpdatePronounsParams; + [WRITE_COMMANDS.UPDATE_DISPLAY_NAME]: Parameters.UpdateDisplayNameParams; + [WRITE_COMMANDS.UPDATE_LEGAL_NAME]: Parameters.UpdateLegalNameParams; + [WRITE_COMMANDS.UPDATE_DATE_OF_BIRTH]: Parameters.UpdateDateOfBirthParams; + [WRITE_COMMANDS.UPDATE_HOME_ADDRESS]: Parameters.UpdateHomeAddressParams; + [WRITE_COMMANDS.UPDATE_AUTOMATIC_TIMEZONE]: Parameters.UpdateAutomaticTimezoneParams; + [WRITE_COMMANDS.UPDATE_SELECTED_TIMEZONE]: Parameters.UpdateSelectedTimezoneParams; + [WRITE_COMMANDS.UPDATE_USER_AVATAR]: Parameters.UpdateUserAvatarParams; [WRITE_COMMANDS.DELETE_USER_AVATAR]: EmptyObject; - [WRITE_COMMANDS.REFER_TEACHERS_UNITE_VOLUNTEER]: ReferTeachersUniteVolunteerParams; - [WRITE_COMMANDS.ADD_SCHOOL_PRINCIPAL]: AddSchoolPrincipalParams; - [WRITE_COMMANDS.CLOSE_ACCOUNT]: CloseAccountParams; - [WRITE_COMMANDS.REQUEST_CONTACT_METHOD_VALIDATE_CODE]: RequestContactMethodValidateCodeParams; - [WRITE_COMMANDS.UPDATE_NEWSLETTER_SUBSCRIPTION]: UpdateNewsletterSubscriptionParams; - [WRITE_COMMANDS.DELETE_CONTACT_METHOD]: DeleteContactMethodParams; - [WRITE_COMMANDS.ADD_NEW_CONTACT_METHOD]: AddNewContactMethodParams; - [WRITE_COMMANDS.VALIDATE_LOGIN]: ValidateLoginParams; - [WRITE_COMMANDS.VALIDATE_SECONDARY_LOGIN]: ValidateSecondaryLoginParams; - [WRITE_COMMANDS.UPDATE_PREFERRED_EMOJI_SKIN_TONE]: UpdatePreferredEmojiSkinToneParams; - [WRITE_COMMANDS.UPDATE_FREQUENTLY_USED_EMOJIS]: UpdateFrequentlyUsedEmojisParams; - [WRITE_COMMANDS.UPDATE_CHAT_PRIORITY_MODE]: UpdateChatPriorityModeParams; - [WRITE_COMMANDS.SET_CONTACT_METHOD_AS_DEFAULT]: SetContactMethodAsDefaultParams; - [WRITE_COMMANDS.UPDATE_THEME]: UpdateThemeParams; - [WRITE_COMMANDS.UPDATE_STATUS]: UpdateStatusParams; + [WRITE_COMMANDS.REFER_TEACHERS_UNITE_VOLUNTEER]: Parameters.ReferTeachersUniteVolunteerParams; + [WRITE_COMMANDS.ADD_SCHOOL_PRINCIPAL]: Parameters.AddSchoolPrincipalParams; + [WRITE_COMMANDS.CLOSE_ACCOUNT]: Parameters.CloseAccountParams; + [WRITE_COMMANDS.REQUEST_CONTACT_METHOD_VALIDATE_CODE]: Parameters.RequestContactMethodValidateCodeParams; + [WRITE_COMMANDS.UPDATE_NEWSLETTER_SUBSCRIPTION]: Parameters.UpdateNewsletterSubscriptionParams; + [WRITE_COMMANDS.DELETE_CONTACT_METHOD]: Parameters.DeleteContactMethodParams; + [WRITE_COMMANDS.ADD_NEW_CONTACT_METHOD]: Parameters.AddNewContactMethodParams; + [WRITE_COMMANDS.VALIDATE_LOGIN]: Parameters.ValidateLoginParams; + [WRITE_COMMANDS.VALIDATE_SECONDARY_LOGIN]: Parameters.ValidateSecondaryLoginParams; + [WRITE_COMMANDS.UPDATE_PREFERRED_EMOJI_SKIN_TONE]: Parameters.UpdatePreferredEmojiSkinToneParams; + [WRITE_COMMANDS.UPDATE_FREQUENTLY_USED_EMOJIS]: Parameters.UpdateFrequentlyUsedEmojisParams; + [WRITE_COMMANDS.UPDATE_CHAT_PRIORITY_MODE]: Parameters.UpdateChatPriorityModeParams; + [WRITE_COMMANDS.SET_CONTACT_METHOD_AS_DEFAULT]: Parameters.SetContactMethodAsDefaultParams; + [WRITE_COMMANDS.UPDATE_THEME]: Parameters.UpdateThemeParams; + [WRITE_COMMANDS.UPDATE_STATUS]: Parameters.UpdateStatusParams; [WRITE_COMMANDS.CLEAR_STATUS]: EmptyObject; - [WRITE_COMMANDS.UPDATE_PERSONAL_DETAILS_FOR_WALLET]: UpdatePersonalDetailsForWalletParams; - [WRITE_COMMANDS.VERIFY_IDENTITY]: VerifyIdentityParams; - [WRITE_COMMANDS.ACCEPT_WALLET_TERMS]: AcceptWalletTermsParams; - [WRITE_COMMANDS.ANSWER_QUESTIONS_FOR_WALLET]: AnswerQuestionsForWalletParams; - [WRITE_COMMANDS.REQUEST_PHYSICAL_EXPENSIFY_CARD]: RequestPhysicalExpensifyCardParams; - [WRITE_COMMANDS.LOG_OUT]: LogOutParams; - [WRITE_COMMANDS.REQUEST_ACCOUNT_VALIDATION_LINK]: RequestAccountValidationLinkParams; - [WRITE_COMMANDS.REQUEST_NEW_VALIDATE_CODE]: RequestNewValidateCodeParams; - [WRITE_COMMANDS.SIGN_IN_WITH_APPLE]: SignInWithAppleParams; - [WRITE_COMMANDS.SIGN_IN_WITH_GOOGLE]: SignInWithGoogleParams; + [WRITE_COMMANDS.UPDATE_PERSONAL_DETAILS_FOR_WALLET]: Parameters.UpdatePersonalDetailsForWalletParams; + [WRITE_COMMANDS.VERIFY_IDENTITY]: Parameters.VerifyIdentityParams; + [WRITE_COMMANDS.ACCEPT_WALLET_TERMS]: Parameters.AcceptWalletTermsParams; + [WRITE_COMMANDS.ANSWER_QUESTIONS_FOR_WALLET]: Parameters.AnswerQuestionsForWalletParams; + [WRITE_COMMANDS.REQUEST_PHYSICAL_EXPENSIFY_CARD]: Parameters.RequestPhysicalExpensifyCardParams; + [WRITE_COMMANDS.LOG_OUT]: Parameters.LogOutParams; + [WRITE_COMMANDS.REQUEST_ACCOUNT_VALIDATION_LINK]: Parameters.RequestAccountValidationLinkParams; + [WRITE_COMMANDS.REQUEST_NEW_VALIDATE_CODE]: Parameters.RequestNewValidateCodeParams; + [WRITE_COMMANDS.SIGN_IN_WITH_APPLE]: Parameters.BeginAppleSignInParams; + [WRITE_COMMANDS.SIGN_IN_WITH_GOOGLE]: Parameters.BeginGoogleSignInParams; [WRITE_COMMANDS.SIGN_IN_USER]: SignInUserParams; - [WRITE_COMMANDS.SIGN_IN_USER_WITH_LINK]: SignInUserWithLinkParams; - [WRITE_COMMANDS.REQUEST_UNLINK_VALIDATION_LINK]: RequestUnlinkValidationLinkParams; - [WRITE_COMMANDS.UNLINK_LOGIN]: UnlinkLoginParams; + [WRITE_COMMANDS.SIGN_IN_USER_WITH_LINK]: Parameters.SignInUserWithLinkParams; + [WRITE_COMMANDS.REQUEST_UNLINK_VALIDATION_LINK]: Parameters.RequestUnlinkValidationLinkParams; + [WRITE_COMMANDS.UNLINK_LOGIN]: Parameters.UnlinkLoginParams; [WRITE_COMMANDS.ENABLE_TWO_FACTOR_AUTH]: EmptyObject; [WRITE_COMMANDS.DISABLE_TWO_FACTOR_AUTH]: EmptyObject; - [WRITE_COMMANDS.TWO_FACTOR_AUTH_VALIDATE]: ValidateTwoFactorAuthParams; - [WRITE_COMMANDS.ADD_COMMENT]: AddCommentOrAttachementParams; - [WRITE_COMMANDS.ADD_ATTACHMENT]: AddCommentOrAttachementParams; - [WRITE_COMMANDS.CONNECT_BANK_ACCOUNT_WITH_PLAID]: ConnectBankAccountWithPlaidParams; - [WRITE_COMMANDS.ADD_PERSONAL_BANK_ACCOUNT]: AddPersonalBankAccountParams; - [WRITE_COMMANDS.OPT_IN_TO_PUSH_NOTIFICATIONS]: OptInToPushNotificationsParams; - [WRITE_COMMANDS.OPT_OUT_OF_PUSH_NOTIFICATIONS]: OptOutOfPushNotificationsParams; - [WRITE_COMMANDS.RECONNECT_TO_REPORT]: ReconnectToReportParams; - [WRITE_COMMANDS.READ_NEWEST_ACTION]: ReadNewestActionParams; - [WRITE_COMMANDS.MARK_AS_UNREAD]: MarkAsUnreadParams; - [WRITE_COMMANDS.TOGGLE_PINNED_CHAT]: TogglePinnedChatParams; - [WRITE_COMMANDS.DELETE_COMMENT]: DeleteCommentParams; - [WRITE_COMMANDS.UPDATE_COMMENT]: UpdateCommentParams; - [WRITE_COMMANDS.UPDATE_REPORT_NOTIFICATION_PREFERENCE]: UpdateReportNotificationPreferenceParams; - [WRITE_COMMANDS.UPDATE_WELCOME_MESSAGE]: UpdateWelcomeMessageParams; - [WRITE_COMMANDS.UPDATE_REPORT_WRITE_CAPABILITY]: UpdateReportWriteCapabilityParams; - [WRITE_COMMANDS.ADD_WORKSPACE_ROOM]: AddWorkspaceRoomParams; - [WRITE_COMMANDS.UPDATE_POLICY_ROOM_NAME]: UpdatePolicyRoomNameParams; - [WRITE_COMMANDS.ADD_EMOJI_REACTION]: AddEmojiReactionParams; - [WRITE_COMMANDS.REMOVE_EMOJI_REACTION]: RemoveEmojiReactionParams; - [WRITE_COMMANDS.LEAVE_ROOM]: LeaveRoomParams; - [WRITE_COMMANDS.INVITE_TO_ROOM]: InviteToRoomParams; - [WRITE_COMMANDS.REMOVE_FROM_ROOM]: RemoveFromRoomParams; - [WRITE_COMMANDS.FLAG_COMMENT]: FlagCommentParams; - [WRITE_COMMANDS.UPDATE_REPORT_PRIVATE_NOTE]: UpdateReportPrivateNoteParams; - [WRITE_COMMANDS.RESOLVE_ACTIONABLE_MENTION_WHISPER]: ResolveActionableMentionWhisperParams; + [WRITE_COMMANDS.TWO_FACTOR_AUTH_VALIDATE]: Parameters.ValidateTwoFactorAuthParams; + [WRITE_COMMANDS.ADD_COMMENT]: Parameters.AddCommentOrAttachementParams; + [WRITE_COMMANDS.ADD_ATTACHMENT]: Parameters.AddCommentOrAttachementParams; + [WRITE_COMMANDS.CONNECT_BANK_ACCOUNT_WITH_PLAID]: Parameters.ConnectBankAccountWithPlaidParams; + [WRITE_COMMANDS.ADD_PERSONAL_BANK_ACCOUNT]: Parameters.AddPersonalBankAccountParams; + [WRITE_COMMANDS.OPT_IN_TO_PUSH_NOTIFICATIONS]: Parameters.OptInOutToPushNotificationsParams; + [WRITE_COMMANDS.OPT_OUT_OF_PUSH_NOTIFICATIONS]: Parameters.OptInOutToPushNotificationsParams; + [WRITE_COMMANDS.RECONNECT_TO_REPORT]: Parameters.ReconnectToReportParams; + [WRITE_COMMANDS.READ_NEWEST_ACTION]: Parameters.ReadNewestActionParams; + [WRITE_COMMANDS.MARK_AS_UNREAD]: Parameters.MarkAsUnreadParams; + [WRITE_COMMANDS.TOGGLE_PINNED_CHAT]: Parameters.TogglePinnedChatParams; + [WRITE_COMMANDS.DELETE_COMMENT]: Parameters.DeleteCommentParams; + [WRITE_COMMANDS.UPDATE_COMMENT]: Parameters.UpdateCommentParams; + [WRITE_COMMANDS.UPDATE_REPORT_NOTIFICATION_PREFERENCE]: Parameters.UpdateReportNotificationPreferenceParams; + [WRITE_COMMANDS.UPDATE_WELCOME_MESSAGE]: Parameters.UpdateWelcomeMessageParams; + [WRITE_COMMANDS.UPDATE_REPORT_WRITE_CAPABILITY]: Parameters.UpdateReportWriteCapabilityParams; + [WRITE_COMMANDS.ADD_WORKSPACE_ROOM]: Parameters.AddWorkspaceRoomParams; + [WRITE_COMMANDS.UPDATE_POLICY_ROOM_NAME]: Parameters.UpdatePolicyRoomNameParams; + [WRITE_COMMANDS.ADD_EMOJI_REACTION]: Parameters.AddEmojiReactionParams; + [WRITE_COMMANDS.REMOVE_EMOJI_REACTION]: Parameters.RemoveEmojiReactionParams; + [WRITE_COMMANDS.LEAVE_ROOM]: Parameters.LeaveRoomParams; + [WRITE_COMMANDS.INVITE_TO_ROOM]: Parameters.InviteToRoomParams; + [WRITE_COMMANDS.REMOVE_FROM_ROOM]: Parameters.RemoveFromRoomParams; + [WRITE_COMMANDS.FLAG_COMMENT]: Parameters.FlagCommentParams; + [WRITE_COMMANDS.UPDATE_REPORT_PRIVATE_NOTE]: Parameters.UpdateReportPrivateNoteParams; + [WRITE_COMMANDS.RESOLVE_ACTIONABLE_MENTION_WHISPER]: Parameters.ResolveActionableMentionWhisperParams; }; const READ_COMMANDS = { @@ -291,30 +219,30 @@ const READ_COMMANDS = { type ReadCommand = ValueOf; type ReadCommandParameters = { - [READ_COMMANDS.OPEN_APP]: OpenAppParams; - [READ_COMMANDS.OPEN_REIMBURSEMENT_ACCOUNT_PAGE]: OpenReimbursementAccountPageParams; + [READ_COMMANDS.OPEN_APP]: Parameters.OpenAppParams; + [READ_COMMANDS.OPEN_REIMBURSEMENT_ACCOUNT_PAGE]: Parameters.OpenReimbursementAccountPageParams; [READ_COMMANDS.OPEN_WORKSPACE_VIEW]: EmptyObject; [READ_COMMANDS.GET_MAPBOX_ACCESS_TOKEN]: EmptyObject; [READ_COMMANDS.OPEN_PAYMENTS_PAGE]: EmptyObject; [READ_COMMANDS.OPEN_PERSONAL_DETAILS_PAGE]: EmptyObject; - [READ_COMMANDS.OPEN_PUBLIC_PROFILE_PAGE]: OpenPublicProfilePageParams; - [READ_COMMANDS.OPEN_PLAID_BANK_LOGIN]: OpenPlaidBankLoginParams; - [READ_COMMANDS.OPEN_PLAID_BANK_ACCOUNT_SELECTOR]: OpenPlaidBankAccountSelectorParams; - [READ_COMMANDS.GET_OLDER_ACTIONS]: GetOlderActionsParams; - [READ_COMMANDS.GET_NEWER_ACTIONS]: GetNewerActionsParams; - [READ_COMMANDS.EXPAND_URL_PREVIEW]: ExpandURLPreviewParams; - [READ_COMMANDS.GET_REPORT_PRIVATE_NOTE]: GetReportPrivateNoteParams; - [READ_COMMANDS.OPEN_ROOM_MEMBERS_PAGE]: OpenRoomMembersPageParams; - [READ_COMMANDS.SEARCH_FOR_REPORTS]: SearchForReportsParams; - [READ_COMMANDS.SEND_PERFORMANCE_TIMING]: SendPerformanceTimingParams; - [READ_COMMANDS.GET_ROUTE]: GetRouteParams; - [READ_COMMANDS.GET_ROUTE_FOR_DRAFT]: GetRouteForDraftParams; - [READ_COMMANDS.GET_STATEMENT_PDF]: GetStatementPDFParams; + [READ_COMMANDS.OPEN_PUBLIC_PROFILE_PAGE]: Parameters.OpenPublicProfilePageParams; + [READ_COMMANDS.OPEN_PLAID_BANK_LOGIN]: Parameters.OpenPlaidBankLoginParams; + [READ_COMMANDS.OPEN_PLAID_BANK_ACCOUNT_SELECTOR]: Parameters.OpenPlaidBankAccountSelectorParams; + [READ_COMMANDS.GET_OLDER_ACTIONS]: Parameters.GetOlderActionsParams; + [READ_COMMANDS.GET_NEWER_ACTIONS]: Parameters.GetNewerActionsParams; + [READ_COMMANDS.EXPAND_URL_PREVIEW]: Parameters.ExpandURLPreviewParams; + [READ_COMMANDS.GET_REPORT_PRIVATE_NOTE]: Parameters.GetReportPrivateNoteParams; + [READ_COMMANDS.OPEN_ROOM_MEMBERS_PAGE]: Parameters.OpenRoomMembersPageParams; + [READ_COMMANDS.SEARCH_FOR_REPORTS]: Parameters.SearchForReportsParams; + [READ_COMMANDS.SEND_PERFORMANCE_TIMING]: Parameters.SendPerformanceTimingParams; + [READ_COMMANDS.GET_ROUTE]: Parameters.GetRouteParams; + [READ_COMMANDS.GET_ROUTE_FOR_DRAFT]: Parameters.GetRouteForDraftParams; + [READ_COMMANDS.GET_STATEMENT_PDF]: Parameters.GetStatementPDFParams; [READ_COMMANDS.OPEN_ONFIDO_FLOW]: EmptyObject; [READ_COMMANDS.OPEN_INITIAL_SETTINGS_PAGE]: EmptyObject; [READ_COMMANDS.OPEN_ENABLE_PAYMENTS_PAGE]: EmptyObject; - [READ_COMMANDS.BEGIN_SIGNIN]: BeginSignInParams; - [READ_COMMANDS.SIGN_IN_WITH_SHORT_LIVED_AUTH_TOKEN]: SignInWithShortLivedAuthTokenParams; + [READ_COMMANDS.BEGIN_SIGNIN]: Parameters.BeginSignInParams; + [READ_COMMANDS.SIGN_IN_WITH_SHORT_LIVED_AUTH_TOKEN]: Parameters.SignInWithShortLivedAuthTokenParams; }; const SIDE_EFFECT_REQUEST_COMMANDS = { @@ -329,12 +257,12 @@ const SIDE_EFFECT_REQUEST_COMMANDS = { type SideEffectRequestCommand = ReadCommand | ValueOf; type SideEffectRequestCommandParameters = ReadCommandParameters & { - [SIDE_EFFECT_REQUEST_COMMANDS.AUTHENTICATE_PUSHER]: AuthenticatePusherParams; - [SIDE_EFFECT_REQUEST_COMMANDS.OPEN_REPORT]: OpenReportParams; - [SIDE_EFFECT_REQUEST_COMMANDS.OPEN_OLD_DOT_LINK]: OpenOldDotLinkParams; - [SIDE_EFFECT_REQUEST_COMMANDS.REVEAL_EXPENSIFY_CARD_DETAILS]: RevealExpensifyCardDetailsParams; - [SIDE_EFFECT_REQUEST_COMMANDS.GET_MISSING_ONYX_MESSAGES]: GetMissingOnyxMessagesParams; - [SIDE_EFFECT_REQUEST_COMMANDS.RECONNECT_APP]: ReconnectAppParams; + [SIDE_EFFECT_REQUEST_COMMANDS.AUTHENTICATE_PUSHER]: Parameters.AuthenticatePusherParams; + [SIDE_EFFECT_REQUEST_COMMANDS.OPEN_REPORT]: Parameters.OpenReportParams; + [SIDE_EFFECT_REQUEST_COMMANDS.OPEN_OLD_DOT_LINK]: Parameters.OpenOldDotLinkParams; + [SIDE_EFFECT_REQUEST_COMMANDS.REVEAL_EXPENSIFY_CARD_DETAILS]: Parameters.RevealExpensifyCardDetailsParams; + [SIDE_EFFECT_REQUEST_COMMANDS.GET_MISSING_ONYX_MESSAGES]: Parameters.GetMissingOnyxMessagesParams; + [SIDE_EFFECT_REQUEST_COMMANDS.RECONNECT_APP]: Parameters.ReconnectAppParams; }; export {WRITE_COMMANDS, READ_COMMANDS, SIDE_EFFECT_REQUEST_COMMANDS}; diff --git a/src/libs/actions/BankAccounts.ts b/src/libs/actions/BankAccounts.ts index 28334f359566..58509379b232 100644 --- a/src/libs/actions/BankAccounts.ts +++ b/src/libs/actions/BankAccounts.ts @@ -7,10 +7,13 @@ import type { ConnectBankAccountWithPlaidParams, DeletePaymentBankAccountParams, OpenReimbursementAccountPageParams, + UpdateCompanyInformationForBankAccountParams, UpdatePersonalInformationForBankAccountParams, ValidateBankAccountWithTransactionsParams, VerifyIdentityForBankAccountParams, } from '@libs/API/parameters'; +import type UpdateBeneficialOwnersForBankAccountParams from '@libs/API/parameters/UpdateBeneficialOwnersForBankAccountParams'; +import type {BankAccountCompanyInformation} from '@libs/API/parameters/UpdateCompanyInformationForBankAccountParams'; import {READ_COMMANDS, WRITE_COMMANDS} from '@libs/API/types'; import * as ErrorUtils from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; @@ -20,7 +23,7 @@ import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type PlaidBankAccount from '@src/types/onyx/PlaidBankAccount'; import type {BankAccountStep, BankAccountSubStep} from '@src/types/onyx/ReimbursementAccount'; -import type {ACHContractStepProps, BankAccountStepProps, CompanyStepProps, OnfidoData, ReimbursementAccountProps} from '@src/types/onyx/ReimbursementAccountDraft'; +import type {OnfidoData} from '@src/types/onyx/ReimbursementAccountDraft'; import type {OnyxData} from '@src/types/onyx/Request'; import * as ReimbursementAccount from './ReimbursementAccount'; @@ -39,8 +42,6 @@ export { export {openPlaidBankAccountSelector, openPlaidBankLogin} from './Plaid'; export {openOnfidoFlow, answerQuestionsForWallet, verifyIdentity, acceptWalletTerms} from './Wallet'; -type BankAccountCompanyInformation = BankAccountStepProps & CompanyStepProps & ReimbursementAccountProps; - type ReimbursementAccountStep = BankAccountStep | ''; type ReimbursementAccountSubStep = BankAccountSubStep | ''; @@ -325,8 +326,6 @@ function openReimbursementAccountPage(stepToOpen: ReimbursementAccountStep, subS * Updates the bank account in the database with the company step data */ function updateCompanyInformationForBankAccount(bankAccount: BankAccountCompanyInformation, policyID: string) { - type UpdateCompanyInformationForBankAccountParams = BankAccountCompanyInformation & {policyID: string}; - const parameters: UpdateCompanyInformationForBankAccountParams = {...bankAccount, policyID}; API.write(WRITE_COMMANDS.UPDATE_COMPANY_INFORMATION_FOR_BANK_ACCOUNT, parameters, getVBBADataForOnyx(CONST.BANK_ACCOUNT.STEP.COMPANY)); @@ -335,7 +334,7 @@ function updateCompanyInformationForBankAccount(bankAccount: BankAccountCompanyI /** * Add beneficial owners for the bank account, accept the ACH terms and conditions and verify the accuracy of the information provided */ -function updateBeneficialOwnersForBankAccount(params: ACHContractStepProps) { +function updateBeneficialOwnersForBankAccount(params: UpdateBeneficialOwnersForBankAccountParams) { API.write(WRITE_COMMANDS.UPDATE_BENEFICIAL_OWNERS_FOR_BANK_ACCOUNT, params, getVBBADataForOnyx()); } diff --git a/src/libs/actions/PersonalDetails.ts b/src/libs/actions/PersonalDetails.ts index d831adfb5e61..e7d9b48c46e9 100644 --- a/src/libs/actions/PersonalDetails.ts +++ b/src/libs/actions/PersonalDetails.ts @@ -2,7 +2,17 @@ import Str from 'expensify-common/lib/str'; import type {OnyxEntry, OnyxUpdate} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import * as API from '@libs/API'; -import type {OpenPublicProfilePageParams, UpdateAutomaticTimezoneParams, UpdateDateOfBirthParams, UpdateDisplayNameParams, UpdateHomeAddressParams, UpdateLegalNameParams, UpdatePronounsParams, UpdateSelectedTimezoneParams, UpdateUserAvatarParams} from '@libs/API/parameters'; +import type { + OpenPublicProfilePageParams, + UpdateAutomaticTimezoneParams, + UpdateDateOfBirthParams, + UpdateDisplayNameParams, + UpdateHomeAddressParams, + UpdateLegalNameParams, + UpdatePronounsParams, + UpdateSelectedTimezoneParams, + UpdateUserAvatarParams, +} from '@libs/API/parameters'; import {READ_COMMANDS, WRITE_COMMANDS} from '@libs/API/types'; import type {CustomRNImageManipulatorResult} from '@libs/cropOrRotateImage/types'; import DateUtils from '@libs/DateUtils'; diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 79ffcd05ddf1..629315314f5f 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -11,14 +11,33 @@ import type {Emoji} from '@assets/emojis/types'; import * as ActiveClientManager from '@libs/ActiveClientManager'; import * as API from '@libs/API'; import type { + AddCommentOrAttachementParams, + AddEmojiReactionParams, + AddWorkspaceRoomParams, + DeleteCommentParams, ExpandURLPreviewParams, + FlagCommentParams, GetNewerActionsParams, GetOlderActionsParams, GetReportPrivateNoteParams, + InviteToRoomParams, + LeaveRoomParams, + MarkAsUnreadParams, OpenReportParams, OpenRoomMembersPageParams, + ReadNewestActionParams, + ReconnectToReportParams, + RemoveEmojiReactionParams, + RemoveFromRoomParams, ResolveActionableMentionWhisperParams, SearchForReportsParams, + TogglePinnedChatParams, + UpdateCommentParams, + UpdatePolicyRoomNameParams, + UpdateReportNotificationPreferenceParams, + UpdateReportPrivateNoteParams, + UpdateReportWriteCapabilityParams, + UpdateWelcomeMessageParams, } from '@libs/API/parameters'; import {READ_COMMANDS, SIDE_EFFECT_REQUEST_COMMANDS, WRITE_COMMANDS} from '@libs/API/types'; import * as CollectionUtils from '@libs/CollectionUtils'; diff --git a/src/libs/actions/Session/index.ts b/src/libs/actions/Session/index.ts index 6a4bbffd9df2..d10763100a8f 100644 --- a/src/libs/actions/Session/index.ts +++ b/src/libs/actions/Session/index.ts @@ -21,6 +21,7 @@ import type { UnlinkLoginParams, ValidateTwoFactorAuthParams, } from '@libs/API/parameters'; +import type SignInUserParams from '@libs/API/parameters/SignInUserParams'; import {READ_COMMANDS, SIDE_EFFECT_REQUEST_COMMANDS, WRITE_COMMANDS} from '@libs/API/types'; import * as Authentication from '@libs/Authentication'; import * as ErrorUtils from '@libs/ErrorUtils'; diff --git a/src/libs/actions/TeachersUnite.ts b/src/libs/actions/TeachersUnite.ts index bea9e24a85cc..055d1f2b53a2 100644 --- a/src/libs/actions/TeachersUnite.ts +++ b/src/libs/actions/TeachersUnite.ts @@ -1,6 +1,7 @@ import Onyx from 'react-native-onyx'; import type {OnyxEntry, OnyxUpdate} from 'react-native-onyx'; import * as API from '@libs/API'; +import type {AddSchoolPrincipalParams, ReferTeachersUniteVolunteerParams} from '@libs/API/parameters'; import {WRITE_COMMANDS} from '@libs/API/types'; import Navigation from '@libs/Navigation/Navigation'; import * as OptionsListUtils from '@libs/OptionsListUtils'; @@ -9,7 +10,6 @@ import type {OptimisticCreatedReportAction} from '@libs/ReportUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {PersonalDetailsList} from '@src/types/onyx'; -import { AddSchoolPrincipalParams, ReferTeachersUniteVolunteerParams } from '@libs/API/parameters'; type CreationData = { reportID: string; diff --git a/src/libs/actions/User.ts b/src/libs/actions/User.ts index e16a9ec30857..ec1d7b723d3a 100644 --- a/src/libs/actions/User.ts +++ b/src/libs/actions/User.ts @@ -821,9 +821,7 @@ function clearCustomStatus() { }, }, ]; - API.write(WRITE_COMMANDS.CLEAR_STATUS, {}, { - optimisticData, - }); + API.write(WRITE_COMMANDS.CLEAR_STATUS, {}, {optimisticData}); } /** diff --git a/src/libs/actions/Wallet.ts b/src/libs/actions/Wallet.ts index 717b6df2680d..b03b5e8f6d3d 100644 --- a/src/libs/actions/Wallet.ts +++ b/src/libs/actions/Wallet.ts @@ -2,7 +2,13 @@ import type {OnyxUpdate} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import * as API from '@libs/API'; -import type {RequestPhysicalExpensifyCardParams} from '@libs/API/parameters'; +import type { + AcceptWalletTermsParams, + AnswerQuestionsForWalletParams, + RequestPhysicalExpensifyCardParams, + UpdatePersonalDetailsForWalletParams, + VerifyIdentityParams, +} from '@libs/API/parameters'; import {READ_COMMANDS, WRITE_COMMANDS} from '@libs/API/types'; import type {PrivatePersonalDetails} from '@libs/GetPhysicalCardUtils'; import type CONST from '@src/CONST'; @@ -10,32 +16,11 @@ import ONYXKEYS from '@src/ONYXKEYS'; import type {WalletAdditionalQuestionDetails} from '@src/types/onyx'; import type * as OnyxCommon from '@src/types/onyx/OnyxCommon'; -type WalletTerms = { - hasAcceptedTerms: boolean; - reportID: string; -}; - type WalletQuestionAnswer = { question: string; answer: string; }; -type IdentityVerification = { - onfidoData: string; -}; - -type PersonalDetails = { - phoneNumber: string; - legalFirstName: string; - legalLastName: string; - addressStreet: string; - addressCity: string; - addressState: string; - addressZip: string; - dob: string; - ssn: string; -}; - /** * Fetch and save locally the Onfido SDK token and applicantID * - The sdkToken is used to initialize the Onfido SDK client @@ -90,7 +75,7 @@ function setKYCWallSource(source?: ValueOf, chatRe /** * Validates a user's provided details against a series of checks */ -function updatePersonalDetails(personalDetails: PersonalDetails) { +function updatePersonalDetails(personalDetails: UpdatePersonalDetailsForWalletParams) { const optimisticData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, @@ -125,7 +110,7 @@ function updatePersonalDetails(personalDetails: PersonalDetails) { * The API will always return the updated userWallet in the response as a convenience so we can avoid an additional * API request to fetch the userWallet after we call VerifyIdentity */ -function verifyIdentity(parameters: IdentityVerification) { +function verifyIdentity(parameters: VerifyIdentityParams) { const optimisticData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, @@ -178,7 +163,7 @@ function verifyIdentity(parameters: IdentityVerification) { * * @param parameters.chatReportID When accepting the terms of wallet to pay an IOU, indicates the parent chat ID of the IOU */ -function acceptWalletTerms(parameters: WalletTerms) { +function acceptWalletTerms(parameters: AcceptWalletTermsParams) { const optimisticData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, @@ -218,7 +203,7 @@ function acceptWalletTerms(parameters: WalletTerms) { }, ]; - const requestParams: WalletTerms = {hasAcceptedTerms: parameters.hasAcceptedTerms, reportID: parameters.reportID}; + const requestParams: AcceptWalletTermsParams = {hasAcceptedTerms: parameters.hasAcceptedTerms, reportID: parameters.reportID}; API.write(WRITE_COMMANDS.ACCEPT_WALLET_TERMS, requestParams, {optimisticData, successData, failureData}); } From e98c37caa4f0cb8ab2dbc8d04b461a3d5ccf3e94 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Wed, 17 Jan 2024 20:58:09 +0100 Subject: [PATCH 275/580] Fix typecheck --- src/libs/API/index.ts | 12 ++---------- .../API/parameters/ChronosRemoveOOOEventParams.ts | 6 ++++++ .../API/parameters/TransferWalletBalanceParams.ts | 6 ++++++ src/libs/API/parameters/index.ts | 2 ++ src/libs/API/types.ts | 2 ++ src/libs/actions/Chronos.ts | 15 +++++++-------- src/libs/actions/PaymentMethods.ts | 7 ++----- 7 files changed, 27 insertions(+), 23 deletions(-) create mode 100644 src/libs/API/parameters/ChronosRemoveOOOEventParams.ts create mode 100644 src/libs/API/parameters/TransferWalletBalanceParams.ts diff --git a/src/libs/API/index.ts b/src/libs/API/index.ts index d3751e0db8d2..d53503e35055 100644 --- a/src/libs/API/index.ts +++ b/src/libs/API/index.ts @@ -9,15 +9,7 @@ import CONST from '@src/CONST'; import type OnyxRequest from '@src/types/onyx/Request'; import type Response from '@src/types/onyx/Response'; import pkg from '../../../package.json'; -import type { - ApiRequestWithSideEffects, - ReadCommand, - ReadCommandParameters, - SideEffectRequestCommand, - SideEffectRequestCommandParameters, - WriteCommand, - WriteCommandParameters, -} from './types'; +import type {ApiRequestWithSideEffects, ReadCommand, SideEffectRequestCommand, SideEffectRequestCommandParameters, WriteCommand, WriteCommandParameters} from './types'; // Setup API middlewares. Each request made will pass through a series of middleware functions that will get called in sequence (each one passing the result of the previous to the next). // Note: The ordering here is intentional as we want to Log, Recheck Connection, Reauthenticate, and Save the Response in Onyx. Errors thrown in one middleware will bubble to the next. @@ -163,7 +155,7 @@ function makeRequestWithSideEffects( * @param [onyxData.failureData] - Onyx instructions that will be passed to Onyx.update() when the response has jsonCode !== 200. * @param [onyxData.finallyData] - Onyx instructions that will be passed to Onyx.update() when the response has jsonCode === 200 or jsonCode !== 200. */ -function read(command: TCommand, apiCommandParameters: ReadCommandParameters[TCommand], onyxData: OnyxData = {}) { +function read(command: TCommand, apiCommandParameters: SideEffectRequestCommandParameters[TCommand], onyxData: OnyxData = {}) { // Ensure all write requests on the sequential queue have finished responding before running read requests. // Responses from read requests can overwrite the optimistic data inserted by // write requests that use the same Onyx keys and haven't responded yet. diff --git a/src/libs/API/parameters/ChronosRemoveOOOEventParams.ts b/src/libs/API/parameters/ChronosRemoveOOOEventParams.ts new file mode 100644 index 000000000000..4a4f8fe6008a --- /dev/null +++ b/src/libs/API/parameters/ChronosRemoveOOOEventParams.ts @@ -0,0 +1,6 @@ +type ChronosRemoveOOOEventParams = { + googleEventID: string; + reportActionID: string; +}; + +export default ChronosRemoveOOOEventParams; diff --git a/src/libs/API/parameters/TransferWalletBalanceParams.ts b/src/libs/API/parameters/TransferWalletBalanceParams.ts new file mode 100644 index 000000000000..c25268d92bf3 --- /dev/null +++ b/src/libs/API/parameters/TransferWalletBalanceParams.ts @@ -0,0 +1,6 @@ +import type {ValueOf} from 'type-fest'; +import type CONST from '@src/CONST'; + +type TransferWalletBalanceParams = Partial, number | undefined>>; + +export default TransferWalletBalanceParams; diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index 0e3e521d1441..a0a57b1b65d3 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -98,3 +98,5 @@ export type {default as UpdateCompanyInformationForBankAccountParams} from './Up export type {default as UpdatePersonalDetailsForWalletParams} from './UpdatePersonalDetailsForWalletParams'; export type {default as VerifyIdentityParams} from './VerifyIdentityParams'; export type {default as AcceptWalletTermsParams} from './AcceptWalletTermsParams'; +export type {default as ChronosRemoveOOOEventParams} from './ChronosRemoveOOOEventParams'; +export type {default as TransferWalletBalanceParams} from './TransferWalletBalanceParams'; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index 593470507978..cd72efa89f22 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -187,6 +187,8 @@ type WriteCommandParameters = { [WRITE_COMMANDS.FLAG_COMMENT]: Parameters.FlagCommentParams; [WRITE_COMMANDS.UPDATE_REPORT_PRIVATE_NOTE]: Parameters.UpdateReportPrivateNoteParams; [WRITE_COMMANDS.RESOLVE_ACTIONABLE_MENTION_WHISPER]: Parameters.ResolveActionableMentionWhisperParams; + [WRITE_COMMANDS.CHRONOS_REMOVE_OOO_EVENT]: Parameters.ChronosRemoveOOOEventParams; + [WRITE_COMMANDS.TRANSFER_WALLET_BALANCE]: Parameters.TransferWalletBalanceParams; }; const READ_COMMANDS = { diff --git a/src/libs/actions/Chronos.ts b/src/libs/actions/Chronos.ts index e49ace95ce5c..548b8398beec 100644 --- a/src/libs/actions/Chronos.ts +++ b/src/libs/actions/Chronos.ts @@ -1,6 +1,7 @@ import type {OnyxUpdate} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import * as API from '@libs/API'; +import type {ChronosRemoveOOOEventParams} from '@libs/API/parameters'; import {WRITE_COMMANDS} from '@libs/API/types'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -47,14 +48,12 @@ const removeEvent = (reportID: string, reportActionID: string, eventID: string, }, ]; - API.write( - WRITE_COMMANDS.CHRONOS_REMOVE_OOO_EVENT, - { - googleEventID: eventID, - reportActionID, - }, - {optimisticData, successData, failureData}, - ); + const parameters: ChronosRemoveOOOEventParams = { + googleEventID: eventID, + reportActionID, + }; + + API.write(WRITE_COMMANDS.CHRONOS_REMOVE_OOO_EVENT, parameters, {optimisticData, successData, failureData}); }; export { diff --git a/src/libs/actions/PaymentMethods.ts b/src/libs/actions/PaymentMethods.ts index 4e5190929647..5984fda9752e 100644 --- a/src/libs/actions/PaymentMethods.ts +++ b/src/libs/actions/PaymentMethods.ts @@ -4,10 +4,9 @@ import type {NativeTouchEvent} from 'react-native'; import type {OnyxUpdate} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx/lib/types'; -import type {ValueOf} from 'type-fest'; import type {TransferMethod} from '@components/KYCWall/types'; import * as API from '@libs/API'; -import type {AddPaymentCardParams, DeletePaymentCardParams, MakeDefaultPaymentMethodParams, PaymentCardParams} from '@libs/API/parameters'; +import type {AddPaymentCardParams, DeletePaymentCardParams, MakeDefaultPaymentMethodParams, PaymentCardParams, TransferWalletBalanceParams} from '@libs/API/parameters'; import {READ_COMMANDS, WRITE_COMMANDS} from '@libs/API/types'; import * as CardUtils from '@libs/CardUtils'; import Navigation from '@libs/Navigation/Navigation'; @@ -216,9 +215,7 @@ function transferWalletBalance(paymentMethod: PaymentMethod) { const paymentMethodIDKey = paymentMethod.accountType === CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT ? CONST.PAYMENT_METHOD_ID_KEYS.BANK_ACCOUNT : CONST.PAYMENT_METHOD_ID_KEYS.DEBIT_CARD; - type TransferWalletBalanceParameters = Partial, number | undefined>>; - - const parameters: TransferWalletBalanceParameters = { + const parameters: TransferWalletBalanceParams = { [paymentMethodIDKey]: paymentMethod.methodID, }; From 19350f9ee56a493b87239c5759836f855550b8bc Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Wed, 17 Jan 2024 21:21:06 +0100 Subject: [PATCH 276/580] fix: callback deps --- .../AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.js b/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.js index 6d2e8ec6242a..0f6a4a54b1af 100644 --- a/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.js +++ b/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.js @@ -54,7 +54,7 @@ function BaseAttachmentViewPdf({ attachmentCarouselPagerContext.onTap(e); } }, - [attachmentCarouselPagerContext], + [attachmentCarouselPagerContext, onPressProp], ); return ( From bab77d98ce379575c432e7b401c18d8573f736f9 Mon Sep 17 00:00:00 2001 From: rory Date: Wed, 17 Jan 2024 16:26:54 -0800 Subject: [PATCH 277/580] Remove unused focusAndUpdateMultilineInputRange.ts --- src/libs/focusAndUpdateMultilineInputRange.ts | 24 ------------------- 1 file changed, 24 deletions(-) delete mode 100644 src/libs/focusAndUpdateMultilineInputRange.ts diff --git a/src/libs/focusAndUpdateMultilineInputRange.ts b/src/libs/focusAndUpdateMultilineInputRange.ts deleted file mode 100644 index 2e4a3d23631e..000000000000 --- a/src/libs/focusAndUpdateMultilineInputRange.ts +++ /dev/null @@ -1,24 +0,0 @@ -import type {TextInput} from 'react-native'; - -/** - * Focus a multiline text input and place the cursor at the end of the value (if there is a value in the input). - * - * When a multiline input contains a text value that goes beyond the scroll height, the cursor will be placed - * at the end of the text value, and automatically scroll the input field to this position after the field gains - * focus. This provides a better user experience in cases where the text in the field has to be edited. The auto- - * scroll behaviour works on all platforms except iOS native. - * See https://github.com/Expensify/App/issues/20836 for more details. - */ -export default function focusAndUpdateMultilineInputRange(input: TextInput | HTMLTextAreaElement) { - if (!input) { - return; - } - - input.focus(); - if ('setSelectionRange' in input && input.value) { - const length = input.value.length; - input.setSelectionRange(length, length); - // eslint-disable-next-line no-param-reassign - input.scrollTop = input.scrollHeight; - } -} From 827202a859667a123ba77f503cd9c850311d6109 Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Thu, 18 Jan 2024 10:04:09 +0700 Subject: [PATCH 278/580] clear distance when save waypoint --- src/libs/actions/Transaction.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libs/actions/Transaction.ts b/src/libs/actions/Transaction.ts index 430de0557674..bad35f68f1a2 100644 --- a/src/libs/actions/Transaction.ts +++ b/src/libs/actions/Transaction.ts @@ -73,6 +73,7 @@ function saveWaypoint(transactionID: string, index: string, waypoint: RecentWayp // Clear the existing route so that we don't show an old route routes: { route0: { + distance: null, geometry: { coordinates: null, }, From ed8e47eb0d2a93b3f69e76c384eb08e2c517a649 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Thu, 18 Jan 2024 09:01:23 +0100 Subject: [PATCH 279/580] Fix ts error --- src/components/ReportActionItem/ReportPreview.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ReportActionItem/ReportPreview.tsx b/src/components/ReportActionItem/ReportPreview.tsx index c04945302de4..785992de70b8 100644 --- a/src/components/ReportActionItem/ReportPreview.tsx +++ b/src/components/ReportActionItem/ReportPreview.tsx @@ -133,7 +133,7 @@ function ReportPreview({ const lastThreeTransactionsWithReceipts = transactionsWithReceipts.slice(-3); const lastThreeReceipts = lastThreeTransactionsWithReceipts.map((transaction) => ReceiptUtils.getThumbnailAndImageURIs(transaction)); let formattedMerchant = numberOfRequests === 1 && hasReceipts ? TransactionUtils.getMerchant(transactionsWithReceipts[0]) : null; - if (TransactionUtils.isPartialMerchant(formattedMerchant)) { + if (TransactionUtils.isPartialMerchant(formattedMerchant ?? '')) { formattedMerchant = null; } const hasPendingWaypoints = formattedMerchant && hasOnlyDistanceRequests && transactionsWithReceipts.every((transaction) => transaction.pendingFields?.waypoints); From 78493ff8a333320cb3f00857286416e4e594ff31 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Thu, 18 Jan 2024 16:21:17 +0800 Subject: [PATCH 280/580] copy the action text if available --- .../report/ContextMenu/ContextMenuActions.tsx | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx index ea25a00ee1d3..d088583417c5 100644 --- a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx +++ b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx @@ -32,11 +32,16 @@ import type IconAsset from '@src/types/utils/IconAsset'; import {hideContextMenu, showContextMenu, showDeleteModal} from './ReportActionContextMenu'; /** Gets the HTML version of the message in an action */ -function getActionText(reportAction: OnyxEntry): string { +function getActionHtml(reportAction: OnyxEntry): string { const message = reportAction?.message?.at(-1) ?? null; return message?.html ?? ''; } +/** Gets the text version of the message in an action */ +function getActionText(reportAction: OnyxEntry): string { + return reportAction?.message?.reduce((acc, curr) => `${acc}${curr.text}`, '') ?? ''; +} + /** Sets the HTML string to Clipboard */ function setClipboardMessage(content: string) { const parser = new ExpensiMark(); @@ -162,7 +167,7 @@ const ContextMenuActions: ContextMenuAction[] = [ ); }, onPress: (closePopover, {reportAction}) => { - const html = getActionText(reportAction); + const html = getActionHtml(reportAction); const {originalFileName, sourceURL} = getAttachmentDetails(html); const sourceURLWithAuth = addEncryptedAuthTokenToURL(sourceURL ?? ''); const sourceID = (sourceURL?.match(CONST.REGEX.ATTACHMENT_ID) ?? [])[1]; @@ -220,7 +225,7 @@ const ContextMenuActions: ContextMenuAction[] = [ } const editAction = () => { if (!draftMessage) { - Report.saveReportActionDraft(reportID, reportAction, getActionText(reportAction)); + Report.saveReportActionDraft(reportID, reportAction, getActionHtml(reportAction)); } else { Report.deleteReportActionDraft(reportID, reportAction); } @@ -371,7 +376,8 @@ const ContextMenuActions: ContextMenuAction[] = [ onPress: (closePopover, {reportAction, selection}) => { const isTaskAction = ReportActionsUtils.isTaskAction(reportAction); const isReportPreviewAction = ReportActionsUtils.isReportPreviewAction(reportAction); - const messageHtml = isTaskAction ? TaskUtils.getTaskReportActionMessage(reportAction?.actionName) : getActionText(reportAction); + const messageHtml = isTaskAction ? TaskUtils.getTaskReportActionMessage(reportAction?.actionName) : getActionHtml(reportAction); + const messageText = getActionText(reportAction); const isAttachment = ReportActionsUtils.isReportActionAttachment(reportAction); if (!isAttachment) { @@ -392,11 +398,10 @@ const ContextMenuActions: ContextMenuAction[] = [ } else if (ReportActionsUtils.isMemberChangeAction(reportAction)) { const logMessage = ReportActionsUtils.getMemberChangeMessageFragment(reportAction).html ?? ''; setClipboardMessage(logMessage); - } else if (ReportActionsUtils.isSubmittedExpenseAction(reportAction)) { - const submittedMessage = reportAction?.message?.reduce((acc, curr) => `${acc}${curr.text}`, ''); - Clipboard.setString(submittedMessage ?? ''); } else if (content) { setClipboardMessage(content); + } else if (messageText) { + Clipboard.setString(messageText) } } From 67307aec0252dae8d764dc4d05f0c867d0ff35ed Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Thu, 18 Jan 2024 16:21:24 +0800 Subject: [PATCH 281/580] remove unused function --- src/libs/ReportActionsUtils.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index f967cb244268..a7d6438809e0 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -111,10 +111,6 @@ function isModifiedExpenseAction(reportAction: OnyxEntry): boolean return reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.MODIFIEDEXPENSE; } -function isSubmittedExpenseAction(reportAction: OnyxEntry): boolean { - return reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.SUBMITTED; -} - function isWhisperAction(reportAction: OnyxEntry): boolean { return (reportAction?.whisperedToAccountIDs ?? []).length > 0; } @@ -834,7 +830,6 @@ export { isDeletedParentAction, isMessageDeleted, isModifiedExpenseAction, - isSubmittedExpenseAction, isMoneyRequestAction, isNotifiableReportAction, isPendingRemove, From 5f205e741b366a981f4eb215fef03c6f9b628589 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Thu, 18 Jan 2024 16:39:06 +0800 Subject: [PATCH 282/580] prettier --- src/pages/home/report/ContextMenu/ContextMenuActions.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx index d088583417c5..52ee7d3c269d 100644 --- a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx +++ b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx @@ -401,7 +401,7 @@ const ContextMenuActions: ContextMenuAction[] = [ } else if (content) { setClipboardMessage(content); } else if (messageText) { - Clipboard.setString(messageText) + Clipboard.setString(messageText); } } From 05ade3ceb3bed7ad1238d4342a30f2ef39064f9c Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Thu, 18 Jan 2024 10:56:04 +0100 Subject: [PATCH 283/580] fix: pdf; don't allow tap when zoomed in --- .../AttachmentViewPdf/BaseAttachmentViewPdf.js | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.js b/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.js index 0f6a4a54b1af..e9ad8f5e529b 100644 --- a/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.js +++ b/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.js @@ -1,4 +1,4 @@ -import React, {memo, useCallback, useContext, useEffect} from 'react'; +import React, {memo, useCallback, useContext, useEffect, useState} from 'react'; import AttachmentCarouselPagerContext from '@components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext'; import PDFView from '@components/PDFView'; import {attachmentViewPdfDefaultProps, attachmentViewPdfPropTypes} from './propTypes'; @@ -16,6 +16,7 @@ function BaseAttachmentViewPdf({ style, }) { const attachmentCarouselPagerContext = useContext(AttachmentCarouselPagerContext); + const [scale, setScale] = useState(); useEffect(() => { if (!attachmentCarouselPagerContext) { @@ -26,12 +27,13 @@ function BaseAttachmentViewPdf({ }, []); const onScaleChanged = useCallback( - (scale) => { - onScaleChangedProp(scale); + (newScale) => { + onScaleChangedProp(newScale); + setScale(newScale); // When a pdf is shown in a carousel, we want to disable the pager scroll when the pdf is zoomed in if (isUsedInCarousel && attachmentCarouselPagerContext) { - const isPdfZooming = scale === 1; + const isPdfZooming = newScale === 1; attachmentCarouselPagerContext.onScaleChanged(1); @@ -50,11 +52,11 @@ function BaseAttachmentViewPdf({ if (onPressProp !== undefined) { onPressProp(e); } - if (attachmentCarouselPagerContext !== null && attachmentCarouselPagerContext.onTap !== null) { + if (attachmentCarouselPagerContext !== null && attachmentCarouselPagerContext.onTap !== null && scale === 1) { attachmentCarouselPagerContext.onTap(e); } }, - [attachmentCarouselPagerContext, onPressProp], + [attachmentCarouselPagerContext, onPressProp, scale], ); return ( From fcee0f2e5c7a880687a2e4c0853e0eb52d97adaa Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Thu, 18 Jan 2024 11:03:34 +0100 Subject: [PATCH 284/580] fix: callback prop --- src/components/MultiGestureCanvas/index.tsx | 11 ++++++----- src/components/MultiGestureCanvas/types.ts | 2 +- src/components/MultiGestureCanvas/useTapGestures.ts | 4 +--- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/src/components/MultiGestureCanvas/index.tsx b/src/components/MultiGestureCanvas/index.tsx index 3bbc201f255f..55a683a1278c 100644 --- a/src/components/MultiGestureCanvas/index.tsx +++ b/src/components/MultiGestureCanvas/index.tsx @@ -123,11 +123,7 @@ function MultiGestureCanvas({ /** * Resets the canvas to the initial state and animates back smoothly */ - const reset = useWorkletCallback((animated: boolean, callbackProp?: () => void) => { - const callback = callbackProp ?? (() => {}); - - pinchScale.value = 1; - + const reset = useWorkletCallback((animated: boolean, callback?: () => void) => { stopAnimation(); pinchScale.value = 1; @@ -140,6 +136,7 @@ function MultiGestureCanvas({ pinchTranslateX.value = withSpring(0, SPRING_CONFIG); pinchTranslateY.value = withSpring(0, SPRING_CONFIG); zoomScale.value = withSpring(1, SPRING_CONFIG, callback); + return; } @@ -151,6 +148,10 @@ function MultiGestureCanvas({ pinchTranslateY.value = 0; zoomScale.value = 1; + if (callback === undefined) { + return; + } + callback(); }); diff --git a/src/components/MultiGestureCanvas/types.ts b/src/components/MultiGestureCanvas/types.ts index ec1975c883e8..c10b1ab677a8 100644 --- a/src/components/MultiGestureCanvas/types.ts +++ b/src/components/MultiGestureCanvas/types.ts @@ -42,7 +42,7 @@ type MultiGestureCanvasVariables = { pinchTranslateX: SharedValue; pinchTranslateY: SharedValue; stopAnimation: () => void; - reset: (animated: boolean, callbackProp: () => void) => void; + reset: (animated: boolean, callback: () => void) => void; onTap: OnTapCallback | undefined; onScaleChanged?: OnScaleChangedCallback; }; diff --git a/src/components/MultiGestureCanvas/useTapGestures.ts b/src/components/MultiGestureCanvas/useTapGestures.ts index 137c2287e8cc..6eba3849d572 100644 --- a/src/components/MultiGestureCanvas/useTapGestures.ts +++ b/src/components/MultiGestureCanvas/useTapGestures.ts @@ -36,7 +36,7 @@ const useTapGestures = ({ const doubleTapScale = useMemo(() => Math.max(DOUBLE_TAP_SCALE, maxContentScale / minContentScale), [maxContentScale, minContentScale]); const zoomToCoordinates = useWorkletCallback( - (focalX: number, focalY: number, callbackProp: () => void) => { + (focalX: number, focalY: number, callback: () => void) => { 'worklet'; stopAnimation(); @@ -100,8 +100,6 @@ const useTapGestures = ({ offsetAfterZooming.y = 0; } - const callback = callbackProp || (() => {}); - offsetX.value = withSpring(offsetAfterZooming.x, SPRING_CONFIG); offsetY.value = withSpring(offsetAfterZooming.y, SPRING_CONFIG); zoomScale.value = withSpring(doubleTapScale, SPRING_CONFIG, callback); From 395e25ce96c1d23d74503bef95acb6c2d91f153f Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Thu, 18 Jan 2024 11:13:19 +0100 Subject: [PATCH 285/580] move style --- src/components/MultiGestureCanvas/index.tsx | 8 +------- src/styles/utils/index.ts | 8 ++++++++ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/components/MultiGestureCanvas/index.tsx b/src/components/MultiGestureCanvas/index.tsx index 55a683a1278c..625c58bdb7ec 100644 --- a/src/components/MultiGestureCanvas/index.tsx +++ b/src/components/MultiGestureCanvas/index.tsx @@ -239,13 +239,7 @@ function MultiGestureCanvas({ return ( ({ From 8164ff5a222a9a9e95f8a84a9e8513833d7f611c Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Thu, 18 Jan 2024 11:16:17 +0100 Subject: [PATCH 286/580] memoize style --- src/components/MultiGestureCanvas/index.tsx | 4 +++- src/styles/utils/index.ts | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/components/MultiGestureCanvas/index.tsx b/src/components/MultiGestureCanvas/index.tsx index 625c58bdb7ec..a53166c292b3 100644 --- a/src/components/MultiGestureCanvas/index.tsx +++ b/src/components/MultiGestureCanvas/index.tsx @@ -236,10 +236,12 @@ function MultiGestureCanvas({ }; }); + const containerStyle = useMemo(() => StyleUtils.getMultiGestureCanvasContainerStyle(canvasSize.width), [StyleUtils, canvasSize.width]); + return ( ({ From 5cba6a865656370f09598f38e310d5292e8df8c6 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Thu, 18 Jan 2024 11:17:36 +0100 Subject: [PATCH 287/580] fix: wrong condition --- .../AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.js b/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.js index e9ad8f5e529b..4c85b7d0a456 100644 --- a/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.js +++ b/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.js @@ -52,7 +52,7 @@ function BaseAttachmentViewPdf({ if (onPressProp !== undefined) { onPressProp(e); } - if (attachmentCarouselPagerContext !== null && attachmentCarouselPagerContext.onTap !== null && scale === 1) { + if (attachmentCarouselPagerContext !== undefined && attachmentCarouselPagerContext.onTap !== undefined && scale === 1) { attachmentCarouselPagerContext.onTap(e); } }, From e90de14bd3c3e4d730ad0c5ffa68421fbdfffcfb Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Thu, 18 Jan 2024 11:36:08 +0100 Subject: [PATCH 288/580] fix: arrows not shown after pdf is unzoomed --- .../Pager/AttachmentCarouselPagerContext.ts | 2 +- .../AttachmentCarousel/Pager/index.tsx | 10 +++++----- .../AttachmentCarousel/index.native.js | 10 +++++++++- .../AttachmentViewPdf/BaseAttachmentViewPdf.js | 15 +++------------ .../AttachmentViewPdf/index.android.js | 2 +- 5 files changed, 19 insertions(+), 20 deletions(-) diff --git a/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts b/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts index 481c11ee0397..b73f30e7114c 100644 --- a/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts +++ b/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts @@ -6,7 +6,7 @@ import type {SharedValue} from 'react-native-reanimated'; type AttachmentCarouselPagerContextValue = { pagerRef: ForwardedRef; isPagerSwiping: SharedValue; - isPdfZooming: SharedValue; + scale: number; onTap: () => void; onScaleChanged: (scale: number) => void; }; diff --git a/src/components/Attachments/AttachmentCarousel/Pager/index.tsx b/src/components/Attachments/AttachmentCarousel/Pager/index.tsx index cf95382ccc30..cb6fa58d8f48 100644 --- a/src/components/Attachments/AttachmentCarousel/Pager/index.tsx +++ b/src/components/Attachments/AttachmentCarousel/Pager/index.tsx @@ -27,6 +27,7 @@ type PagerItem = { type AttachmentCarouselPagerProps = { items: PagerItem[]; + scale: number; scrollEnabled?: boolean; renderItem: (props: {item: PagerItem; index: number; isActive: boolean}) => React.ReactNode; initialIndex: number; @@ -36,13 +37,12 @@ type AttachmentCarouselPagerProps = { }; function AttachmentCarouselPager( - {items, scrollEnabled = true, renderItem, initialIndex, onTap, onPageSelected, onScaleChanged}: AttachmentCarouselPagerProps, + {items, scale, scrollEnabled = true, renderItem, initialIndex, onTap, onPageSelected, onScaleChanged}: AttachmentCarouselPagerProps, ref: ForwardedRef, ) { const styles = useThemeStyles(); const pagerRef = useRef(null); - const isPdfZooming = useSharedValue(false); const isPagerSwiping = useSharedValue(false); const activePage = useSharedValue(initialIndex); const [activePageState, setActivePageState] = useState(initialIndex); @@ -68,11 +68,11 @@ function AttachmentCarouselPager( () => ({ pagerRef, isPagerSwiping, - isPdfZooming, + scale, onTap, onScaleChanged, }), - [isPagerSwiping, isPdfZooming, onTap, onScaleChanged], + [isPagerSwiping, scale, onTap, onScaleChanged], ); useImperativeHandle( @@ -90,7 +90,7 @@ function AttachmentCarouselPager( { + if (newScale === scale) { + return; + } + + setScale(newScale); + const newIsZoomedOut = newScale === 1; if (isZoomedOut === newIsZoomedOut) { @@ -117,7 +124,7 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source, setIsZoomedOut(newIsZoomedOut); setShouldShowArrows(newIsZoomedOut); }, - [isZoomedOut, setShouldShowArrows], + [isZoomedOut, scale, setShouldShowArrows], ); return ( @@ -147,6 +154,7 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source, { if (!attachmentCarouselPagerContext) { @@ -29,19 +29,10 @@ function BaseAttachmentViewPdf({ const onScaleChanged = useCallback( (newScale) => { onScaleChangedProp(newScale); - setScale(newScale); // When a pdf is shown in a carousel, we want to disable the pager scroll when the pdf is zoomed in if (isUsedInCarousel && attachmentCarouselPagerContext) { - const isPdfZooming = newScale === 1; - - attachmentCarouselPagerContext.onScaleChanged(1); - - if (attachmentCarouselPagerContext.isPdfZooming.value === isPdfZooming) { - return; - } - - attachmentCarouselPagerContext.isPdfZooming.value = isPdfZooming; + attachmentCarouselPagerContext.onScaleChanged(newScale); } }, [attachmentCarouselPagerContext, isUsedInCarousel, onScaleChangedProp], diff --git a/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.android.js b/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.android.js index 9c83bc7fc54d..53e5c1ff9ddd 100644 --- a/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.android.js +++ b/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.android.js @@ -22,7 +22,7 @@ function AttachmentViewPdf(props) { // frozen, which combined with Reanimated using strict mode since 3.6.0 was resulting in errors. // Without strict mode, it would just silently fail. // See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/freeze#description - const isPdfZooming = attachmentCarouselPagerContext !== null ? attachmentCarouselPagerContext.shouldPagerScroll : undefined; + const isPdfZooming = attachmentCarouselPagerContext !== null ? attachmentCarouselPagerContext.scale !== 1 : undefined; const Pan = Gesture.Pan() .manualActivation(true) From e744f155c905e37f808065b5a4f7b324b2b79fd0 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Thu, 18 Jan 2024 12:15:44 +0100 Subject: [PATCH 289/580] 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 290/580] 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 291/580] 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 292/580] 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 773b4dcdc0992590d78231c75033b4886a22ae19 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Thu, 18 Jan 2024 14:13:00 +0100 Subject: [PATCH 293/580] improve style memo --- src/components/MultiGestureCanvas/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/MultiGestureCanvas/index.tsx b/src/components/MultiGestureCanvas/index.tsx index a53166c292b3..d8452becf01b 100644 --- a/src/components/MultiGestureCanvas/index.tsx +++ b/src/components/MultiGestureCanvas/index.tsx @@ -236,12 +236,12 @@ function MultiGestureCanvas({ }; }); - const containerStyle = useMemo(() => StyleUtils.getMultiGestureCanvasContainerStyle(canvasSize.width), [StyleUtils, canvasSize.width]); + const containerStyles = useMemo(() => [styles.flex1, StyleUtils.getMultiGestureCanvasContainerStyle(canvasSize.width)], [StyleUtils, canvasSize.width, styles.flex1]); return ( Date: Thu, 18 Jan 2024 14:47:04 +0100 Subject: [PATCH 294/580] 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 295/580] 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 296/580] 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 297/580] 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 298/580] 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 299/580] 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 300/580] 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 301/580] 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 302/580] 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 303/580] 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 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 304/580] 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 10dd583f74e84527bb59d7e844f8568fd9617a11 Mon Sep 17 00:00:00 2001 From: someone-here Date: Fri, 19 Jan 2024 00:24:00 +0530 Subject: [PATCH 305/580] Fix Context menu from keyboard --- src/libs/calculateAnchorPosition.ts | 6 +-- .../PopoverReportActionContextMenu.tsx | 41 +++++++++++-------- 2 files changed, 28 insertions(+), 19 deletions(-) diff --git a/src/libs/calculateAnchorPosition.ts b/src/libs/calculateAnchorPosition.ts index 66966b7b504c..2ff0c178e6e0 100644 --- a/src/libs/calculateAnchorPosition.ts +++ b/src/libs/calculateAnchorPosition.ts @@ -1,5 +1,5 @@ -/* eslint-disable no-console */ -import type {View} from 'react-native'; +/* eslint-disable no-restricted-imports */ +import type {Text as RNText, View} from 'react-native'; import type {ValueOf} from 'type-fest'; import CONST from '@src/CONST'; import type {AnchorPosition} from '@src/styles'; @@ -13,7 +13,7 @@ type AnchorOrigin = { /** * Gets the x,y position of the passed in component for the purpose of anchoring another component to it. */ -export default function calculateAnchorPosition(anchorComponent: View, anchorOrigin?: AnchorOrigin): Promise { +export default function calculateAnchorPosition(anchorComponent: View | RNText, anchorOrigin?: AnchorOrigin): Promise { return new Promise((resolve) => { if (!anchorComponent) { return resolve({horizontal: 0, vertical: 0}); diff --git a/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx b/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx index 46b783bca3f9..476efa591177 100644 --- a/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx +++ b/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx @@ -1,11 +1,14 @@ import type {ForwardedRef} from 'react'; import React, {forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState} from 'react'; -import type {EmitterSubscription, GestureResponderEvent, NativeTouchEvent, View} from 'react-native'; + +/* eslint-disable no-restricted-imports */ +import type {EmitterSubscription, GestureResponderEvent, NativeTouchEvent, Text as RNText, View} from 'react-native'; import {Dimensions} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import ConfirmModal from '@components/ConfirmModal'; import PopoverWithMeasuredContent from '@components/PopoverWithMeasuredContent'; import useLocalize from '@hooks/useLocalize'; +import calculateAnchorPosition from '@libs/calculateAnchorPosition'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import * as IOU from '@userActions/IOU'; import * as Report from '@userActions/Report'; @@ -14,10 +17,6 @@ import type {ReportAction} from '@src/types/onyx'; import BaseReportActionContextMenu from './BaseReportActionContextMenu'; import type {ContextMenuType, ReportActionContextMenu} from './ReportActionContextMenu'; -type ContextMenuAnchorCallback = (x: number, y: number) => void; - -type ContextMenuAnchor = {measureInWindow: (callback: ContextMenuAnchorCallback) => void}; - type Location = { x: number; y: number; @@ -66,7 +65,7 @@ function PopoverReportActionContextMenu(_props: never, ref: ForwardedRef(null); const anchorRef = useRef(null); const dimensionsEventListener = useRef(null); - const contextMenuAnchorRef = useRef(null); + const contextMenuAnchorRef = useRef(null); const contextMenuTargetNode = useRef(null); const onPopoverShow = useRef(() => {}); @@ -171,16 +170,26 @@ function PopoverReportActionContextMenu(_props: never, ref: ForwardedRef { - popoverAnchorPosition.current = { - horizontal: pageX - x, - vertical: pageY - y, - }; - - popoverAnchorPosition.current = { - horizontal: pageX, - vertical: pageY, - }; + new Promise((resolve) => { + if (!pageX && !pageY && contextMenuAnchorRef.current) { + calculateAnchorPosition(contextMenuAnchorRef.current).then((position) => { + popoverAnchorPosition.current = position; + resolve(); + }); + } else { + getContextMenuMeasuredLocation().then(({x, y}) => { + cursorRelativePosition.current = { + horizontal: pageX - x, + vertical: pageY - y, + }; + popoverAnchorPosition.current = { + horizontal: pageX, + vertical: pageY, + }; + resolve(); + }); + } + }).then(() => { typeRef.current = type; reportIDRef.current = reportID ?? '0'; reportActionIDRef.current = reportActionID ?? '0'; From b48c0b6566aee7df501900718f1a7aa1a64dc580 Mon Sep 17 00:00:00 2001 From: Andrew Rosiclair Date: Thu, 18 Jan 2024 14:42:00 -0500 Subject: [PATCH 306/580] remove project and marketing version settings in the NSE target --- ios/NewExpensify.xcodeproj/project.pbxproj | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/ios/NewExpensify.xcodeproj/project.pbxproj b/ios/NewExpensify.xcodeproj/project.pbxproj index acd08500fc11..d56a99739f98 100644 --- a/ios/NewExpensify.xcodeproj/project.pbxproj +++ b/ios/NewExpensify.xcodeproj/project.pbxproj @@ -1043,7 +1043,6 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 368M544MTT; @@ -1075,7 +1074,6 @@ "@executable_path/../../Frameworks", ); LOCALIZATION_PREFERS_STRING_CATALOGS = YES; - MARKETING_VERSION = 1.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -1130,7 +1128,6 @@ CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 368M544MTT; @@ -1162,7 +1159,6 @@ "@executable_path/../../Frameworks", ); LOCALIZATION_PREFERS_STRING_CATALOGS = YES; - MARKETING_VERSION = 1.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -1218,7 +1214,6 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 368M544MTT; @@ -1250,7 +1245,6 @@ "@executable_path/../../Frameworks", ); LOCALIZATION_PREFERS_STRING_CATALOGS = YES; - MARKETING_VERSION = 1.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -1306,7 +1300,6 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 368M544MTT; @@ -1332,7 +1325,6 @@ "@executable_path/../../Frameworks", ); LOCALIZATION_PREFERS_STRING_CATALOGS = YES; - MARKETING_VERSION = 1.0; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; @@ -1386,7 +1378,6 @@ CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 368M544MTT; @@ -1412,7 +1403,6 @@ "@executable_path/../../Frameworks", ); LOCALIZATION_PREFERS_STRING_CATALOGS = YES; - MARKETING_VERSION = 1.0; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; @@ -1467,7 +1457,6 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 368M544MTT; @@ -1493,7 +1482,6 @@ "@executable_path/../../Frameworks", ); LOCALIZATION_PREFERS_STRING_CATALOGS = YES; - MARKETING_VERSION = 1.0; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; From 0d6384d50abe777aac837a89343c35437671abc1 Mon Sep 17 00:00:00 2001 From: hurali97 Date: Fri, 19 Jan 2024 08:42:37 +0500 Subject: [PATCH 307/580] 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 05a4c19ae1f2b942b602efdfc3f1e8afb8d0fa73 Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Fri, 19 Jan 2024 11:18:08 +0700 Subject: [PATCH 308/580] fixlogic clear status and save status --- .../EmojiPicker/EmojiPickerButtonDropdown.js | 7 ++++++- src/pages/settings/Profile/CustomStatus/StatusPage.js | 10 ++++++---- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/components/EmojiPicker/EmojiPickerButtonDropdown.js b/src/components/EmojiPicker/EmojiPickerButtonDropdown.js index bfcb66aeefbb..7f60b0615785 100644 --- a/src/components/EmojiPicker/EmojiPickerButtonDropdown.js +++ b/src/components/EmojiPicker/EmojiPickerButtonDropdown.js @@ -60,7 +60,12 @@ function EmojiPickerButtonDropdown(props) { style={styles.emojiPickerButtonDropdownIcon} numberOfLines={1} > - {props.value} + {props.value || ( + + )} { @@ -79,10 +79,9 @@ function StatusPage({draftStatus, currentUserPersonalDetails}) { setBrickRoadIndicator(isValidClearAfterDate() ? null : CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR); return; } - User.updateCustomStatus({ text: statusText, - emojiCode, + emojiCode: emojiCode || initialEmoji, clearAfter: clearAfterTime !== CONST.CUSTOM_STATUS_TYPES.NEVER ? clearAfterTime : '', }); @@ -101,7 +100,10 @@ function StatusPage({draftStatus, currentUserPersonalDetails}) { emojiCode: '', clearAfter: DateUtils.getEndOfToday(), }); - formRef.current.resetForm({[INPUT_IDS.EMOJI_CODE]: initialEmoji}); + formRef.current.resetForm({[INPUT_IDS.EMOJI_CODE]: ''}); + InteractionManager.runAfterInteractions(() => { + navigateBackToPreviousScreen(); + }); }; useEffect(() => setBrickRoadIndicator(isValidClearAfterDate() ? null : CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR), [isValidClearAfterDate]); From d3998d4b0c7a5cf611eaa6c57a20f27657867813 Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Fri, 19 Jan 2024 12:06:23 +0700 Subject: [PATCH 309/580] format created of transaction with date time value if the created date is the current date --- src/libs/actions/IOU.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index 7ee752a1f0ef..3d361b9b3130 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -870,6 +870,8 @@ function createDistanceRequest(report, participant, comment, created, category, // If the report is an iou or expense report, we should get the linked chat report to be passed to the getMoneyRequestInformation function const isMoneyRequestReport = ReportUtils.isMoneyRequestReport(report); const currentChatReport = isMoneyRequestReport ? ReportUtils.getReport(report.chatReportID) : report; + const currentTime = DateUtils.getDBTime(); + const currentCreated = created === format(new Date(currentTime), CONST.DATE.FNS_FORMAT_STRING) ? currentTime : created; const optimisticReceipt = { source: ReceiptGeneric, @@ -881,7 +883,7 @@ function createDistanceRequest(report, participant, comment, created, category, comment, amount, currency, - created, + currentCreated, merchant, userAccountID, currentUserEmail, @@ -906,7 +908,7 @@ function createDistanceRequest(report, participant, comment, created, category, createdIOUReportActionID, reportPreviewReportActionID: reportPreviewAction.reportActionID, waypoints: JSON.stringify(validWaypoints), - created, + created: currentCreated, category, tag, billable, @@ -1260,6 +1262,8 @@ function requestMoney( // If the report is iou or expense report, we should get the linked chat report to be passed to the getMoneyRequestInformation function const isMoneyRequestReport = ReportUtils.isMoneyRequestReport(report); const currentChatReport = isMoneyRequestReport ? ReportUtils.getReport(report.chatReportID) : report; + const currentTime = DateUtils.getDBTime(); + const currentCreated = created === format(new Date(currentTime), CONST.DATE.FNS_FORMAT_STRING) ? currentTime : created; const {payerAccountID, payerEmail, iouReport, chatReport, transaction, iouAction, createdChatReportActionID, createdIOUReportActionID, reportPreviewAction, onyxData} = getMoneyRequestInformation( currentChatReport, @@ -1267,7 +1271,7 @@ function requestMoney( comment, amount, currency, - created, + currentCreated, merchant, payeeAccountID, payeeEmail, @@ -1290,7 +1294,7 @@ function requestMoney( amount, currency, comment, - created, + created: currentCreated, merchant, iouReportID: iouReport.reportID, chatReportID: chatReport.reportID, From 2ccf45fb79d55efb4a1c6a23796e2a093494a66a Mon Sep 17 00:00:00 2001 From: VickyStash Date: Fri, 19 Jan 2024 09:17:44 +0100 Subject: [PATCH 310/580] Update TransactionViolations type --- src/components/ReportActionItem/ReportPreview.tsx | 5 ++++- src/types/onyx/TransactionViolation.ts | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/components/ReportActionItem/ReportPreview.tsx b/src/components/ReportActionItem/ReportPreview.tsx index 15d9ce2dceba..9c2075dd927f 100644 --- a/src/components/ReportActionItem/ReportPreview.tsx +++ b/src/components/ReportActionItem/ReportPreview.tsx @@ -28,7 +28,7 @@ import * as IOU from '@userActions/IOU'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import type {Policy, Report, ReportAction, Session, Transaction} from '@src/types/onyx'; +import type {Policy, Report, ReportAction, Session, Transaction, TransactionViolations} from '@src/types/onyx'; import type {PaymentMethodType} from '@src/types/onyx/OriginalMessage'; import ReportActionItemImages from './ReportActionItemImages'; @@ -49,6 +49,9 @@ type ReportPreviewOnyxProps = { /** All the transactions, used to update ReportPreview label and status */ transactions: OnyxCollection; + + /** All of the transaction violations */ + transactionViolations: OnyxCollection; }; type ReportPreviewProps = ReportPreviewOnyxProps & { diff --git a/src/types/onyx/TransactionViolation.ts b/src/types/onyx/TransactionViolation.ts index 20603adc7cfa..e7be24d5cb18 100644 --- a/src/types/onyx/TransactionViolation.ts +++ b/src/types/onyx/TransactionViolation.ts @@ -31,7 +31,7 @@ type TransactionViolation = { }; }; -type TransactionViolations = Record; +type TransactionViolations = TransactionViolation[]; export type {TransactionViolation, ViolationName}; export default TransactionViolations; From 72b4fb9dd3da36f213fd573817b6f7703b65c076 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Fri, 19 Jan 2024 16:30:32 +0800 Subject: [PATCH 311/580] ignore width if 0 --- src/components/TextInput/BaseTextInput/index.native.tsx | 3 +++ src/components/TextInput/BaseTextInput/index.tsx | 3 +++ 2 files changed, 6 insertions(+) diff --git a/src/components/TextInput/BaseTextInput/index.native.tsx b/src/components/TextInput/BaseTextInput/index.native.tsx index 99b3e98588ac..9ff4e39e6bd1 100644 --- a/src/components/TextInput/BaseTextInput/index.native.tsx +++ b/src/components/TextInput/BaseTextInput/index.native.tsx @@ -425,6 +425,9 @@ function BaseTextInput( styles.visibilityHidden, ]} onLayout={(e) => { + if (e.nativeEvent.layout.width === 0) { + return; + } setTextInputWidth(e.nativeEvent.layout.width); setTextInputHeight(e.nativeEvent.layout.height); }} diff --git a/src/components/TextInput/BaseTextInput/index.tsx b/src/components/TextInput/BaseTextInput/index.tsx index 9c3899979aaa..ccd8b7f94d9d 100644 --- a/src/components/TextInput/BaseTextInput/index.tsx +++ b/src/components/TextInput/BaseTextInput/index.tsx @@ -445,6 +445,9 @@ function BaseTextInput( styles.visibilityHidden, ]} onLayout={(e) => { + if (e.nativeEvent.layout.width === 0) { + return; + } let additionalWidth = 0; if (Browser.isMobileSafari() || Browser.isSafari()) { additionalWidth = 2; From 5022bcf59911000af6a554f756669a87620abc77 Mon Sep 17 00:00:00 2001 From: s-alves10 Date: Fri, 19 Jan 2024 02:57:47 -0600 Subject: [PATCH 312/580] fix: check visibility as well to show another login message --- src/pages/signin/SignInPage.js | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/pages/signin/SignInPage.js b/src/pages/signin/SignInPage.js index 9d5b51d667ff..f40f4b17c666 100644 --- a/src/pages/signin/SignInPage.js +++ b/src/pages/signin/SignInPage.js @@ -18,6 +18,7 @@ import * as Localize from '@libs/Localize'; import Log from '@libs/Log'; import Navigation from '@libs/Navigation/Navigation'; import Performance from '@libs/Performance'; +import Visibility from '@libs/Visibility'; import * as App from '@userActions/App'; import * as Session from '@userActions/Session'; import CONST from '@src/CONST'; @@ -96,7 +97,7 @@ const defaultProps = { * @param {Boolean} hasEmailDeliveryFailure * @returns {Object} */ -function getRenderOptions({hasLogin, hasValidateCode, account, isPrimaryLogin, isUsingMagicCode, hasInitiatedSAMLLogin, isClientTheLeader}) { +function getRenderOptions({hasLogin, hasValidateCode, account, isPrimaryLogin, isUsingMagicCode, hasInitiatedSAMLLogin, shouldShowAnotherLogin}) { const hasAccount = !_.isEmpty(account); const isSAMLEnabled = Boolean(account.isSAMLEnabled); const isSAMLRequired = Boolean(account.isSAMLRequired); @@ -113,13 +114,13 @@ function getRenderOptions({hasLogin, hasValidateCode, account, isPrimaryLogin, i Session.clearSignInData(); } - const shouldShowLoginForm = isClientTheLeader && !hasLogin && !hasValidateCode; + const shouldShowLoginForm = !shouldShowAnotherLogin && !hasLogin && !hasValidateCode; const shouldShowEmailDeliveryFailurePage = hasLogin && hasEmailDeliveryFailure && !shouldShowChooseSSOOrMagicCode && !shouldInitiateSAMLLogin; const isUnvalidatedSecondaryLogin = hasLogin && !isPrimaryLogin && !account.validated && !hasEmailDeliveryFailure; const shouldShowValidateCodeForm = hasAccount && (hasLogin || hasValidateCode) && !isUnvalidatedSecondaryLogin && !hasEmailDeliveryFailure && !shouldShowChooseSSOOrMagicCode && !isSAMLRequired; const shouldShowWelcomeHeader = shouldShowLoginForm || shouldShowValidateCodeForm || shouldShowChooseSSOOrMagicCode || isUnvalidatedSecondaryLogin; - const shouldShowWelcomeText = shouldShowLoginForm || shouldShowValidateCodeForm || shouldShowChooseSSOOrMagicCode || !isClientTheLeader; + const shouldShowWelcomeText = shouldShowLoginForm || shouldShowValidateCodeForm || shouldShowChooseSSOOrMagicCode || shouldShowAnotherLogin; return { shouldShowLoginForm, shouldShowEmailDeliveryFailurePage, @@ -154,6 +155,8 @@ function SignInPageInner({credentials, account, isInModal, activeClients, prefer const [hasInitiatedSAMLLogin, setHasInitiatedSAMLLogin] = useState(false); const isClientTheLeader = activeClients && ActiveClientManager.isClientTheLeader(); + // eslint-disable-next-line rulesdir/no-negated-variables + const shouldShowAnotherLogin = Visibility.isVisible() && !isClientTheLeader; useEffect(() => Performance.measureTTI(), []); useEffect(() => { @@ -192,7 +195,7 @@ function SignInPageInner({credentials, account, isInModal, activeClients, prefer isPrimaryLogin: !account.primaryLogin || account.primaryLogin === credentials.login, isUsingMagicCode, hasInitiatedSAMLLogin, - isClientTheLeader, + shouldShowAnotherLogin, }); if (shouldInitiateSAMLLogin) { @@ -204,7 +207,7 @@ function SignInPageInner({credentials, account, isInModal, activeClients, prefer let welcomeText = ''; const headerText = translate('login.hero.header'); - if (!isClientTheLeader) { + if (shouldShowAnotherLogin) { welcomeHeader = translate('welcomeText.anotherLoginPageIsOpen'); welcomeText = translate('welcomeText.anotherLoginPageIsOpenExplanation'); } else if (shouldShowLoginForm) { @@ -270,7 +273,7 @@ function SignInPageInner({credentials, account, isInModal, activeClients, prefer blurOnSubmit={account.validated === false} scrollPageToTop={signInPageLayoutRef.current && signInPageLayoutRef.current.scrollPageToTop} /> - {isClientTheLeader && ( + {!shouldShowAnotherLogin && ( <> {shouldShowValidateCodeForm && ( Date: Fri, 19 Jan 2024 14:49:50 +0530 Subject: [PATCH 313/580] feat: updated placement of the user icon --- .../home/sidebar/AvatarWithOptionalStatus.js | 6 ++--- src/styles/index.ts | 23 +++++++++++-------- .../utils/statusEmojiStyles/index.desktop.ts | 7 ++++++ src/styles/utils/statusEmojiStyles/index.ts | 5 ++++ .../utils/statusEmojiStyles/index.website.ts | 10 ++++++++ src/styles/utils/statusEmojiStyles/types.ts | 5 ++++ 6 files changed, 44 insertions(+), 12 deletions(-) create mode 100644 src/styles/utils/statusEmojiStyles/index.desktop.ts create mode 100644 src/styles/utils/statusEmojiStyles/index.ts create mode 100644 src/styles/utils/statusEmojiStyles/index.website.ts create mode 100644 src/styles/utils/statusEmojiStyles/types.ts diff --git a/src/pages/home/sidebar/AvatarWithOptionalStatus.js b/src/pages/home/sidebar/AvatarWithOptionalStatus.js index 0bad9e845e9c..9c31e1250c39 100644 --- a/src/pages/home/sidebar/AvatarWithOptionalStatus.js +++ b/src/pages/home/sidebar/AvatarWithOptionalStatus.js @@ -41,14 +41,15 @@ function AvatarWithOptionalStatus({emojiStatus, isCreateMenuOpen}) { return ( + - + - ); } diff --git a/src/styles/index.ts b/src/styles/index.ts index aace13c34594..0a4f0e6dfa8a 100644 --- a/src/styles/index.ts +++ b/src/styles/index.ts @@ -32,6 +32,7 @@ import pointerEventsNone from './utils/pointerEventsNone'; import positioning from './utils/positioning'; import sizing from './utils/sizing'; import spacing from './utils/spacing'; +import statusEmojiStyles from './utils/statusEmojiStyles'; import textDecorationLine from './utils/textDecorationLine'; import textUnderline from './utils/textUnderline'; import userSelect from './utils/userSelect'; @@ -3918,24 +3919,28 @@ const styles = (theme: ThemeColors) => emojiStatusLHN: { fontSize: 22, }, + + statusEmojiStyles, + sidebarStatusAvatarContainer: { height: 44, - width: 84, + width: 44, backgroundColor: theme.componentBG, - flexDirection: 'row', alignItems: 'center', - justifyContent: 'space-between', - borderRadius: 42, - paddingHorizontal: 2, - marginVertical: -2, - marginRight: -2, + justifyContent: 'center', + borderRadius: 22, }, sidebarStatusAvatar: { - flex: 1, alignItems: 'center', justifyContent: 'center', + backgroundColor: theme.componentBG, + height: 30, + width: 30, + borderRadius: 15, + position: 'absolute', + right: -7, + bottom: -7, }, - moneyRequestViewImage: { ...spacing.mh5, ...spacing.mv3, diff --git a/src/styles/utils/statusEmojiStyles/index.desktop.ts b/src/styles/utils/statusEmojiStyles/index.desktop.ts new file mode 100644 index 000000000000..88b33768abc2 --- /dev/null +++ b/src/styles/utils/statusEmojiStyles/index.desktop.ts @@ -0,0 +1,7 @@ +import type StatusEmojiStyles from './types'; + +const statusEmojiStyles: StatusEmojiStyles = { + marginTop: 2, +}; + +export default statusEmojiStyles; diff --git a/src/styles/utils/statusEmojiStyles/index.ts b/src/styles/utils/statusEmojiStyles/index.ts new file mode 100644 index 000000000000..6761d04a4825 --- /dev/null +++ b/src/styles/utils/statusEmojiStyles/index.ts @@ -0,0 +1,5 @@ +import type StatusEmojiStyles from './types'; + +const statusEmojiStyles: StatusEmojiStyles = {}; + +export default statusEmojiStyles; diff --git a/src/styles/utils/statusEmojiStyles/index.website.ts b/src/styles/utils/statusEmojiStyles/index.website.ts new file mode 100644 index 000000000000..8cd724504314 --- /dev/null +++ b/src/styles/utils/statusEmojiStyles/index.website.ts @@ -0,0 +1,10 @@ +import * as Browser from '@libs/Browser'; +import type StatusEmojiStyles from './types'; + +const statusEmojiStyles: StatusEmojiStyles = Browser.isMobile() + ? {} + : { + marginTop: 2, + }; + +export default statusEmojiStyles; diff --git a/src/styles/utils/statusEmojiStyles/types.ts b/src/styles/utils/statusEmojiStyles/types.ts new file mode 100644 index 000000000000..7ed2ce2b20f1 --- /dev/null +++ b/src/styles/utils/statusEmojiStyles/types.ts @@ -0,0 +1,5 @@ +import type {ViewStyle} from 'react-native'; + +type StatusEmojiStyles = Pick; + +export default StatusEmojiStyles; From 030d802b97db94af5c51e4865155aa9a1cad8dd2 Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Fri, 19 Jan 2024 17:01:03 +0700 Subject: [PATCH 314/580] implement util function for get current date of money request --- src/libs/DateUtils.ts | 10 ++++++++++ src/libs/actions/IOU.js | 6 ++---- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/libs/DateUtils.ts b/src/libs/DateUtils.ts index 1a10eb03a00e..d2b7ec4366a7 100644 --- a/src/libs/DateUtils.ts +++ b/src/libs/DateUtils.ts @@ -730,6 +730,15 @@ function formatToSupportedTimezone(timezoneInput: Timezone): Timezone { }; } +/** + * Return the date with full format if the created date is the current date. + * Otherwise return the created date. + */ +function enrichMoneyRequestTimestamp(created: string) { + const currentTime = getDBTime(); + return isSameDay(new Date(created), new Date(currentTime)) ? currentTime : created; +} + const DateUtils = { formatToDayOfWeek, formatToLongDateWithWeekday, @@ -774,6 +783,7 @@ const DateUtils = { getWeekEndsOn, isTimeAtLeastOneMinuteInFuture, formatToSupportedTimezone, + enrichMoneyRequestTimestamp, }; export default DateUtils; diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index 3d361b9b3130..1a1d8a415977 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -870,8 +870,7 @@ function createDistanceRequest(report, participant, comment, created, category, // If the report is an iou or expense report, we should get the linked chat report to be passed to the getMoneyRequestInformation function const isMoneyRequestReport = ReportUtils.isMoneyRequestReport(report); const currentChatReport = isMoneyRequestReport ? ReportUtils.getReport(report.chatReportID) : report; - const currentTime = DateUtils.getDBTime(); - const currentCreated = created === format(new Date(currentTime), CONST.DATE.FNS_FORMAT_STRING) ? currentTime : created; + const currentCreated = DateUtils.enrichMoneyRequestTimestamp(created); const optimisticReceipt = { source: ReceiptGeneric, @@ -1262,8 +1261,7 @@ function requestMoney( // If the report is iou or expense report, we should get the linked chat report to be passed to the getMoneyRequestInformation function const isMoneyRequestReport = ReportUtils.isMoneyRequestReport(report); const currentChatReport = isMoneyRequestReport ? ReportUtils.getReport(report.chatReportID) : report; - const currentTime = DateUtils.getDBTime(); - const currentCreated = created === format(new Date(currentTime), CONST.DATE.FNS_FORMAT_STRING) ? currentTime : created; + const currentCreated = DateUtils.enrichMoneyRequestTimestamp(created); const {payerAccountID, payerEmail, iouReport, chatReport, transaction, iouAction, createdChatReportActionID, createdIOUReportActionID, reportPreviewAction, onyxData} = getMoneyRequestInformation( currentChatReport, From b61949d499d78f548a1d81921621fe265f17b6ae Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Fri, 19 Jan 2024 11:37:35 +0100 Subject: [PATCH 315/580] fix: arrows in pdf delayed --- .../Pager/AttachmentCarouselPagerContext.ts | 2 +- .../Attachments/AttachmentCarousel/Pager/index.tsx | 7 +++---- .../Attachments/AttachmentCarousel/index.native.js | 9 ++++----- .../AttachmentViewPdf/BaseAttachmentViewPdf.js | 6 +++--- 4 files changed, 11 insertions(+), 13 deletions(-) diff --git a/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts b/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts index b73f30e7114c..b2aaea942073 100644 --- a/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts +++ b/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts @@ -6,7 +6,7 @@ import type {SharedValue} from 'react-native-reanimated'; type AttachmentCarouselPagerContextValue = { pagerRef: ForwardedRef; isPagerSwiping: SharedValue; - scale: number; + scrollEnabled: boolean; onTap: () => void; onScaleChanged: (scale: number) => void; }; diff --git a/src/components/Attachments/AttachmentCarousel/Pager/index.tsx b/src/components/Attachments/AttachmentCarousel/Pager/index.tsx index cb6fa58d8f48..f4b6d9e918da 100644 --- a/src/components/Attachments/AttachmentCarousel/Pager/index.tsx +++ b/src/components/Attachments/AttachmentCarousel/Pager/index.tsx @@ -27,7 +27,6 @@ type PagerItem = { type AttachmentCarouselPagerProps = { items: PagerItem[]; - scale: number; scrollEnabled?: boolean; renderItem: (props: {item: PagerItem; index: number; isActive: boolean}) => React.ReactNode; initialIndex: number; @@ -37,7 +36,7 @@ type AttachmentCarouselPagerProps = { }; function AttachmentCarouselPager( - {items, scale, scrollEnabled = true, renderItem, initialIndex, onTap, onPageSelected, onScaleChanged}: AttachmentCarouselPagerProps, + {items, scrollEnabled = true, renderItem, initialIndex, onTap, onPageSelected, onScaleChanged}: AttachmentCarouselPagerProps, ref: ForwardedRef, ) { const styles = useThemeStyles(); @@ -68,11 +67,11 @@ function AttachmentCarouselPager( () => ({ pagerRef, isPagerSwiping, - scale, + scrollEnabled, onTap, onScaleChanged, }), - [isPagerSwiping, scale, onTap, onScaleChanged], + [isPagerSwiping, scrollEnabled, onTap, onScaleChanged], ); useImperativeHandle( diff --git a/src/components/Attachments/AttachmentCarousel/index.native.js b/src/components/Attachments/AttachmentCarousel/index.native.js index 1bf03457722e..ac402130beae 100644 --- a/src/components/Attachments/AttachmentCarousel/index.native.js +++ b/src/components/Attachments/AttachmentCarousel/index.native.js @@ -23,7 +23,7 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source, const pagerRef = useRef(null); const [page, setPage] = useState(); const [attachments, setAttachments] = useState([]); - const [scale, setScale] = useState(1); + const scale = useRef(1); const [isZoomedOut, setIsZoomedOut] = useState(true); const [shouldShowArrows, setShouldShowArrows, autoHideArrows, cancelAutoHideArrows] = useCarouselArrows(); const [activeSource, setActiveSource] = useState(source); @@ -109,11 +109,11 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source, const handleScaleChange = useCallback( (newScale) => { - if (newScale === scale) { + if (newScale === scale.current) { return; } - setScale(newScale); + scale.current = newScale; const newIsZoomedOut = newScale === 1; @@ -124,7 +124,7 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source, setIsZoomedOut(newIsZoomedOut); setShouldShowArrows(newIsZoomedOut); }, - [isZoomedOut, scale, setShouldShowArrows], + [isZoomedOut, setShouldShowArrows], ); return ( @@ -154,7 +154,6 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source, { if (!attachmentCarouselPagerContext) { @@ -43,11 +43,11 @@ function BaseAttachmentViewPdf({ if (onPressProp !== undefined) { onPressProp(e); } - if (attachmentCarouselPagerContext !== undefined && attachmentCarouselPagerContext.onTap !== undefined && scale === 1) { + if (attachmentCarouselPagerContext !== undefined && attachmentCarouselPagerContext.onTap !== undefined && scrollEnabled) { attachmentCarouselPagerContext.onTap(e); } }, - [attachmentCarouselPagerContext, onPressProp, scale], + [attachmentCarouselPagerContext, scrollEnabled, onPressProp], ); return ( From 1e0bb542e5b0b2a4ed50f69e1f6887133b44c3f6 Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Fri, 19 Jan 2024 17:51:31 +0700 Subject: [PATCH 316/580] add return type explicitly --- src/libs/DateUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/DateUtils.ts b/src/libs/DateUtils.ts index d2b7ec4366a7..73051c3b759d 100644 --- a/src/libs/DateUtils.ts +++ b/src/libs/DateUtils.ts @@ -734,7 +734,7 @@ function formatToSupportedTimezone(timezoneInput: Timezone): Timezone { * Return the date with full format if the created date is the current date. * Otherwise return the created date. */ -function enrichMoneyRequestTimestamp(created: string) { +function enrichMoneyRequestTimestamp(created: string): string { const currentTime = getDBTime(); return isSameDay(new Date(created), new Date(currentTime)) ? currentTime : created; } From e4ff7b1c008afac833f845f7b43f0d111d8c25d9 Mon Sep 17 00:00:00 2001 From: s-alves10 Date: Fri, 19 Jan 2024 05:05:32 -0600 Subject: [PATCH 317/580] fix: check if the activePolicyID is a public workspace --- src/pages/workspace/WorkspaceNewRoomPage.js | 38 ++++++++++++++------- 1 file changed, 26 insertions(+), 12 deletions(-) diff --git a/src/pages/workspace/WorkspaceNewRoomPage.js b/src/pages/workspace/WorkspaceNewRoomPage.js index 35fab36e5d41..979aaaf23144 100644 --- a/src/pages/workspace/WorkspaceNewRoomPage.js +++ b/src/pages/workspace/WorkspaceNewRoomPage.js @@ -100,10 +100,25 @@ function WorkspaceNewRoomPage(props) { const {isOffline} = useNetwork(); const {isSmallScreenWidth} = useWindowDimensions(); const [visibility, setVisibility] = useState(CONST.REPORT.VISIBILITY.RESTRICTED); - const [policyID, setPolicyID] = useState(props.activePolicyID); const [writeCapability, setWriteCapability] = useState(CONST.REPORT.WRITE_CAPABILITIES.ALL); const wasLoading = usePrevious(props.formState.isLoading); const visibilityDescription = useMemo(() => translate(`newRoomPage.${visibility}Description`), [translate, visibility]); + + const workspaceOptions = useMemo( + () => + _.map(PolicyUtils.getActivePolicies(props.policies), (policy) => ({ + label: policy.name, + key: policy.id, + value: policy.id, + })), + [props.policies], + ); + const [policyID, setPolicyID] = useState(() => { + if (_.some(workspaceOptions, option => option.value === props.activePolicyID)) { + return props.activePolicyID; + } + return ''; + }); const isPolicyAdmin = useMemo(() => { if (!policyID) { return false; @@ -144,10 +159,17 @@ function WorkspaceNewRoomPage(props) { useEffect(() => { if (policyID) { + if (_.some(workspaceOptions, opt => opt.value === policyID)) { + setPolicyID(''); + } return; } - setPolicyID(props.activePolicyID); - }, [props.activePolicyID, policyID]); + if (_.some(workspaceOptions, opt => opt.value === props.activePolicyID)) { + setPolicyID(props.activePolicyID); + } else { + setPolicyID(''); + } + }, [props.activePolicyID, policyID, workspaceOptions]); useEffect(() => { if (!(((wasLoading && !props.formState.isLoading) || (isOffline && props.formState.isLoading)) && _.isEmpty(props.formState.errorFields))) { @@ -196,15 +218,7 @@ function WorkspaceNewRoomPage(props) { [props.reports], ); - const workspaceOptions = useMemo( - () => - _.map(PolicyUtils.getActivePolicies(props.policies), (policy) => ({ - label: policy.name, - key: policy.id, - value: policy.id, - })), - [props.policies], - ); + const writeCapabilityOptions = useMemo( () => From 30a44a800d261d2e9e976adf3f13b3fc4abfe854 Mon Sep 17 00:00:00 2001 From: s-alves10 Date: Fri, 19 Jan 2024 05:06:26 -0600 Subject: [PATCH 318/580] fix: prettier --- src/pages/workspace/WorkspaceNewRoomPage.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/pages/workspace/WorkspaceNewRoomPage.js b/src/pages/workspace/WorkspaceNewRoomPage.js index 979aaaf23144..aaaa71219c2a 100644 --- a/src/pages/workspace/WorkspaceNewRoomPage.js +++ b/src/pages/workspace/WorkspaceNewRoomPage.js @@ -114,7 +114,7 @@ function WorkspaceNewRoomPage(props) { [props.policies], ); const [policyID, setPolicyID] = useState(() => { - if (_.some(workspaceOptions, option => option.value === props.activePolicyID)) { + if (_.some(workspaceOptions, (option) => option.value === props.activePolicyID)) { return props.activePolicyID; } return ''; @@ -159,12 +159,12 @@ function WorkspaceNewRoomPage(props) { useEffect(() => { if (policyID) { - if (_.some(workspaceOptions, opt => opt.value === policyID)) { + if (_.some(workspaceOptions, (opt) => opt.value === policyID)) { setPolicyID(''); } return; } - if (_.some(workspaceOptions, opt => opt.value === props.activePolicyID)) { + if (_.some(workspaceOptions, (opt) => opt.value === props.activePolicyID)) { setPolicyID(props.activePolicyID); } else { setPolicyID(''); @@ -218,8 +218,6 @@ function WorkspaceNewRoomPage(props) { [props.reports], ); - - const writeCapabilityOptions = useMemo( () => _.map(CONST.REPORT.WRITE_CAPABILITIES, (value) => ({ From 8f68bc31f71b8627950f6f80cf8d793604e3d016 Mon Sep 17 00:00:00 2001 From: tienifr Date: Fri, 19 Jan 2024 18:28:23 +0700 Subject: [PATCH 319/580] 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 320/580] 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 1049b845ed5f6aa8da24925b3be4842307265132 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Fri, 19 Jan 2024 12:55:15 +0100 Subject: [PATCH 321/580] improve Lightbox on Android --- .../{Lightbox.tsx => Lightbox/index.tsx} | 33 +++++++++---------- .../numberOfConcurrentLightboxes/index.ios.ts | 8 +++++ .../numberOfConcurrentLightboxes/index.ts | 8 +++++ .../numberOfConcurrentLightboxes/types.ts | 5 +++ 4 files changed, 36 insertions(+), 18 deletions(-) rename src/components/{Lightbox.tsx => Lightbox/index.tsx} (89%) create mode 100644 src/components/Lightbox/numberOfConcurrentLightboxes/index.ios.ts create mode 100644 src/components/Lightbox/numberOfConcurrentLightboxes/index.ts create mode 100644 src/components/Lightbox/numberOfConcurrentLightboxes/types.ts diff --git a/src/components/Lightbox.tsx b/src/components/Lightbox/index.tsx similarity index 89% rename from src/components/Lightbox.tsx rename to src/components/Lightbox/index.tsx index d814e34933c0..8af6d301c2b7 100644 --- a/src/components/Lightbox.tsx +++ b/src/components/Lightbox/index.tsx @@ -1,16 +1,13 @@ import React, {useCallback, useEffect, useMemo, useState} from 'react'; import type {LayoutChangeEvent, NativeSyntheticEvent, StyleProp, ViewStyle} from 'react-native'; import {ActivityIndicator, PixelRatio, StyleSheet, View} from 'react-native'; +import Image from '@components/Image'; +import MultiGestureCanvas, {DEFAULT_ZOOM_RANGE} from '@components/MultiGestureCanvas'; +import type {CanvasSize, ContentSize, OnScaleChangedCallback, ZoomRange} from '@components/MultiGestureCanvas/types'; +import {getCanvasFitScale} from '@components/MultiGestureCanvas/utils'; import useStyleUtils from '@hooks/useStyleUtils'; -import Image from './Image'; -import MultiGestureCanvas, {DEFAULT_ZOOM_RANGE} from './MultiGestureCanvas'; -import type {CanvasSize, ContentSize, OnScaleChangedCallback, ZoomRange} from './MultiGestureCanvas/types'; -import {getCanvasFitScale} from './MultiGestureCanvas/utils'; - -// Increase/decrease this number to change the number of concurrent lightboxes -// The more concurrent lighboxes, the worse performance gets (especially on low-end devices) -type LightboxConcurrencyLimit = number | 'UNLIMITED'; -const NUMBER_OF_CONCURRENT_LIGHTBOXES: LightboxConcurrencyLimit = 3; +import NUMBER_OF_CONCURRENT_LIGHTBOXES from './numberOfConcurrentLightboxes'; + const DEFAULT_IMAGE_SIZE = 200; const DEFAULT_IMAGE_DIMENSION: ContentSize = {width: DEFAULT_IMAGE_SIZE, height: DEFAULT_IMAGE_SIZE}; @@ -75,7 +72,6 @@ function Lightbox({ ); const [contentSize, setInternalContentSize] = useState(() => cachedImageDimensions.get(uri)); - const isContentLoaded = contentSize !== undefined; const setContentSize = useCallback( (newDimensions: ContentSize | undefined) => { setInternalContentSize(newDimensions); @@ -131,7 +127,8 @@ function Lightbox({ // because it's only going to be rendered after the fallback image is hidden. const shouldShowLightbox = !hasSiblingCarouselItems || !isFallbackVisible; - const isLoading = isActive && (!isCanvasLoaded || !isContentLoaded); + const isContentLoaded = isLightboxImageLoaded || isFallbackImageLoaded; + const isLoading = isActive && (!isCanvasLoaded || !isContentLoaded || isFallbackVisible); // We delay setting a page to active state by a (few) millisecond(s), // to prevent the image transformer from flashing while still rendering @@ -148,6 +145,7 @@ function Lightbox({ if (isLightboxVisible) { return; } + setLightboxImageLoaded(false); setContentSize(undefined); }, [isLightboxVisible, setContentSize]); @@ -157,12 +155,9 @@ function Lightbox({ } if (isActive) { - if (isContentLoaded && isFallbackVisible) { - // We delay hiding the fallback image while image transformer is still rendering - setTimeout(() => { - setFallbackVisible(false); - setFallbackImageLoaded(false); - }, 100); + if (isLightboxImageLoaded && isFallbackVisible) { + setFallbackVisible(false); + setFallbackImageLoaded(false); } } else { if (isLightboxVisible && isLightboxImageLoaded) { @@ -196,7 +191,9 @@ function Lightbox({ isAuthTokenRequired={isAuthTokenRequired} onError={onError} onLoad={updateContentSize} - onLoadEnd={() => setLightboxImageLoaded(true)} + onLoadEnd={() => { + setLightboxImageLoaded(true); + }} /> diff --git a/src/components/Lightbox/numberOfConcurrentLightboxes/index.ios.ts b/src/components/Lightbox/numberOfConcurrentLightboxes/index.ios.ts new file mode 100644 index 000000000000..1ce0d2cee405 --- /dev/null +++ b/src/components/Lightbox/numberOfConcurrentLightboxes/index.ios.ts @@ -0,0 +1,8 @@ +import type LightboxConcurrencyLimit from './types'; + +// On iOS we can allow multiple lightboxes to be rendered at the same time. +// This enables faster time to interaction when swiping between pages in the carousel. +// When the lightbox is pre-rendered, we don't need to wait for the gestures to initialize. +const NUMBER_OF_CONCURRENT_LIGHTBOXES: LightboxConcurrencyLimit = 3; + +export default NUMBER_OF_CONCURRENT_LIGHTBOXES; diff --git a/src/components/Lightbox/numberOfConcurrentLightboxes/index.ts b/src/components/Lightbox/numberOfConcurrentLightboxes/index.ts new file mode 100644 index 000000000000..f6f55a8913c7 --- /dev/null +++ b/src/components/Lightbox/numberOfConcurrentLightboxes/index.ts @@ -0,0 +1,8 @@ +import type LightboxConcurrencyLimit from './types'; + +// On web, this is not used. +// On Android, we don't want to allow rendering multiple lightboxes, +// because performance is typically slower than on iOS and this caused issues. +const NUMBER_OF_CONCURRENT_LIGHTBOXES: LightboxConcurrencyLimit = 1; + +export default NUMBER_OF_CONCURRENT_LIGHTBOXES; diff --git a/src/components/Lightbox/numberOfConcurrentLightboxes/types.ts b/src/components/Lightbox/numberOfConcurrentLightboxes/types.ts new file mode 100644 index 000000000000..57aaa53cca8c --- /dev/null +++ b/src/components/Lightbox/numberOfConcurrentLightboxes/types.ts @@ -0,0 +1,5 @@ +// Increase/decrease this number to change the number of concurrent lightboxes +// The more concurrent lighboxes, the worse performance gets (especially on low-end devices) +type LightboxConcurrencyLimit = number | 'UNLIMITED'; + +export default LightboxConcurrencyLimit; From 5221cffe76793274cb39a4776a0c8c3c32bb54bd Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Fri, 19 Jan 2024 13:00:05 +0100 Subject: [PATCH 322/580] fix: lightbox flashing --- src/components/Lightbox/index.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/Lightbox/index.tsx b/src/components/Lightbox/index.tsx index 8af6d301c2b7..61942cf7e419 100644 --- a/src/components/Lightbox/index.tsx +++ b/src/components/Lightbox/index.tsx @@ -125,7 +125,7 @@ function Lightbox({ // so that we don't see two overlapping images at the same time. // If there the Lightbox is not used within a carousel, we don't need to hide the Lightbox, // because it's only going to be rendered after the fallback image is hidden. - const shouldShowLightbox = !hasSiblingCarouselItems || !isFallbackVisible; + const shouldShowLightbox = isLightboxImageLoaded && (!hasSiblingCarouselItems || !isFallbackVisible); const isContentLoaded = isLightboxImageLoaded || isFallbackImageLoaded; const isLoading = isActive && (!isCanvasLoaded || !isContentLoaded || isFallbackVisible); @@ -141,6 +141,7 @@ function Lightbox({ } }, [isItemActive]); + // Resets the lightbox when it becomes inactive useEffect(() => { if (isLightboxVisible) { return; @@ -149,6 +150,7 @@ function Lightbox({ setContentSize(undefined); }, [isLightboxVisible, setContentSize]); + // Enables and disables the fallback image when the carousel item is active or not useEffect(() => { if (!hasSiblingCarouselItems) { return; From 2dc798d2407ba3e5368e138a6a172c1053d6bcbf Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Fri, 19 Jan 2024 13:25:35 +0100 Subject: [PATCH 323/580] simplify lightbox concurrency --- src/components/Lightbox/index.tsx | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/src/components/Lightbox/index.tsx b/src/components/Lightbox/index.tsx index 61942cf7e419..eeecc71dfab2 100644 --- a/src/components/Lightbox/index.tsx +++ b/src/components/Lightbox/index.tsx @@ -92,9 +92,19 @@ function Lightbox({ const isItemActive = index === activeIndex; const [isActive, setActive] = useState(isItemActive); - const isInactiveCarouselItem = hasSiblingCarouselItems && !isActive; - const [isFallbackVisible, setFallbackVisible] = useState(isInactiveCarouselItem); + const isLightboxVisible = useMemo(() => { + if (!hasSiblingCarouselItems || NUMBER_OF_CONCURRENT_LIGHTBOXES === 'UNLIMITED') { + return true; + } + + const indexCanvasOffset = Math.floor((NUMBER_OF_CONCURRENT_LIGHTBOXES - 1) / 2) || 0; + const indexOutOfRange = index > activeIndex + indexCanvasOffset || index < activeIndex - indexCanvasOffset; + return !indexOutOfRange; + }, [activeIndex, hasSiblingCarouselItems, index]); + const [isLightboxImageLoaded, setLightboxImageLoaded] = useState(false); + + const [isFallbackVisible, setFallbackVisible] = useState(!isLightboxVisible); const [isFallbackImageLoaded, setFallbackImageLoaded] = useState(false); const fallbackSize = useMemo(() => { if (!hasSiblingCarouselItems || !contentSize || !isCanvasLoaded) { @@ -109,18 +119,6 @@ function Lightbox({ }; }, [hasSiblingCarouselItems, contentSize, isCanvasLoaded, canvasSize]); - const isLightboxInRange = useMemo(() => { - if (NUMBER_OF_CONCURRENT_LIGHTBOXES === 'UNLIMITED') { - return true; - } - - const indexCanvasOffset = Math.floor((NUMBER_OF_CONCURRENT_LIGHTBOXES - 1) / 2) || 0; - const indexOutOfRange = index > activeIndex + indexCanvasOffset || index < activeIndex - indexCanvasOffset; - return !indexOutOfRange; - }, [activeIndex, index]); - const [isLightboxImageLoaded, setLightboxImageLoaded] = useState(false); - const isLightboxVisible = isLightboxInRange && (isActive || isLightboxImageLoaded || isFallbackImageLoaded); - // If the fallback image is currently visible, we want to hide the Lightbox until the fallback gets hidden, // so that we don't see two overlapping images at the same time. // If there the Lightbox is not used within a carousel, we don't need to hide the Lightbox, From 08a6c4131c33d9e18eecc5a40dbf0cc772f7385e Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Fri, 19 Jan 2024 13:27:05 +0100 Subject: [PATCH 324/580] add comment --- src/components/Lightbox/index.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/components/Lightbox/index.tsx b/src/components/Lightbox/index.tsx index eeecc71dfab2..c7515040cb83 100644 --- a/src/components/Lightbox/index.tsx +++ b/src/components/Lightbox/index.tsx @@ -93,6 +93,10 @@ function Lightbox({ const isItemActive = index === activeIndex; const [isActive, setActive] = useState(isItemActive); + // Enables/disables the lightbox based on the number of concurrent lightboxes + // On higher-end devices, we can show render lightboxes at the same time, + // while on lower-end devices we want to only render the active carousel item as a lightbox + // to avoid performance issues. const isLightboxVisible = useMemo(() => { if (!hasSiblingCarouselItems || NUMBER_OF_CONCURRENT_LIGHTBOXES === 'UNLIMITED') { return true; From 55408387417e016eb45bb908df9ade8b373c3a68 Mon Sep 17 00:00:00 2001 From: brunovjk Date: Fri, 19 Jan 2024 09:51:36 -0300 Subject: [PATCH 325/580] 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 256a25af83ab62793322ca80b3c07861040a6228 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Fri, 19 Jan 2024 14:55:18 +0100 Subject: [PATCH 326/580] fix: tests --- tests/unit/MigrationTest.js | 4 ++-- tests/unit/ReportActionsUtilsTest.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/unit/MigrationTest.js b/tests/unit/MigrationTest.js index ebffc71e4e0e..4a363d1de36b 100644 --- a/tests/unit/MigrationTest.js +++ b/tests/unit/MigrationTest.js @@ -51,8 +51,8 @@ describe('Migrations', () => { it('Should remove any individual reportActions that have no data in Onyx', () => Onyx.multiSet({ [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}1`]: { - 1: null, - 2: null, + 1: {}, + 2: {}, }, }) .then(PersonalDetailsByAccountID) diff --git a/tests/unit/ReportActionsUtilsTest.js b/tests/unit/ReportActionsUtilsTest.js index b8b6eb5e7673..19a89d1c892c 100644 --- a/tests/unit/ReportActionsUtilsTest.js +++ b/tests/unit/ReportActionsUtilsTest.js @@ -368,7 +368,7 @@ describe('ReportActionsUtils', () => { callback: () => { Onyx.disconnect(connectionID); const res = ReportActionsUtils.getLastVisibleAction(report.reportID); - expect(res).toBe(action2); + expect(res).toEqual(action2); resolve(); }, }); From e6b100d0767ef68de14c1568aa1effdbda31a394 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Fri, 19 Jan 2024 14:56:22 +0100 Subject: [PATCH 327/580] bump node --- .nvmrc | 2 +- package.json | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.nvmrc b/.nvmrc index 43bff1f8cf98..d5a159609d09 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -20.9.0 \ No newline at end of file +20.10.0 diff --git a/package.json b/package.json index 60106ab33d08..7d98c42ef96a 100644 --- a/package.json +++ b/package.json @@ -88,7 +88,6 @@ "@react-ng/bounds-observer": "^0.2.1", "@rnmapbox/maps": "^10.0.11", "@shopify/flash-list": "^1.6.3", - "@types/node": "^18.14.0", "@ua/react-native-airship": "^15.3.1", "@vue/preload-webpack-plugin": "^2.0.0", "awesome-phonenumber": "^5.4.0", @@ -217,6 +216,7 @@ "@types/js-yaml": "^4.0.5", "@types/lodash": "^4.14.195", "@types/mapbox-gl": "^2.7.13", + "@types/node": "^20.11.5", "@types/pusher-js": "^5.1.0", "@types/react": "18.2.45", "@types/react-beautiful-dnd": "^13.1.4", @@ -316,7 +316,7 @@ ] }, "engines": { - "node": "20.9.0", - "npm": "10.1.0" + "node": "20.10.0", + "npm": "10.2.3" } } From 8f5f879da515b998472f5cbd4ffdc33428e9fadb Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Fri, 19 Jan 2024 16:19:08 +0100 Subject: [PATCH 328/580] 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 a58be47ce4632b4e59850cb07547f35f4e586285 Mon Sep 17 00:00:00 2001 From: Ishpaul Singh Date: Fri, 19 Jan 2024 21:25:13 +0530 Subject: [PATCH 329/580] 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 13:53:25 -0300 Subject: [PATCH 330/580] 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 c512b7ef0fdc5d08c777b4b83583cf684ec5e7c5 Mon Sep 17 00:00:00 2001 From: Marc Glasser Date: Fri, 19 Jan 2024 14:45:17 -1000 Subject: [PATCH 331/580] Add TS types for new participants object format --- src/types/onyx/Report.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/types/onyx/Report.ts b/src/types/onyx/Report.ts index b1571e7514e4..36c71056055a 100644 --- a/src/types/onyx/Report.ts +++ b/src/types/onyx/Report.ts @@ -13,6 +13,13 @@ type Note = { pendingAction?: OnyxCommon.PendingAction; }; +type Participant = { + hidden: boolean; + role?: 'admin' | 'member'; +}; + +type Participants = Record; + type Report = { /** The specific type of chat */ chatType?: ValueOf; @@ -117,6 +124,7 @@ type Report = { lastActorAccountID?: number; ownerAccountID?: number; ownerEmail?: string; + participants?: Participants; participantAccountIDs?: number[]; visibleChatMemberAccountIDs?: number[]; total?: number; From f2e4760ac4c3086f2dc5cce3ecc2b274d2882b97 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Sat, 20 Jan 2024 15:03:47 +0800 Subject: [PATCH 332/580] keep selected category at the same position if the list length is below the threshold --- src/libs/OptionsListUtils.js | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/src/libs/OptionsListUtils.js b/src/libs/OptionsListUtils.js index e86c9daacb42..812f142f4a5a 100644 --- a/src/libs/OptionsListUtils.js +++ b/src/libs/OptionsListUtils.js @@ -875,32 +875,32 @@ function getCategoryListSections(categories, recentlyUsedCategories, selectedOpt return categorySections; } - if (!_.isEmpty(selectedOptions)) { + const selectedOptionNames = _.map(selectedOptions, (selectedOption) => selectedOption.name); + const enabledAndSelectedCategories = _.filter(sortedCategories, (category) => category.enabled || _.includes(selectedOptionNames, category.name)); + const numberOfVisibleCategories = enabledAndSelectedCategories.length; + + if (numberOfVisibleCategories < CONST.CATEGORY_LIST_THRESHOLD) { categorySections.push({ - // "Selected" section + // "All" section when items amount less than the threshold title: '', shouldShow: false, indexOffset, - data: getCategoryOptionTree(selectedOptions, true), + data: getCategoryOptionTree(enabledAndSelectedCategories), }); - indexOffset += selectedOptions.length; + return categorySections; } - const selectedOptionNames = _.map(selectedOptions, (selectedOption) => selectedOption.name); - const filteredCategories = _.filter(enabledCategories, (category) => !_.includes(selectedOptionNames, category.name)); - const numberOfVisibleCategories = selectedOptions.length + filteredCategories.length; - - if (numberOfVisibleCategories < CONST.CATEGORY_LIST_THRESHOLD) { + if (!_.isEmpty(selectedOptions)) { categorySections.push({ - // "All" section when items amount less than the threshold + // "Selected" section title: '', shouldShow: false, indexOffset, - data: getCategoryOptionTree(filteredCategories), + data: getCategoryOptionTree(selectedOptions, true), }); - return categorySections; + indexOffset += selectedOptions.length; } const filteredRecentlyUsedCategories = _.chain(recentlyUsedCategories) @@ -925,6 +925,8 @@ function getCategoryListSections(categories, recentlyUsedCategories, selectedOpt indexOffset += filteredRecentlyUsedCategories.length; } + const filteredCategories = _.filter(enabledCategories, (category) => !_.includes(selectedOptionNames, category.name)); + categorySections.push({ // "All" section when items amount more than the threshold title: Localize.translateLocal('common.all'), From 5dfacd83687526f6000f5b0f095d48e83595791f Mon Sep 17 00:00:00 2001 From: someone-here Date: Sat, 20 Jan 2024 21:08:22 +0530 Subject: [PATCH 333/580] Show only left-out options in overflow menu --- .../BaseReportActionContextMenu.tsx | 58 +++++++++++++++---- .../report/ContextMenu/ContextMenuActions.tsx | 24 ++------ .../PopoverReportActionContextMenu.tsx | 5 ++ .../ContextMenu/ReportActionContextMenu.ts | 4 ++ 4 files changed, 61 insertions(+), 30 deletions(-) diff --git a/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx b/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx index 3eecb74a048a..213d94f51f81 100755 --- a/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx +++ b/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx @@ -2,6 +2,8 @@ import lodashIsEqual from 'lodash/isEqual'; import type {MutableRefObject, RefObject} from 'react'; import React, {memo, useMemo, useRef, useState} from 'react'; import {InteractionManager, View} from 'react-native'; +// eslint-disable-next-line no-restricted-imports +import type {GestureResponderEvent, Text as RNText, View as ViewType} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import type {ContextMenuItemHandle} from '@components/ContextMenuItem'; @@ -12,15 +14,16 @@ import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useStyleUtils from '@hooks/useStyleUtils'; import useWindowDimensions from '@hooks/useWindowDimensions'; +import * as ReportUtils from '@libs/ReportUtils'; import * as Session from '@userActions/Session'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Beta, ReportAction, ReportActions} from '@src/types/onyx'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; -import type {ContextMenuActionPayload} from './ContextMenuActions'; +import type {ContextMenuAction, ContextMenuActionPayload} from './ContextMenuActions'; import ContextMenuActions from './ContextMenuActions'; import type {ContextMenuType} from './ReportActionContextMenu'; -import {hideContextMenu} from './ReportActionContextMenu'; +import {hideContextMenu, showContextMenu} from './ReportActionContextMenu'; type BaseReportActionContextMenuOnyxProps = { /** Beta features list */ @@ -78,7 +81,11 @@ type BaseReportActionContextMenuProps = BaseReportActionContextMenuOnyxProps & { /** Content Ref */ contentRef?: RefObject; + /** Function to check if context menu is active */ checkIfContextMenuActive?: () => void; + + /** List of disabled actions */ + disabledActions?: ContextMenuAction[]; }; type MenuItemRefs = Record; @@ -100,6 +107,7 @@ function BaseReportActionContextMenu({ betas, reportActions, checkIfContextMenuActive, + disabledActions = [], }: BaseReportActionContextMenuProps) { const StyleUtils = useStyleUtils(); const {translate} = useLocalize(); @@ -117,13 +125,22 @@ function BaseReportActionContextMenu({ }, [reportActions, reportActionID]); const shouldEnableArrowNavigation = !isMini && (isVisible || shouldKeepOpen); - let filteredContextMenuActions = ContextMenuActions.filter((contextAction) => - contextAction.shouldShow(type, reportAction, isArchivedRoom, betas, anchor, isChronosReport, reportID, isPinnedChat, isUnreadChat, !!isOffline, isMini), + let filteredContextMenuActions = ContextMenuActions.filter( + (contextAction) => + !disabledActions.includes(contextAction) && + contextAction.shouldShow(type, reportAction, isArchivedRoom, betas, anchor, isChronosReport, reportID, isPinnedChat, isUnreadChat, !!isOffline, isMini), ); - filteredContextMenuActions = - isMini && filteredContextMenuActions.length > CONST.MINI_CONTEXT_MENU_MAX_ITEMS - ? ([...filteredContextMenuActions.slice(0, CONST.MINI_CONTEXT_MENU_MAX_ITEMS - 1), filteredContextMenuActions.at(-1)] as typeof filteredContextMenuActions) - : filteredContextMenuActions; + + if (isMini) { + const menuAction = filteredContextMenuActions.at(-1); + const otherActions = filteredContextMenuActions.slice(0, -1); + if (otherActions.length > CONST.MINI_CONTEXT_MENU_MAX_ITEMS && menuAction) { + filteredContextMenuActions = otherActions.slice(0, CONST.MINI_CONTEXT_MENU_MAX_ITEMS - 1); + filteredContextMenuActions.push(menuAction); + } else { + filteredContextMenuActions = otherActions; + } + } // Context menu actions that are not rendered as menu items are excluded from arrow navigation const nonMenuItemActionIndexes = filteredContextMenuActions.map((contextAction, index) => @@ -172,6 +189,28 @@ function BaseReportActionContextMenu({ {isActive: shouldEnableArrowNavigation}, ); + const openOverflowMenu = (event: GestureResponderEvent | MouseEvent) => { + const originalReportID = ReportUtils.getOriginalReportID(reportID, reportAction); + const originalReport = ReportUtils.getReport(originalReportID); + showContextMenu( + CONST.CONTEXT_MENU_TYPES.REPORT_ACTION, + event, + selection, + anchor?.current as ViewType | RNText | null, + reportID, + reportAction?.reportActionID, + originalReportID, + draftMessage, + checkIfContextMenuActive, + checkIfContextMenuActive, + ReportUtils.isArchivedRoom(originalReport), + ReportUtils.chatIncludesChronos(originalReport), + undefined, + undefined, + filteredContextMenuActions, + ); + }; + return ( (isVisible || shouldKeepOpen) && ( setShouldKeepOpen(false), openContextMenu: () => setShouldKeepOpen(true), interceptAnonymousUser, - anchor, - checkIfContextMenuActive, + openOverflowMenu, }; if ('renderContent' in contextAction) { diff --git a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx index ea25a00ee1d3..1e8f9dde64d6 100644 --- a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx +++ b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx @@ -70,8 +70,7 @@ type ContextMenuActionPayload = { close: () => void; openContextMenu: () => void; interceptAnonymousUser: (callback: () => void, isAnonymousAction?: boolean) => void; - anchor?: MutableRefObject; - checkIfContextMenuActive?: () => void; + openOverflowMenu: (event: GestureResponderEvent | MouseEvent) => void; event?: GestureResponderEvent | MouseEvent | KeyboardEvent; }; @@ -502,27 +501,12 @@ const ContextMenuActions: ContextMenuAction[] = [ textTranslateKey: 'reportActionContextMenu.menu', icon: Expensicons.ThreeDots, shouldShow: (type, reportAction, isArchivedRoom, betas, anchor, isChronosReport, reportID, isPinnedChat, isUnreadChat, isOffline, isMini) => isMini, - onPress: (closePopover, {reportAction, reportID, event, anchor, selection, draftMessage, checkIfContextMenuActive}) => { - const originalReportID = ReportUtils.getOriginalReportID(reportID, reportAction); - const originalReport = ReportUtils.getReport(originalReportID); - showContextMenu( - CONST.CONTEXT_MENU_TYPES.REPORT_ACTION, - event as GestureResponderEvent | MouseEvent, - selection, - anchor?.current as View | RNText | null, - reportID, - reportAction.reportActionID, - originalReportID, - draftMessage, - checkIfContextMenuActive, - checkIfContextMenuActive, - ReportUtils.isArchivedRoom(originalReport), - ReportUtils.chatIncludesChronos(originalReport), - ); + onPress: (closePopover, {openOverflowMenu, event}) => { + openOverflowMenu(event as GestureResponderEvent | MouseEvent); }, getDescription: () => {}, }, ]; export default ContextMenuActions; -export type {ContextMenuActionPayload}; +export type {ContextMenuActionPayload, ContextMenuAction}; diff --git a/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx b/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx index 476efa591177..42f27ecdfd59 100644 --- a/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx +++ b/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx @@ -15,6 +15,7 @@ import * as Report from '@userActions/Report'; import CONST from '@src/CONST'; import type {ReportAction} from '@src/types/onyx'; import BaseReportActionContextMenu from './BaseReportActionContextMenu'; +import type {ContextMenuAction} from './ContextMenuActions'; import type {ContextMenuType, ReportActionContextMenu} from './ReportActionContextMenu'; type Location = { @@ -61,6 +62,7 @@ function PopoverReportActionContextMenu(_props: never, ref: ForwardedRef([]); const contentRef = useRef(null); const anchorRef = useRef(null); @@ -160,6 +162,7 @@ function PopoverReportActionContextMenu(_props: never, ref: ForwardedRef { const {pageX = 0, pageY = 0} = extractPointerEvent(event); contextMenuAnchorRef.current = contextMenuAnchor; @@ -190,6 +193,7 @@ function PopoverReportActionContextMenu(_props: never, ref: ForwardedRef { + setDisabledActions(disabledActions); typeRef.current = type; reportIDRef.current = reportID ?? '0'; reportActionIDRef.current = reportActionID ?? '0'; @@ -319,6 +323,7 @@ function PopoverReportActionContextMenu(_props: never, ref: ForwardedRef void; @@ -30,6 +31,7 @@ type ShowContextMenu = ( isChronosReport?: boolean, isPinnedChat?: boolean, isUnreadChat?: boolean, + disabledOptions?: ContextMenuAction[], ) => void; type ReportActionContextMenu = { @@ -108,6 +110,7 @@ function showContextMenu( isChronosReport = false, isPinnedChat = false, isUnreadChat = false, + disabledActions: ContextMenuAction[] = [], ) { if (!contextMenuRef.current) { return; @@ -134,6 +137,7 @@ function showContextMenu( isChronosReport, isPinnedChat, isUnreadChat, + disabledActions, ); } From cbc19a1ab83129ce5dc351f8d5dfb3dddcd9050e Mon Sep 17 00:00:00 2001 From: someone-here Date: Sat, 20 Jan 2024 22:01:56 +0530 Subject: [PATCH 334/580] Fix lint --- src/pages/home/report/ContextMenu/ContextMenuActions.tsx | 5 ++--- .../report/ContextMenu/PopoverReportActionContextMenu.tsx | 4 ++-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx index 1e8f9dde64d6..dde99f746d9f 100644 --- a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx +++ b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx @@ -1,8 +1,7 @@ import ExpensiMark from 'expensify-common/lib/ExpensiMark'; import type {MutableRefObject} from 'react'; import React from 'react'; -// eslint-disable-next-line no-restricted-imports -import type {GestureResponderEvent, Text as RNText, View} from 'react-native'; +import type {GestureResponderEvent} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import type {Emoji} from '@assets/emojis/types'; import * as Expensicons from '@components/Icon/Expensicons'; @@ -29,7 +28,7 @@ import type {TranslationPaths} from '@src/languages/types'; import ROUTES from '@src/ROUTES'; import type {Beta, ReportAction, ReportActionReactions} from '@src/types/onyx'; import type IconAsset from '@src/types/utils/IconAsset'; -import {hideContextMenu, showContextMenu, showDeleteModal} from './ReportActionContextMenu'; +import {hideContextMenu, showDeleteModal} from './ReportActionContextMenu'; /** Gets the HTML version of the message in an action */ function getActionText(reportAction: OnyxEntry): string { diff --git a/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx b/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx index 42f27ecdfd59..b28374fae04a 100644 --- a/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx +++ b/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx @@ -162,7 +162,7 @@ function PopoverReportActionContextMenu(_props: never, ref: ForwardedRef { const {pageX = 0, pageY = 0} = extractPointerEvent(event); contextMenuAnchorRef.current = contextMenuAnchor; @@ -193,7 +193,7 @@ function PopoverReportActionContextMenu(_props: never, ref: ForwardedRef { - setDisabledActions(disabledActions); + setDisabledActions(disabledOptions); typeRef.current = type; reportIDRef.current = reportID ?? '0'; reportActionIDRef.current = reportActionID ?? '0'; 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 335/580] 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 336/580] 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 aae981c1222f3b2b561d2a94b25bf6c533e72f3c Mon Sep 17 00:00:00 2001 From: Hans Date: Sun, 21 Jan 2024 21:32:11 +0700 Subject: [PATCH 337/580] fix offline notes --- src/pages/home/report/withReportAndPrivateNotesOrNotFound.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/pages/home/report/withReportAndPrivateNotesOrNotFound.js b/src/pages/home/report/withReportAndPrivateNotesOrNotFound.js index de5f7d36299c..6ca7b4b26f91 100644 --- a/src/pages/home/report/withReportAndPrivateNotesOrNotFound.js +++ b/src/pages/home/report/withReportAndPrivateNotesOrNotFound.js @@ -64,6 +64,7 @@ export default function (pageTitle) { const prevIsOffline = usePrevious(network.isOffline); const isReconnecting = prevIsOffline && !network.isOffline; const isOtherUserNote = accountID && Number(session.accountID) !== Number(accountID); + const isPrivateNotesFetchFinished = isPrivateNotesFetchTriggered && !report.isLoadingPrivateNotes; const isPrivateNotesEmpty = accountID ? _.has(lodashGet(report, ['privateNotes', accountID, 'note'], '')) : _.isEmpty(report.privateNotes); useEffect(() => { @@ -76,7 +77,7 @@ export default function (pageTitle) { // eslint-disable-next-line react-hooks/exhaustive-deps -- do not add report.isLoadingPrivateNotes to dependencies }, [report.reportID, network.isOffline, isPrivateNotesFetchTriggered, isReconnecting]); - const shouldShowFullScreenLoadingIndicator = !isPrivateNotesFetchTriggered || (isPrivateNotesEmpty && (report.isLoadingPrivateNotes || !isOtherUserNote)); + const shouldShowFullScreenLoadingIndicator = !isPrivateNotesFetchFinished || (isPrivateNotesEmpty && (report.isLoadingPrivateNotes || !isOtherUserNote)); // eslint-disable-next-line rulesdir/no-negated-variables const shouldShowNotFoundPage = useMemo(() => { 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 338/580] 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 339/580] 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 340/580] 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 341/580] 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 c2547efcd3c7a24ffa6b938d6bedabfb7943c69e Mon Sep 17 00:00:00 2001 From: Tsaqif Date: Mon, 22 Jan 2024 14:18:39 +0700 Subject: [PATCH 342/580] 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 343/580] 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 6f9f7569e23a63ae0d1d7f16b9238cb4390eb039 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Mon, 22 Jan 2024 09:19:12 +0100 Subject: [PATCH 344/580] Update react-native-onyx with TS setup --- jest/setup.js | 2 +- src/ONYXKEYS.ts | 2 +- src/components/AnonymousReportFooter.tsx | 3 +-- src/components/ConfirmedRoute.tsx | 2 +- src/components/Indicator.tsx | 3 +-- src/components/WalletStatementModal/types.ts | 2 +- src/libs/Navigation/OnyxTabNavigator.tsx | 2 +- src/libs/ReportActionsUtils.ts | 2 +- src/libs/actions/FormActions.ts | 2 +- src/libs/actions/IOU.js | 2 +- src/libs/actions/MemoryOnlyKeys/MemoryOnlyKeys.ts | 2 +- src/libs/actions/PaymentMethods.ts | 2 +- src/libs/actions/Policy.ts | 3 +-- src/libs/actions/Report.ts | 3 +-- src/libs/actions/User.ts | 3 +-- src/libs/migrations/RenameReceiptFilename.ts | 2 +- src/pages/GetAssistancePage.tsx | 2 +- 17 files changed, 17 insertions(+), 22 deletions(-) diff --git a/jest/setup.js b/jest/setup.js index 38b4b55a68b3..e82bf678941d 100644 --- a/jest/setup.js +++ b/jest/setup.js @@ -19,7 +19,7 @@ jest.mock('@react-native-clipboard/clipboard', () => mockClipboard); // Mock react-native-onyx storage layer because the SQLite storage layer doesn't work in jest. // Mocking this file in __mocks__ does not work because jest doesn't support mocking files that are not directly used in the testing project, // and we only want to mock the storage layer, not the whole Onyx module. -jest.mock('react-native-onyx/lib/storage', () => require('react-native-onyx/lib/storage/__mocks__')); +jest.mock('react-native-onyx/dist/storage', () => require('react-native-onyx/dist/storage/__mocks__')); // Turn off the console logs for timing events. They are not relevant for unit tests and create a lot of noise jest.spyOn(console, 'debug').mockImplementation((...params) => { diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 40a43d8195de..c97e3014d8c1 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -1,4 +1,4 @@ -import type {OnyxEntry} from 'react-native-onyx/lib/types'; +import type {OnyxEntry} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import type CONST from './CONST'; import type * as OnyxTypes from './types/onyx'; diff --git a/src/components/AnonymousReportFooter.tsx b/src/components/AnonymousReportFooter.tsx index 04e8a5f8d55b..37115e8f3242 100644 --- a/src/components/AnonymousReportFooter.tsx +++ b/src/components/AnonymousReportFooter.tsx @@ -1,7 +1,6 @@ import React from 'react'; import {View} from 'react-native'; -import type {OnyxCollection} from 'react-native-onyx'; -import type {OnyxEntry} from 'react-native-onyx/lib/types'; +import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import * as Session from '@userActions/Session'; diff --git a/src/components/ConfirmedRoute.tsx b/src/components/ConfirmedRoute.tsx index c01f7c6250f4..c38241b275ba 100644 --- a/src/components/ConfirmedRoute.tsx +++ b/src/components/ConfirmedRoute.tsx @@ -1,7 +1,7 @@ import React, {useCallback, useEffect} from 'react'; import type {ReactNode} from 'react'; import {withOnyx} from 'react-native-onyx'; -import type {OnyxEntry} from 'react-native-onyx/lib/types'; +import type {OnyxEntry} from 'react-native-onyx'; import useNetwork from '@hooks/useNetwork'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; diff --git a/src/components/Indicator.tsx b/src/components/Indicator.tsx index 5bf93eb8a6b3..486189c66710 100644 --- a/src/components/Indicator.tsx +++ b/src/components/Indicator.tsx @@ -1,8 +1,7 @@ import React from 'react'; import {StyleSheet, View} from 'react-native'; -import type {OnyxCollection} from 'react-native-onyx'; +import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; -import type {OnyxEntry} from 'react-native-onyx/lib/types'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import * as PolicyUtils from '@libs/PolicyUtils'; diff --git a/src/components/WalletStatementModal/types.ts b/src/components/WalletStatementModal/types.ts index f6989f37f49b..567202730b7d 100644 --- a/src/components/WalletStatementModal/types.ts +++ b/src/components/WalletStatementModal/types.ts @@ -1,4 +1,4 @@ -import type {OnyxEntry} from 'react-native-onyx/lib/types'; +import type {OnyxEntry} from 'react-native-onyx'; import type {Session} from '@src/types/onyx'; type WalletStatementOnyxProps = { diff --git a/src/libs/Navigation/OnyxTabNavigator.tsx b/src/libs/Navigation/OnyxTabNavigator.tsx index b5466a9bbc2f..2ae3414956a8 100644 --- a/src/libs/Navigation/OnyxTabNavigator.tsx +++ b/src/libs/Navigation/OnyxTabNavigator.tsx @@ -3,7 +3,7 @@ import {createMaterialTopTabNavigator} from '@react-navigation/material-top-tabs import type {EventMapCore, NavigationState, ScreenListeners} from '@react-navigation/native'; import React from 'react'; import {withOnyx} from 'react-native-onyx'; -import type {OnyxEntry} from 'react-native-onyx/lib/types'; +import type {OnyxEntry} from 'react-native-onyx'; import Tab from '@userActions/Tab'; import ONYXKEYS from '@src/ONYXKEYS'; import type ChildrenProps from '@src/types/utils/ChildrenProps'; diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 262b8bf475af..79f127ef0062 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -2,7 +2,7 @@ import _ from 'lodash'; import lodashFindLast from 'lodash/findLast'; import type {OnyxCollection, OnyxEntry, OnyxUpdate} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; -import OnyxUtils from 'react-native-onyx/lib/utils'; +import OnyxUtils from 'react-native-onyx/dist/utils'; import type {ValueOf} from 'type-fest'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; diff --git a/src/libs/actions/FormActions.ts b/src/libs/actions/FormActions.ts index 6b73636e6d82..901f9d4bfe28 100644 --- a/src/libs/actions/FormActions.ts +++ b/src/libs/actions/FormActions.ts @@ -1,5 +1,5 @@ import Onyx from 'react-native-onyx'; -import type {KeyValueMapping, NullishDeep} from 'react-native-onyx/lib/types'; +import type {KeyValueMapping, NullishDeep} from 'react-native-onyx'; import FormUtils from '@libs/FormUtils'; import type {OnyxFormKey} from '@src/ONYXKEYS'; import type {Form} from '@src/types/onyx'; diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index 7ee752a1f0ef..5cd2eaafdd1e 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -3,7 +3,7 @@ import Str from 'expensify-common/lib/str'; import lodashGet from 'lodash/get'; import lodashHas from 'lodash/has'; import Onyx from 'react-native-onyx'; -import OnyxUtils from 'react-native-onyx/lib/utils'; +import OnyxUtils from 'react-native-onyx/dist/utils'; import _ from 'underscore'; import ReceiptGeneric from '@assets/images/receipt-generic.png'; import * as API from '@libs/API'; diff --git a/src/libs/actions/MemoryOnlyKeys/MemoryOnlyKeys.ts b/src/libs/actions/MemoryOnlyKeys/MemoryOnlyKeys.ts index 3e8c613187b4..15b9133f0aaf 100644 --- a/src/libs/actions/MemoryOnlyKeys/MemoryOnlyKeys.ts +++ b/src/libs/actions/MemoryOnlyKeys/MemoryOnlyKeys.ts @@ -1,5 +1,5 @@ import Onyx from 'react-native-onyx'; -import type {OnyxKey} from 'react-native-onyx/lib/types'; +import type {OnyxKey} from 'react-native-onyx'; import Log from '@libs/Log'; import ONYXKEYS from '@src/ONYXKEYS'; diff --git a/src/libs/actions/PaymentMethods.ts b/src/libs/actions/PaymentMethods.ts index 7e91c3531b3a..ba5cb8fee5ea 100644 --- a/src/libs/actions/PaymentMethods.ts +++ b/src/libs/actions/PaymentMethods.ts @@ -3,7 +3,7 @@ import type {MutableRefObject, SyntheticEvent} from 'react'; import type {NativeTouchEvent} from 'react-native'; import type {OnyxUpdate} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; -import type {OnyxEntry} from 'react-native-onyx/lib/types'; +import type {OnyxEntry} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import type {TransferMethod} from '@components/KYCWall/types'; import * as API from '@libs/API'; diff --git a/src/libs/actions/Policy.ts b/src/libs/actions/Policy.ts index cbbc00dd42fc..0e2e5adea024 100644 --- a/src/libs/actions/Policy.ts +++ b/src/libs/actions/Policy.ts @@ -4,9 +4,8 @@ import Str from 'expensify-common/lib/str'; import {escapeRegExp} from 'lodash'; import lodashClone from 'lodash/clone'; import lodashUnion from 'lodash/union'; -import type {OnyxCollection, OnyxUpdate} from 'react-native-onyx'; +import type {NullishDeep, OnyxCollection, OnyxEntry, OnyxUpdate} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; -import type {NullishDeep, OnyxEntry} from 'react-native-onyx/lib/types'; import * as API from '@libs/API'; import DateUtils from '@libs/DateUtils'; import * as ErrorUtils from '@libs/ErrorUtils'; diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 2ac85dfafa27..eff83e29c9e8 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -3,9 +3,8 @@ import ExpensiMark from 'expensify-common/lib/ExpensiMark'; import Str from 'expensify-common/lib/str'; import isEmpty from 'lodash/isEmpty'; import {DeviceEventEmitter, InteractionManager, Linking} from 'react-native'; -import type {OnyxCollection, OnyxEntry, OnyxUpdate} from 'react-native-onyx'; +import type {NullishDeep, OnyxCollection, OnyxEntry, OnyxUpdate} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; -import type {NullishDeep} from 'react-native-onyx/lib/types'; import type {PartialDeep, ValueOf} from 'type-fest'; import type {Emoji} from '@assets/emojis/types'; import * as ActiveClientManager from '@libs/ActiveClientManager'; diff --git a/src/libs/actions/User.ts b/src/libs/actions/User.ts index df5709ac68e2..9a114282e039 100644 --- a/src/libs/actions/User.ts +++ b/src/libs/actions/User.ts @@ -1,7 +1,6 @@ import {isBefore} from 'date-fns'; -import type {OnyxCollection, OnyxUpdate} from 'react-native-onyx'; +import type {OnyxCollection, OnyxEntry, OnyxUpdate} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; -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'; diff --git a/src/libs/migrations/RenameReceiptFilename.ts b/src/libs/migrations/RenameReceiptFilename.ts index dff2be5c286d..b867024fc74e 100644 --- a/src/libs/migrations/RenameReceiptFilename.ts +++ b/src/libs/migrations/RenameReceiptFilename.ts @@ -1,5 +1,5 @@ import Onyx from 'react-native-onyx'; -import type {NullishDeep, OnyxCollection} from 'react-native-onyx/lib/types'; +import type {NullishDeep, OnyxCollection} from 'react-native-onyx'; import Log from '@libs/Log'; import ONYXKEYS from '@src/ONYXKEYS'; import type Transaction from '@src/types/onyx/Transaction'; diff --git a/src/pages/GetAssistancePage.tsx b/src/pages/GetAssistancePage.tsx index 46963e56997a..948e0c239de9 100644 --- a/src/pages/GetAssistancePage.tsx +++ b/src/pages/GetAssistancePage.tsx @@ -2,7 +2,7 @@ import type {StackScreenProps} from '@react-navigation/stack'; import React from 'react'; import {ScrollView, View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; -import type {OnyxEntry} from 'react-native-onyx/lib/types'; +import type {OnyxEntry} from 'react-native-onyx'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import * as Expensicons from '@components/Icon/Expensicons'; import * as Illustrations from '@components/Icon/Illustrations'; From e9d8f4d7e996fa8d520e2c16a85ddd376a266745 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Mon, 22 Jan 2024 09:48:01 +0100 Subject: [PATCH 345/580] 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 346/580] 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 7910c46358b43b293f58a9b2462800f2f946b86f Mon Sep 17 00:00:00 2001 From: VickyStash Date: Mon, 22 Jan 2024 10:12:14 +0100 Subject: [PATCH 347/580] TS fixes after merging main --- .../ReportActionItem/ReportActionItemImage.tsx | 2 +- .../ReportActionItem/ReportActionItemImages.tsx | 11 ++--------- src/components/ReportActionItem/ReportPreview.tsx | 1 - src/libs/ReceiptUtils.ts | 8 ++++---- 4 files changed, 7 insertions(+), 15 deletions(-) diff --git a/src/components/ReportActionItem/ReportActionItemImage.tsx b/src/components/ReportActionItem/ReportActionItemImage.tsx index aa5d0513f0d7..d408f815e70c 100644 --- a/src/components/ReportActionItem/ReportActionItemImage.tsx +++ b/src/components/ReportActionItem/ReportActionItemImage.tsx @@ -17,7 +17,7 @@ import type {Transaction} from '@src/types/onyx'; type ReportActionItemImageProps = { /** thumbnail URI for the image */ - thumbnail?: string | number; + thumbnail?: string | number | null; /** URI for the image or local numeric reference for the image */ image: string | number; diff --git a/src/components/ReportActionItem/ReportActionItemImages.tsx b/src/components/ReportActionItem/ReportActionItemImages.tsx index c24defb8ac08..06edea95fb89 100644 --- a/src/components/ReportActionItem/ReportActionItemImages.tsx +++ b/src/components/ReportActionItem/ReportActionItemImages.tsx @@ -6,20 +6,13 @@ import Text from '@components/Text'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; +import type {ThumbnailAndImageURI} from '@libs/ReceiptUtils'; import variables from '@styles/variables'; -import type {Transaction} from '@src/types/onyx'; import ReportActionItemImage from './ReportActionItemImage'; -type Image = { - thumbnail: string | number; - image: string | number; - transaction: Transaction; - isLocalFile: boolean; -}; - type ReportActionItemImagesProps = { /** array of image and thumbnail URIs */ - images: Image[]; + images: ThumbnailAndImageURI[]; // We're not providing default values for size and total and disabling the ESLint rule // because we want them to default to the length of images, but we can't set default props diff --git a/src/components/ReportActionItem/ReportPreview.tsx b/src/components/ReportActionItem/ReportPreview.tsx index 9c2075dd927f..42097cb84f4e 100644 --- a/src/components/ReportActionItem/ReportPreview.tsx +++ b/src/components/ReportActionItem/ReportPreview.tsx @@ -245,7 +245,6 @@ function ReportPreview({ {hasReceipts && ( Date: Mon, 22 Jan 2024 10:21:26 +0100 Subject: [PATCH 348/580] 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 e15c9649d9b835c5b4e341122d50e2bc46f62ad1 Mon Sep 17 00:00:00 2001 From: Tsaqif Date: Mon, 22 Jan 2024 16:46:49 +0700 Subject: [PATCH 349/580] 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 0446a6de27f3f1ff38e03ca5c86f5841e204736e Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Mon, 22 Jan 2024 11:19:28 +0100 Subject: [PATCH 350/580] improve --- .../AttachmentViewPdf/BaseAttachmentViewPdf.js | 4 ++-- src/components/MultiGestureCanvas/constants.ts | 4 +++- src/components/MultiGestureCanvas/types.ts | 6 +++--- src/components/MultiGestureCanvas/useTapGestures.ts | 4 +--- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.js b/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.js index cb3db626b423..1333f641aee9 100644 --- a/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.js +++ b/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.js @@ -16,7 +16,7 @@ function BaseAttachmentViewPdf({ style, }) { const attachmentCarouselPagerContext = useContext(AttachmentCarouselPagerContext); - const scrollEnabled = attachmentCarouselPagerContext === undefined ? 1 : attachmentCarouselPagerContext.scrollEnabled; + const scrollEnabled = attachmentCarouselPagerContext === null ? 1 : attachmentCarouselPagerContext.scrollEnabled; useEffect(() => { if (!attachmentCarouselPagerContext) { @@ -43,7 +43,7 @@ function BaseAttachmentViewPdf({ if (onPressProp !== undefined) { onPressProp(e); } - if (attachmentCarouselPagerContext !== undefined && attachmentCarouselPagerContext.onTap !== undefined && scrollEnabled) { + if (attachmentCarouselPagerContext !== null && scrollEnabled) { attachmentCarouselPagerContext.onTap(e); } }, diff --git a/src/components/MultiGestureCanvas/constants.ts b/src/components/MultiGestureCanvas/constants.ts index 7dba3e568ea4..58ad6997bbeb 100644 --- a/src/components/MultiGestureCanvas/constants.ts +++ b/src/components/MultiGestureCanvas/constants.ts @@ -1,6 +1,8 @@ import type {WithSpringConfig} from 'react-native-reanimated'; import type {ZoomRange} from './types'; +const DOUBLE_TAP_SCALE = 3; + // The spring config is used to determine the physics of the spring animation // Details and a playground for testing different configs can be found at // https://docs.swmansion.com/react-native-reanimated/docs/animations/withSpring @@ -23,4 +25,4 @@ const ZOOM_RANGE_BOUNCE_FACTORS: Required = { max: 1.5, }; -export {SPRING_CONFIG, DEFAULT_ZOOM_RANGE, ZOOM_RANGE_BOUNCE_FACTORS}; +export {DOUBLE_TAP_SCALE, SPRING_CONFIG, DEFAULT_ZOOM_RANGE, ZOOM_RANGE_BOUNCE_FACTORS}; diff --git a/src/components/MultiGestureCanvas/types.ts b/src/components/MultiGestureCanvas/types.ts index c10b1ab677a8..bbd8f69e6947 100644 --- a/src/components/MultiGestureCanvas/types.ts +++ b/src/components/MultiGestureCanvas/types.ts @@ -22,7 +22,7 @@ type ZoomRange = { type OnScaleChangedCallback = (zoomScale: number) => void; /** Triggered when the canvas is tapped (single tap) */ -type OnTapCallback = (() => void) | undefined; +type OnTapCallback = () => void; /** Types used of variables used within the MultiGestureCanvas component and it's hooks */ type MultiGestureCanvasVariables = { @@ -43,8 +43,8 @@ type MultiGestureCanvasVariables = { pinchTranslateY: SharedValue; stopAnimation: () => void; reset: (animated: boolean, callback: () => void) => void; - onTap: OnTapCallback | undefined; - onScaleChanged?: OnScaleChangedCallback; + onTap: OnTapCallback; + onScaleChanged: OnScaleChangedCallback | undefined; }; export type {CanvasSize, ContentSize, ZoomRange, OnScaleChangedCallback, MultiGestureCanvasVariables}; diff --git a/src/components/MultiGestureCanvas/useTapGestures.ts b/src/components/MultiGestureCanvas/useTapGestures.ts index 6eba3849d572..ce67f11a91c8 100644 --- a/src/components/MultiGestureCanvas/useTapGestures.ts +++ b/src/components/MultiGestureCanvas/useTapGestures.ts @@ -3,12 +3,10 @@ import {useMemo} from 'react'; import type {TapGesture} from 'react-native-gesture-handler'; import {Gesture} from 'react-native-gesture-handler'; import {runOnJS, useWorkletCallback, withSpring} from 'react-native-reanimated'; -import {SPRING_CONFIG} from './constants'; +import {DOUBLE_TAP_SCALE, SPRING_CONFIG} from './constants'; import type {MultiGestureCanvasVariables} from './types'; import * as MultiGestureCanvasUtils from './utils'; -const DOUBLE_TAP_SCALE = 3; - type UseTapGesturesProps = Pick< MultiGestureCanvasVariables, 'canvasSize' | 'contentSize' | 'minContentScale' | 'maxContentScale' | 'offsetX' | 'offsetY' | 'pinchScale' | 'zoomScale' | 'reset' | 'stopAnimation' | 'onScaleChanged' | 'onTap' From acadc33aaef5bbe0a0603e4adcdf0d5c3a8dd072 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Mon, 22 Jan 2024 11:27:51 +0100 Subject: [PATCH 351/580] improve types --- .../AttachmentCarousel/Pager/usePageScrollHandler.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/components/Attachments/AttachmentCarousel/Pager/usePageScrollHandler.ts b/src/components/Attachments/AttachmentCarousel/Pager/usePageScrollHandler.ts index bcc616883d72..6f0ab4a51dc7 100644 --- a/src/components/Attachments/AttachmentCarousel/Pager/usePageScrollHandler.ts +++ b/src/components/Attachments/AttachmentCarousel/Pager/usePageScrollHandler.ts @@ -2,11 +2,17 @@ import type {PagerViewProps} from 'react-native-pager-view'; import {useEvent, useHandler} from 'react-native-reanimated'; type PageScrollHandler = NonNullable; -type OnPageScrollEventData = Parameters[0]['nativeEvent']; -type OnPageScrollREAHandler = Parameters>>[0][string]; + +type PageScrollEventData = Parameters[0]['nativeEvent']; +type PageScrollContext = Record; + +// Reanimated doesn't expose the type for animated event handlers, therefore we must infer it from the useHandler hook. +// The AnimatedPageScrollHandler type is the type of the onPageScroll prop from react-native-pager-view as an animated handler. +type AnimatedHandlers = Parameters>[0]; +type AnimatedPageScrollHandler = AnimatedHandlers[string]; type Handlers = { - onPageScroll?: OnPageScrollREAHandler; + onPageScroll?: AnimatedPageScrollHandler; }; type Deps = Parameters[1]; 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 352/580] 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 353/580] 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 354/580] 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 ae6f23fa599618c8470ec658d44490cafa97e0df Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Mon, 22 Jan 2024 14:34:04 +0100 Subject: [PATCH 355/580] update onyx --- package-lock.json | 72 +++++++++++++++++++++++++++++++++++------------ package.json | 2 +- 2 files changed, 55 insertions(+), 19 deletions(-) diff --git a/package-lock.json b/package-lock.json index 433e4dcf4de2..98e05d5aa4de 100644 --- a/package-lock.json +++ b/package-lock.json @@ -40,7 +40,6 @@ "@react-ng/bounds-observer": "^0.2.1", "@rnmapbox/maps": "^10.0.11", "@shopify/flash-list": "^1.6.3", - "@types/node": "^18.14.0", "@ua/react-native-airship": "^15.3.1", "@vue/preload-webpack-plugin": "^2.0.0", "awesome-phonenumber": "^5.4.0", @@ -94,7 +93,7 @@ "react-native-linear-gradient": "^2.8.1", "react-native-localize": "^2.2.6", "react-native-modal": "^13.0.0", - "react-native-onyx": "1.0.118", + "react-native-onyx": "1.0.129", "react-native-pager-view": "6.2.2", "react-native-pdf": "6.7.3", "react-native-performance": "^5.1.0", @@ -169,6 +168,7 @@ "@types/js-yaml": "^4.0.5", "@types/lodash": "^4.14.195", "@types/mapbox-gl": "^2.7.13", + "@types/node": "^20.11.5", "@types/pusher-js": "^5.1.0", "@types/react": "18.2.45", "@types/react-beautiful-dnd": "^13.1.4", @@ -240,8 +240,8 @@ "yaml": "^2.2.1" }, "engines": { - "node": "20.9.0", - "npm": "10.1.0" + "node": "20.10.0", + "npm": "10.2.3" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -20646,9 +20646,12 @@ "dev": true }, "node_modules/@types/node": { - "version": "18.17.11", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.17.11.tgz", - "integrity": "sha512-r3hjHPBu+3LzbGBa8DHnr/KAeTEEOrahkcL+cZc4MaBMTM+mk8LtXR+zw+nqfjuDZZzYTYgTcpHuP+BEQk069g==" + "version": "20.11.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.5.tgz", + "integrity": "sha512-g557vgQjUUfN76MZAN/dt1z3dzcUsimuysco0KeluHgrPdJXkP/XdAURgyO2W9fZWHRtRBiVKzKn8vyOAwlG+w==", + "dependencies": { + "undici-types": "~5.26.4" + } }, "node_modules/@types/node-fetch": { "version": "2.6.4", @@ -28752,6 +28755,15 @@ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.435.tgz", "integrity": "sha512-B0CBWVFhvoQCW/XtjRzgrmqcgVWg6RXOEM/dK59+wFV93BFGR6AeNKc4OyhM+T3IhJaOOG8o/V+33Y2mwJWtzw==" }, + "node_modules/electron/node_modules/@types/node": { + "version": "18.19.8", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.8.tgz", + "integrity": "sha512-g1pZtPhsvGVTwmeVoexWZLTQaOvXwoSq//pTL0DHeNzUDrFnir4fgETdhjhIxjVnN+hKOuh98+E1eMLnUXstFg==", + "dev": true, + "dependencies": { + "undici-types": "~5.26.4" + } + }, "node_modules/element-resize-detector": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/element-resize-detector/-/element-resize-detector-1.2.4.tgz", @@ -45081,17 +45093,17 @@ } }, "node_modules/react-native-onyx": { - "version": "1.0.118", - "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-1.0.118.tgz", - "integrity": "sha512-w54jO+Bpu1ElHsrxZXIIpcBqNkrUvuVCQmwWdfOW5LvO4UwsPSwmMxzExbUZ4ip+7CROmm10IgXFaAoyfeYSVQ==", + "version": "1.0.129", + "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-1.0.129.tgz", + "integrity": "sha512-9cvsT4AqD1pwXAckFSIdG6t6yw9AiE+CISF5E6DCxg0KEhe8en/7H0oN++adETRyaEj5qt8micEUltZxmXfh/Q==", "dependencies": { "ascii-table": "0.0.9", "fast-equals": "^4.0.3", "underscore": "^1.13.6" }, "engines": { - "node": ">=16.15.1 <=20.9.0", - "npm": ">=8.11.0 <=10.1.0" + "node": ">=20.10.0", + "npm": ">=10.2.3" }, "peerDependencies": { "idb-keyval": "^6.2.1", @@ -50968,6 +50980,11 @@ "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.6.tgz", "integrity": "sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==" }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + }, "node_modules/unfetch": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/unfetch/-/unfetch-4.2.0.tgz", @@ -68365,9 +68382,12 @@ "dev": true }, "@types/node": { - "version": "18.17.11", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.17.11.tgz", - "integrity": "sha512-r3hjHPBu+3LzbGBa8DHnr/KAeTEEOrahkcL+cZc4MaBMTM+mk8LtXR+zw+nqfjuDZZzYTYgTcpHuP+BEQk069g==" + "version": "20.11.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.5.tgz", + "integrity": "sha512-g557vgQjUUfN76MZAN/dt1z3dzcUsimuysco0KeluHgrPdJXkP/XdAURgyO2W9fZWHRtRBiVKzKn8vyOAwlG+w==", + "requires": { + "undici-types": "~5.26.4" + } }, "@types/node-fetch": { "version": "2.6.4", @@ -74111,6 +74131,17 @@ "@electron/get": "^2.0.0", "@types/node": "^18.11.18", "extract-zip": "^2.0.1" + }, + "dependencies": { + "@types/node": { + "version": "18.19.8", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.8.tgz", + "integrity": "sha512-g1pZtPhsvGVTwmeVoexWZLTQaOvXwoSq//pTL0DHeNzUDrFnir4fgETdhjhIxjVnN+hKOuh98+E1eMLnUXstFg==", + "dev": true, + "requires": { + "undici-types": "~5.26.4" + } + } } }, "electron-builder": { @@ -85998,9 +86029,9 @@ } }, "react-native-onyx": { - "version": "1.0.118", - "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-1.0.118.tgz", - "integrity": "sha512-w54jO+Bpu1ElHsrxZXIIpcBqNkrUvuVCQmwWdfOW5LvO4UwsPSwmMxzExbUZ4ip+7CROmm10IgXFaAoyfeYSVQ==", + "version": "1.0.129", + "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-1.0.129.tgz", + "integrity": "sha512-9cvsT4AqD1pwXAckFSIdG6t6yw9AiE+CISF5E6DCxg0KEhe8en/7H0oN++adETRyaEj5qt8micEUltZxmXfh/Q==", "requires": { "ascii-table": "0.0.9", "fast-equals": "^4.0.3", @@ -90150,6 +90181,11 @@ "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.6.tgz", "integrity": "sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==" }, + "undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + }, "unfetch": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/unfetch/-/unfetch-4.2.0.tgz", diff --git a/package.json b/package.json index d13375369438..25e541a2fa69 100644 --- a/package.json +++ b/package.json @@ -141,7 +141,7 @@ "react-native-linear-gradient": "^2.8.1", "react-native-localize": "^2.2.6", "react-native-modal": "^13.0.0", - "react-native-onyx": "1.0.118", + "react-native-onyx": "1.0.129", "react-native-pager-view": "6.2.2", "react-native-pdf": "6.7.3", "react-native-performance": "^5.1.0", 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 356/580] 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 08698b3d83b421040219e7c62eace49d31f81350 Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Mon, 22 Jan 2024 16:26:24 +0100 Subject: [PATCH 357/580] simplify onChange prop --- src/components/MagicCodeInput.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/components/MagicCodeInput.tsx b/src/components/MagicCodeInput.tsx index 1b5bb8642d32..e6a5148f6ef9 100644 --- a/src/components/MagicCodeInput.tsx +++ b/src/components/MagicCodeInput.tsx @@ -375,9 +375,7 @@ function MagicCodeInput( autoComplete={input.length === 0 ? autoComplete : undefined} shouldDelayFocus={input.length === 0 && shouldDelayFocus} keyboardType={CONST.KEYBOARD_TYPE.NUMBER_PAD} - onChangeText={(textValue) => { - onChangeText(textValue); - }} + onChangeText={onChangeText} onKeyPress={onKeyPress} onFocus={onFocus} onBlur={() => { From 51a4c6479a1ba2c6d20eaf9e652e4d2159b092ba Mon Sep 17 00:00:00 2001 From: someone-here Date: Mon, 22 Jan 2024 23:15:21 +0530 Subject: [PATCH 358/580] Change icon for unread --- src/pages/home/report/ContextMenu/ContextMenuActions.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx index dde99f746d9f..95300d4ed3f4 100644 --- a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx +++ b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx @@ -238,7 +238,7 @@ const ContextMenuActions: ContextMenuAction[] = [ { isAnonymousAction: false, textTranslateKey: 'reportActionContextMenu.markAsUnread', - icon: Expensicons.Mail, + icon: Expensicons.ChatBubbleUnread, successIcon: Expensicons.Checkmark, shouldShow: (type, reportAction, isArchivedRoom, betas, menuTarget, isChronosReport, reportID, isPinnedChat, isUnreadChat) => type === CONST.CONTEXT_MENU_TYPES.REPORT_ACTION || (type === CONST.CONTEXT_MENU_TYPES.REPORT && !isUnreadChat), From 158e5e66f3af413a53a405f18c009b28b508dab1 Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Mon, 22 Jan 2024 12:06:43 -0700 Subject: [PATCH 359/580] calculate reimbursable total based on total and nonReimbursableTotal --- src/libs/ReportUtils.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 78086c354de0..982ff286932b 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -1811,13 +1811,13 @@ function getMoneyRequestReimbursableTotal(report: OnyxEntry, allReportsD moneyRequestReport = allAvailableReports[`${ONYXKEYS.COLLECTION.REPORT}${report.iouReportID}`]; } if (moneyRequestReport) { - const total = moneyRequestReport?.total ?? 0; + const reimbursableTotal = (moneyRequestReport?.total ?? 0) - (moneyRequestReport?.nonReimbursableTotal ?? 0); - if (total !== 0) { + if (reimbursableTotal !== 0) { // There is a possibility that if the Expense report has a negative total. // This is because there are instances where you can get a credit back on your card, // or you enter a negative expense to “offset” future expenses - return isExpenseReport(moneyRequestReport) ? total * -1 : Math.abs(total); + return isExpenseReport(moneyRequestReport) ? reimbursableTotal * -1 : Math.abs(reimbursableTotal); } } return 0; From 09e90dd42268f798b7e581e2022ea36841a3901f Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Mon, 22 Jan 2024 12:12:59 -0700 Subject: [PATCH 360/580] revert changes --- src/libs/ReportUtils.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 982ff286932b..78086c354de0 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -1811,13 +1811,13 @@ function getMoneyRequestReimbursableTotal(report: OnyxEntry, allReportsD moneyRequestReport = allAvailableReports[`${ONYXKEYS.COLLECTION.REPORT}${report.iouReportID}`]; } if (moneyRequestReport) { - const reimbursableTotal = (moneyRequestReport?.total ?? 0) - (moneyRequestReport?.nonReimbursableTotal ?? 0); + const total = moneyRequestReport?.total ?? 0; - if (reimbursableTotal !== 0) { + if (total !== 0) { // There is a possibility that if the Expense report has a negative total. // This is because there are instances where you can get a credit back on your card, // or you enter a negative expense to “offset” future expenses - return isExpenseReport(moneyRequestReport) ? reimbursableTotal * -1 : Math.abs(reimbursableTotal); + return isExpenseReport(moneyRequestReport) ? total * -1 : Math.abs(total); } } return 0; From 0ddc19813ee488fbb7f54797f462da9beb007d83 Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Mon, 22 Jan 2024 12:13:49 -0700 Subject: [PATCH 361/580] use getMoneyRequestSpendBreakdown --- src/components/MoneyReportHeader.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index afdc62218f95..b69fe63bb638 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -52,7 +52,7 @@ function MoneyReportHeader({session, personalDetails, policy, chatReport, nextSt const styles = useThemeStyles(); const {translate} = useLocalize(); const {windowWidth, isSmallScreenWidth} = useWindowDimensions(); - const reimbursableTotal = ReportUtils.getMoneyRequestReimbursableTotal(moneyRequestReport); + const {reimbursableSpend} = ReportUtils.getMoneyRequestSpendBreakdown(moneyRequestReport); const isApproved = ReportUtils.isReportApproved(moneyRequestReport); const isSettled = ReportUtils.isSettled(moneyRequestReport.reportID); const policyType = policy?.type; @@ -65,8 +65,8 @@ function MoneyReportHeader({session, personalDetails, policy, chatReport, nextSt : isPolicyAdmin || (ReportUtils.isMoneyRequestReport(moneyRequestReport) && isManager); const isDraft = ReportUtils.isDraftExpenseReport(moneyRequestReport); const shouldShowPayButton = useMemo( - () => isPayer && !isDraft && !isSettled && !moneyRequestReport.isWaitingOnBankAccount && reimbursableTotal !== 0 && !ReportUtils.isArchivedRoom(chatReport), - [isPayer, isDraft, isSettled, moneyRequestReport, reimbursableTotal, chatReport], + () => isPayer && !isDraft && !isSettled && !moneyRequestReport.isWaitingOnBankAccount && reimbursableSpend !== 0 && !ReportUtils.isArchivedRoom(chatReport), + [isPayer, isDraft, isSettled, moneyRequestReport, reimbursableSpend, chatReport], ); const shouldShowApproveButton = useMemo(() => { if (!isPaidGroupPolicy) { @@ -75,12 +75,12 @@ function MoneyReportHeader({session, personalDetails, policy, chatReport, nextSt return isManager && !isDraft && !isApproved && !isSettled; }, [isPaidGroupPolicy, isManager, isDraft, isApproved, isSettled]); const shouldShowSettlementButton = shouldShowPayButton || shouldShowApproveButton; - const shouldShowSubmitButton = isDraft && reimbursableTotal !== 0; + const shouldShowSubmitButton = isDraft && reimbursableSpend !== 0; const isFromPaidPolicy = policyType === CONST.POLICY.TYPE.TEAM || policyType === CONST.POLICY.TYPE.CORPORATE; const shouldShowNextStep = isFromPaidPolicy && !!nextStep?.message?.length; const shouldShowAnyButton = shouldShowSettlementButton || shouldShowApproveButton || shouldShowSubmitButton || shouldShowNextStep; const bankAccountRoute = ReportUtils.getBankAccountRoute(chatReport); - const formattedAmount = CurrencyUtils.convertToDisplayString(reimbursableTotal, moneyRequestReport.currency); + const formattedAmount = CurrencyUtils.convertToDisplayString(reimbursableSpend, moneyRequestReport.currency); const isMoreContentShown = shouldShowNextStep || (shouldShowAnyButton && isSmallScreenWidth); // The submit button should be success green colour only if the user is submitter and the policy does not have Scheduled Submit turned on From 25595f98f3a5cda690d7fc6d01a77e7117fa2a24 Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Mon, 22 Jan 2024 12:15:19 -0700 Subject: [PATCH 362/580] rename getMoneyRequestReimbursableTotal to getMoneyRequestTotal --- src/libs/OptionsListUtils.js | 2 +- src/libs/ReportUtils.ts | 10 +++++----- src/libs/SidebarUtils.ts | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/libs/OptionsListUtils.js b/src/libs/OptionsListUtils.js index e86c9daacb42..724c6465bdce 100644 --- a/src/libs/OptionsListUtils.js +++ b/src/libs/OptionsListUtils.js @@ -568,7 +568,7 @@ function createOption(accountIDs, personalDetails, report, reportActions = {}, { } result.isIOUReportOwner = ReportUtils.isIOUOwnedByCurrentUser(result); - result.iouReportAmount = ReportUtils.getMoneyRequestReimbursableTotal(result); + result.iouReportAmount = ReportUtils.getMoneyRequestTotal(result); if (!hasMultipleParticipants) { result.login = personalDetail.login; diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 78086c354de0..49e310bcadc8 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -1801,7 +1801,7 @@ function hasNonReimbursableTransactions(iouReportID: string | undefined): boolea return transactions.filter((transaction) => transaction.reimbursable === false).length > 0; } -function getMoneyRequestReimbursableTotal(report: OnyxEntry, allReportsDict: OnyxCollection = null): number { +function getMoneyRequestTotal(report: OnyxEntry, allReportsDict: OnyxCollection = null): number { const allAvailableReports = allReportsDict ?? allReports; let moneyRequestReport: OnyxEntry | undefined; if (isMoneyRequestReport(report)) { @@ -1903,7 +1903,7 @@ function getPolicyExpenseChatName(report: OnyxEntry, policy: OnyxEntry

, policy: OnyxEntry | undefined = undefined): string { - const moneyRequestTotal = getMoneyRequestReimbursableTotal(report); + const moneyRequestTotal = getMoneyRequestTotal(report); const formattedAmount = CurrencyUtils.convertToDisplayString(moneyRequestTotal, report?.currency, hasOnlyDistanceRequestTransactions(report?.reportID)); const payerOrApproverName = isExpenseReport(report) ? getPolicyName(report, false, policy) : getDisplayNameForParticipant(report?.managerID) ?? ''; const payerPaidAmountMessage = Localize.translateLocal('iou.payerPaidAmount', { @@ -2203,7 +2203,7 @@ function getReportPreviewMessage( } } - const totalAmount = getMoneyRequestReimbursableTotal(report); + const totalAmount = getMoneyRequestTotal(report); const policyName = getPolicyName(report, false, policy); const payerName = isExpenseReport(report) ? policyName : getDisplayNameForParticipant(report.managerID, !isPreviewMessageForParentChatReport); @@ -2746,7 +2746,7 @@ function getIOUReportActionMessage(iouReportID: string, type: string, total: num const report = getReport(iouReportID); const amount = type === CONST.IOU.REPORT_ACTION_TYPE.PAY - ? CurrencyUtils.convertToDisplayString(getMoneyRequestReimbursableTotal(!isEmptyObject(report) ? report : null), currency) + ? CurrencyUtils.convertToDisplayString(getMoneyRequestTotal(!isEmptyObject(report) ? report : null), currency) : CurrencyUtils.convertToDisplayString(total, currency); let paymentMethodMessage; @@ -4593,7 +4593,7 @@ export { hasExpensifyGuidesEmails, requiresAttentionFromCurrentUser, isIOUOwnedByCurrentUser, - getMoneyRequestReimbursableTotal, + getMoneyRequestTotal, getMoneyRequestSpendBreakdown, canShowReportRecipientLocalTime, formatReportLastMessageText, diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index 445d9dc30dd8..0548c4071330 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -199,7 +199,7 @@ function getOrderedReportIDs( report.displayName = ReportUtils.getReportName(report); // eslint-disable-next-line no-param-reassign - report.iouReportAmount = ReportUtils.getMoneyRequestReimbursableTotal(report, allReports); + report.iouReportAmount = ReportUtils.getMoneyRequestTotal(report, allReports); const isPinned = report.isPinned ?? false; const reportAction = ReportActionsUtils.getReportAction(report.parentReportID ?? '', report.parentReportActionID ?? ''); @@ -458,7 +458,7 @@ function getOptionData({ } result.isIOUReportOwner = ReportUtils.isIOUOwnedByCurrentUser(result as Report); - result.iouReportAmount = ReportUtils.getMoneyRequestReimbursableTotal(result as Report); + result.iouReportAmount = ReportUtils.getMoneyRequestTotal(result as Report); if (!hasMultipleParticipants) { result.accountID = personalDetail.accountID; From 1be02bcbdb657af69134eaac589d5b80b3f96ac7 Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Mon, 22 Jan 2024 12:27:38 -0700 Subject: [PATCH 363/580] delete getMoneyRequestReimbursableTotal --- src/libs/OptionsListUtils.js | 2 +- src/libs/ReportUtils.ts | 29 +++-------------------------- src/libs/SidebarUtils.ts | 4 ++-- 3 files changed, 6 insertions(+), 29 deletions(-) diff --git a/src/libs/OptionsListUtils.js b/src/libs/OptionsListUtils.js index 724c6465bdce..beab49818fa0 100644 --- a/src/libs/OptionsListUtils.js +++ b/src/libs/OptionsListUtils.js @@ -568,7 +568,7 @@ function createOption(accountIDs, personalDetails, report, reportActions = {}, { } result.isIOUReportOwner = ReportUtils.isIOUOwnedByCurrentUser(result); - result.iouReportAmount = ReportUtils.getMoneyRequestTotal(result); + result.iouReportAmount = ReportUtils.getMoneyRequestSpendBreakdown(result).totalDisplaySpend; if (!hasMultipleParticipants) { result.login = personalDetail.login; diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 49e310bcadc8..ab06125a828c 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -1801,28 +1801,6 @@ function hasNonReimbursableTransactions(iouReportID: string | undefined): boolea return transactions.filter((transaction) => transaction.reimbursable === false).length > 0; } -function getMoneyRequestTotal(report: OnyxEntry, allReportsDict: OnyxCollection = null): number { - const allAvailableReports = allReportsDict ?? allReports; - let moneyRequestReport: OnyxEntry | undefined; - if (isMoneyRequestReport(report)) { - moneyRequestReport = report; - } - if (allAvailableReports && report?.iouReportID) { - moneyRequestReport = allAvailableReports[`${ONYXKEYS.COLLECTION.REPORT}${report.iouReportID}`]; - } - if (moneyRequestReport) { - const total = moneyRequestReport?.total ?? 0; - - if (total !== 0) { - // There is a possibility that if the Expense report has a negative total. - // This is because there are instances where you can get a credit back on your card, - // or you enter a negative expense to “offset” future expenses - return isExpenseReport(moneyRequestReport) ? total * -1 : Math.abs(total); - } - } - return 0; -} - function getMoneyRequestSpendBreakdown(report: OnyxEntry, allReportsDict: OnyxCollection = null): SpendBreakdown { const allAvailableReports = allReportsDict ?? allReports; let moneyRequestReport; @@ -1903,7 +1881,7 @@ function getPolicyExpenseChatName(report: OnyxEntry, policy: OnyxEntry

, policy: OnyxEntry | undefined = undefined): string { - const moneyRequestTotal = getMoneyRequestTotal(report); + const moneyRequestTotal = getMoneyRequestSpendBreakdown(report).totalDisplaySpend; const formattedAmount = CurrencyUtils.convertToDisplayString(moneyRequestTotal, report?.currency, hasOnlyDistanceRequestTransactions(report?.reportID)); const payerOrApproverName = isExpenseReport(report) ? getPolicyName(report, false, policy) : getDisplayNameForParticipant(report?.managerID) ?? ''; const payerPaidAmountMessage = Localize.translateLocal('iou.payerPaidAmount', { @@ -2203,7 +2181,7 @@ function getReportPreviewMessage( } } - const totalAmount = getMoneyRequestTotal(report); + const totalAmount = getMoneyRequestSpendBreakdown(report).totalDisplaySpend; const policyName = getPolicyName(report, false, policy); const payerName = isExpenseReport(report) ? policyName : getDisplayNameForParticipant(report.managerID, !isPreviewMessageForParentChatReport); @@ -2746,7 +2724,7 @@ function getIOUReportActionMessage(iouReportID: string, type: string, total: num const report = getReport(iouReportID); const amount = type === CONST.IOU.REPORT_ACTION_TYPE.PAY - ? CurrencyUtils.convertToDisplayString(getMoneyRequestTotal(!isEmptyObject(report) ? report : null), currency) + ? CurrencyUtils.convertToDisplayString(getMoneyRequestSpendBreakdown(!isEmptyObject(report) ? report : null).totalDisplaySpend, currency) : CurrencyUtils.convertToDisplayString(total, currency); let paymentMethodMessage; @@ -4593,7 +4571,6 @@ export { hasExpensifyGuidesEmails, requiresAttentionFromCurrentUser, isIOUOwnedByCurrentUser, - getMoneyRequestTotal, getMoneyRequestSpendBreakdown, canShowReportRecipientLocalTime, formatReportLastMessageText, diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index 0548c4071330..a3208208dbc2 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -199,7 +199,7 @@ function getOrderedReportIDs( report.displayName = ReportUtils.getReportName(report); // eslint-disable-next-line no-param-reassign - report.iouReportAmount = ReportUtils.getMoneyRequestTotal(report, allReports); + report.iouReportAmount = ReportUtils.getMoneyRequestSpendBreakdown(report, allReports).totalDisplaySpend; const isPinned = report.isPinned ?? false; const reportAction = ReportActionsUtils.getReportAction(report.parentReportID ?? '', report.parentReportActionID ?? ''); @@ -458,7 +458,7 @@ function getOptionData({ } result.isIOUReportOwner = ReportUtils.isIOUOwnedByCurrentUser(result as Report); - result.iouReportAmount = ReportUtils.getMoneyRequestTotal(result as Report); + result.iouReportAmount = ReportUtils.getMoneyRequestSpendBreakdown(result as Report).totalDisplaySpend; if (!hasMultipleParticipants) { result.accountID = personalDetail.accountID; 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 364/580] 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 365/580] 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 d0f1991ca041cc44c9e221fd4f9583165a46c22d Mon Sep 17 00:00:00 2001 From: Tsaqif Date: Tue, 23 Jan 2024 07:23:27 +0700 Subject: [PATCH 366/580] 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 b584ed2f8ec0d2777632de3b75a2e4af5983b757 Mon Sep 17 00:00:00 2001 From: tienifr Date: Tue, 23 Jan 2024 10:03:00 +0700 Subject: [PATCH 367/580] 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 368/580] 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 369/580] 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 f7a19cab866735c8485a25bad4d7abc817d066dd Mon Sep 17 00:00:00 2001 From: Aswin S Date: Tue, 23 Jan 2024 09:50:32 +0530 Subject: [PATCH 370/580] fix: common suffix length calculation --- src/libs/ComposerUtils/index.ts | 6 ++++-- .../ComposerWithSuggestions/ComposerWithSuggestions.js | 4 ++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/libs/ComposerUtils/index.ts b/src/libs/ComposerUtils/index.ts index 54af287a67b7..c0e01e3e751b 100644 --- a/src/libs/ComposerUtils/index.ts +++ b/src/libs/ComposerUtils/index.ts @@ -38,11 +38,13 @@ function canSkipTriggerHotkeys(isSmallScreenWidth: boolean, isKeyboardShown: boo * Finds the length of common suffix between two texts * @param str1 - first string to compare * @param str2 - next string to compare + * @param cursorPosition - position of cursor * @returns number - Length of the common suffix */ -function findCommonSuffixLength(str1: string, str2: string) { +function findCommonSuffixLength(str1: string, str2: string, cursorPosition: number) { let commonSuffixLength = 0; - const minLength = Math.min(str1.length, str2.length); + const minLength = Math.min(str1.length - cursorPosition, str2.length); + for (let i = 1; i <= minLength; i++) { if (str1.charAt(str1.length - i) === str2.charAt(str2.length - i)) { commonSuffixLength++; diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js index 0706fa4fc8e2..90738c66579c 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js @@ -213,7 +213,7 @@ function ComposerWithSuggestions({ if (currentIndex < newText.length) { startIndex = currentIndex; - const commonSuffixLength = ComposerUtils.findCommonSuffixLength(prevText, newText); + const commonSuffixLength = ComposerUtils.findCommonSuffixLength(prevText, newText, selection.end); // if text is getting pasted over find length of common suffix and subtract it from new text length if (commonSuffixLength > 0 || selection.end - selection.start > 0) { endIndex = newText.length - commonSuffixLength; @@ -228,7 +228,7 @@ function ComposerWithSuggestions({ diff: newText.substring(startIndex, endIndex), }; }, - [selection.end, selection.start], + [selection.start, selection.end], ); /** From 83553150afbd3236a82010865715904315ef7526 Mon Sep 17 00:00:00 2001 From: Aswin S Date: Tue, 23 Jan 2024 09:54:12 +0530 Subject: [PATCH 371/580] refactor: remove redundant changes --- .../ComposerWithSuggestions/ComposerWithSuggestions.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js index 90738c66579c..ee7f0168940d 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js @@ -302,14 +302,14 @@ function ComposerWithSuggestions({ } }, [ - raiseIsScrollLikelyLayoutTriggered, + debouncedUpdateFrequentlyUsedEmojis, findNewlyAddedChars, - preferredSkinTone, preferredLocale, + preferredSkinTone, + reportID, setIsCommentEmpty, suggestionsRef, - debouncedUpdateFrequentlyUsedEmojis, - reportID, + raiseIsScrollLikelyLayoutTriggered, debouncedSaveReportComment, selection.end, ], From b97e34887116eeebe23aa7b5ed3e683aa7b824fb Mon Sep 17 00:00:00 2001 From: Dylan Date: Tue, 23 Jan 2024 13:56:39 +0700 Subject: [PATCH 372/580] 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 373/580] 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 cb0548e93d5215a0f691606651d2a3969dda475a Mon Sep 17 00:00:00 2001 From: Jayesh Mangwani Date: Tue, 23 Jan 2024 12:55:13 +0530 Subject: [PATCH 374/580] feat: changed user status stylings --- src/pages/home/sidebar/AvatarWithOptionalStatus.js | 7 ++++--- src/styles/index.ts | 14 ++++++++------ .../utils/statusEmojiStyles/index.desktop.ts | 4 +--- src/styles/utils/statusEmojiStyles/index.ts | 2 +- .../utils/statusEmojiStyles/index.website.ts | 8 ++++---- 5 files changed, 18 insertions(+), 17 deletions(-) diff --git a/src/pages/home/sidebar/AvatarWithOptionalStatus.js b/src/pages/home/sidebar/AvatarWithOptionalStatus.js index 9c31e1250c39..6786af9c6d2d 100644 --- a/src/pages/home/sidebar/AvatarWithOptionalStatus.js +++ b/src/pages/home/sidebar/AvatarWithOptionalStatus.js @@ -46,12 +46,13 @@ function AvatarWithOptionalStatus({emojiStatus, isCreateMenuOpen}) { accessibilityLabel={translate('sidebarScreen.buttonMySettings')} role={CONST.ROLE.BUTTON} onPress={showStatusPage} - style={styles.sidebarStatusAvatar} + style={[styles.sidebarStatusAvatar]} > - + + {/* */} {emojiStatus} diff --git a/src/styles/index.ts b/src/styles/index.ts index 0a4f0e6dfa8a..fa7d4bff450b 100644 --- a/src/styles/index.ts +++ b/src/styles/index.ts @@ -3933,13 +3933,15 @@ const styles = (theme: ThemeColors) => sidebarStatusAvatar: { alignItems: 'center', justifyContent: 'center', - backgroundColor: theme.componentBG, - height: 30, - width: 30, - borderRadius: 15, + backgroundColor: theme.border, + height: 16, + width: 16, + borderRadius: 8, position: 'absolute', - right: -7, - bottom: -7, + right: -2, + bottom: -2, + borderColor: theme.highlightBG, + borderWidth: 2, }, moneyRequestViewImage: { ...spacing.mh5, diff --git a/src/styles/utils/statusEmojiStyles/index.desktop.ts b/src/styles/utils/statusEmojiStyles/index.desktop.ts index 88b33768abc2..6761d04a4825 100644 --- a/src/styles/utils/statusEmojiStyles/index.desktop.ts +++ b/src/styles/utils/statusEmojiStyles/index.desktop.ts @@ -1,7 +1,5 @@ import type StatusEmojiStyles from './types'; -const statusEmojiStyles: StatusEmojiStyles = { - marginTop: 2, -}; +const statusEmojiStyles: StatusEmojiStyles = {}; export default statusEmojiStyles; diff --git a/src/styles/utils/statusEmojiStyles/index.ts b/src/styles/utils/statusEmojiStyles/index.ts index 6761d04a4825..e0d576789575 100644 --- a/src/styles/utils/statusEmojiStyles/index.ts +++ b/src/styles/utils/statusEmojiStyles/index.ts @@ -1,5 +1,5 @@ import type StatusEmojiStyles from './types'; -const statusEmojiStyles: StatusEmojiStyles = {}; +const statusEmojiStyles: StatusEmojiStyles = {marginTop: -2}; export default statusEmojiStyles; diff --git a/src/styles/utils/statusEmojiStyles/index.website.ts b/src/styles/utils/statusEmojiStyles/index.website.ts index 8cd724504314..fffbe2e97195 100644 --- a/src/styles/utils/statusEmojiStyles/index.website.ts +++ b/src/styles/utils/statusEmojiStyles/index.website.ts @@ -2,9 +2,9 @@ import * as Browser from '@libs/Browser'; import type StatusEmojiStyles from './types'; const statusEmojiStyles: StatusEmojiStyles = Browser.isMobile() - ? {} - : { - marginTop: 2, - }; + ? { + marginTop: -2, + } + : {}; export default statusEmojiStyles; 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 375/580] 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 376/580] 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 1a75ea271c99bfcf2f5f0d804b9406f5b447dcf1 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Tue, 23 Jan 2024 16:31:56 +0800 Subject: [PATCH 377/580] show scrollbar by default --- src/components/OptionsList/BaseOptionsList.tsx | 2 +- src/components/SelectionList/BaseSelectionList.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/OptionsList/BaseOptionsList.tsx b/src/components/OptionsList/BaseOptionsList.tsx index c1e4562a0c2d..174b2e14a18a 100644 --- a/src/components/OptionsList/BaseOptionsList.tsx +++ b/src/components/OptionsList/BaseOptionsList.tsx @@ -33,7 +33,7 @@ function BaseOptionsList( optionHoveredStyle, contentContainerStyles, sectionHeaderStyle, - showScrollIndicator = false, + showScrollIndicator = true, listContainerStyles: listContainerStylesProp, shouldDisableRowInnerPadding = false, shouldPreventDefaultFocusOnSelectRow = false, diff --git a/src/components/SelectionList/BaseSelectionList.js b/src/components/SelectionList/BaseSelectionList.js index 960618808fd9..93841c5a885e 100644 --- a/src/components/SelectionList/BaseSelectionList.js +++ b/src/components/SelectionList/BaseSelectionList.js @@ -52,7 +52,7 @@ function BaseSelectionList({ onConfirm, headerContent, footerContent, - showScrollIndicator = false, + showScrollIndicator = true, showLoadingPlaceholder = false, showConfirmButton = false, shouldPreventDefaultFocusOnSelectRow = false, From 586936980f8e7728f9ec04544e2697ef31c63d71 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Tue, 23 Jan 2024 16:32:21 +0800 Subject: [PATCH 378/580] fix wrong indicator style --- src/components/OptionsList/BaseOptionsList.tsx | 1 - src/components/SelectionList/BaseSelectionList.js | 3 --- 2 files changed, 4 deletions(-) diff --git a/src/components/OptionsList/BaseOptionsList.tsx b/src/components/OptionsList/BaseOptionsList.tsx index 174b2e14a18a..4619cd8ac361 100644 --- a/src/components/OptionsList/BaseOptionsList.tsx +++ b/src/components/OptionsList/BaseOptionsList.tsx @@ -241,7 +241,6 @@ function BaseOptionsList( ref={ref} style={listStyles} - indicatorStyle="white" keyboardShouldPersistTaps="always" keyboardDismissMode={keyboardDismissMode} nestedScrollEnabled={nestedScrollEnabled} diff --git a/src/components/SelectionList/BaseSelectionList.js b/src/components/SelectionList/BaseSelectionList.js index 93841c5a885e..e0c0a56b994d 100644 --- a/src/components/SelectionList/BaseSelectionList.js +++ b/src/components/SelectionList/BaseSelectionList.js @@ -18,7 +18,6 @@ import useActiveElementRole from '@hooks/useActiveElementRole'; import useKeyboardShortcut from '@hooks/useKeyboardShortcut'; import useLocalize from '@hooks/useLocalize'; import useStyleUtils from '@hooks/useStyleUtils'; -import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import Log from '@libs/Log'; import variables from '@styles/variables'; @@ -67,7 +66,6 @@ function BaseSelectionList({ shouldUseDynamicMaxToRenderPerBatch = false, rightHandSideComponent, }) { - const theme = useTheme(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); const {translate} = useLocalize(); @@ -474,7 +472,6 @@ function BaseSelectionList({ onScrollBeginDrag={onScrollBeginDrag} keyExtractor={(item) => item.keyForList} extraData={focusedIndex} - indicatorStyle={theme.white} keyboardShouldPersistTaps="always" showsVerticalScrollIndicator={showScrollIndicator} initialNumToRender={12} From acdd24bb3ec97736feeaf4d666f7975294cb7709 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Tue, 23 Jan 2024 09:34:45 +0100 Subject: [PATCH 379/580] 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 380/580] 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 cd01c9a018ddbbe52a9426efc031dafc3dd6896f Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Tue, 23 Jan 2024 16:53:29 +0800 Subject: [PATCH 381/580] add back indicator style --- src/components/OptionsList/BaseOptionsList.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/OptionsList/BaseOptionsList.tsx b/src/components/OptionsList/BaseOptionsList.tsx index 4619cd8ac361..174b2e14a18a 100644 --- a/src/components/OptionsList/BaseOptionsList.tsx +++ b/src/components/OptionsList/BaseOptionsList.tsx @@ -241,6 +241,7 @@ function BaseOptionsList( ref={ref} style={listStyles} + indicatorStyle="white" keyboardShouldPersistTaps="always" keyboardDismissMode={keyboardDismissMode} nestedScrollEnabled={nestedScrollEnabled} From 0110446447181bd0095d945e28bcc6555808735c Mon Sep 17 00:00:00 2001 From: VickyStash Date: Tue, 23 Jan 2024 10:03:28 +0100 Subject: [PATCH 382/580] Fix TS issue --- src/libs/ReceiptUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/ReceiptUtils.ts b/src/libs/ReceiptUtils.ts index ae2658677dd9..9c6f10c12935 100644 --- a/src/libs/ReceiptUtils.ts +++ b/src/libs/ReceiptUtils.ts @@ -29,7 +29,7 @@ type FileNameAndExtension = { */ function getThumbnailAndImageURIs(transaction: Transaction, receiptPath: string | null = null, receiptFileName: string | null = null): ThumbnailAndImageURI { if (Object.hasOwn(transaction?.pendingFields ?? {}, 'waypoints')) { - return {thumbnail: null, image: ReceiptGeneric, isLocalFile: true}; + return {thumbnail: null, image: ReceiptGeneric as string | number, isLocalFile: true}; } // URI to image, i.e. blob:new.expensify.com/9ef3a018-4067-47c6-b29f-5f1bd35f213d or expensify.com/receipts/w_e616108497ef940b7210ec6beb5a462d01a878f4.jpg From 8444a4d4a9b1c5b7a43f30d5a08ecbc76d9dde5e Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Tue, 23 Jan 2024 10:05:22 +0100 Subject: [PATCH 383/580] Final API refactors, use const commands --- src/libs/HttpUtils.ts | 3 ++- src/libs/Middleware/Logging.ts | 3 ++- src/libs/Middleware/SaveResponseInOnyx.ts | 3 ++- src/libs/Network/NetworkStore.ts | 3 ++- src/libs/actions/Session/index.ts | 2 +- 5 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/libs/HttpUtils.ts b/src/libs/HttpUtils.ts index 22e342ac847b..a69647b7b5b1 100644 --- a/src/libs/HttpUtils.ts +++ b/src/libs/HttpUtils.ts @@ -6,6 +6,7 @@ import ONYXKEYS from '@src/ONYXKEYS'; import type {RequestType} from '@src/types/onyx/Request'; import type Response from '@src/types/onyx/Response'; import * as NetworkActions from './actions/Network'; +import {READ_COMMANDS, SIDE_EFFECT_REQUEST_COMMANDS} from './API/types'; import * as ApiUtils from './ApiUtils'; import HttpsError from './Errors/HttpsError'; @@ -29,7 +30,7 @@ let cancellationController = new AbortController(); /** * The API commands that require the skew calculation */ -const addSkewList = ['OpenReport', 'ReconnectApp', 'OpenApp']; +const addSkewList: string[] = [SIDE_EFFECT_REQUEST_COMMANDS.OPEN_REPORT, SIDE_EFFECT_REQUEST_COMMANDS.RECONNECT_APP, READ_COMMANDS.OPEN_APP]; /** * Regex to get API command from the command diff --git a/src/libs/Middleware/Logging.ts b/src/libs/Middleware/Logging.ts index 27a904f692ed..97f4a21866c5 100644 --- a/src/libs/Middleware/Logging.ts +++ b/src/libs/Middleware/Logging.ts @@ -1,3 +1,4 @@ +import {SIDE_EFFECT_REQUEST_COMMANDS} from '@libs/API/types'; import Log from '@libs/Log'; import CONST from '@src/CONST'; import type Request from '@src/types/onyx/Request'; @@ -87,7 +88,7 @@ const Logging: Middleware = (response, request) => { // This error seems to only throw on dev when localhost:8080 tries to access the production web server. It's unclear whether this can happen on production or if // it's a sign that the web server is down. Log.hmmm('[Network] API request error: Gateway Timeout error', logParams); - } else if (request.command === 'AuthenticatePusher') { + } else if (request.command === SIDE_EFFECT_REQUEST_COMMANDS.AUTHENTICATE_PUSHER) { // AuthenticatePusher requests can return with fetch errors and no message. It happens because we return a non 200 header like 403 Forbidden. // This is common to see if we are subscribing to a bad channel related to something the user shouldn't be able to access. There's no additional information // we can get about these requests. diff --git a/src/libs/Middleware/SaveResponseInOnyx.ts b/src/libs/Middleware/SaveResponseInOnyx.ts index a9182745098b..f7b37ab66bf5 100644 --- a/src/libs/Middleware/SaveResponseInOnyx.ts +++ b/src/libs/Middleware/SaveResponseInOnyx.ts @@ -1,3 +1,4 @@ +import {READ_COMMANDS, SIDE_EFFECT_REQUEST_COMMANDS} from '@libs/API/types'; import * as MemoryOnlyKeys from '@userActions/MemoryOnlyKeys/MemoryOnlyKeys'; import * as OnyxUpdates from '@userActions/OnyxUpdates'; import CONST from '@src/CONST'; @@ -6,7 +7,7 @@ import type Middleware from './types'; // If we're executing any of these requests, we don't need to trigger our OnyxUpdates flow to update the current data even if our current value is out of // date because all these requests are updating the app to the most current state. -const requestsToIgnoreLastUpdateID = ['OpenApp', 'ReconnectApp', 'GetMissingOnyxMessages']; +const requestsToIgnoreLastUpdateID = [READ_COMMANDS.OPEN_APP, SIDE_EFFECT_REQUEST_COMMANDS.RECONNECT_APP, SIDE_EFFECT_REQUEST_COMMANDS.GET_MISSING_ONYX_MESSAGES]; const SaveResponseInOnyx: Middleware = (requestResponse, request) => requestResponse.then((response = {}) => { diff --git a/src/libs/Network/NetworkStore.ts b/src/libs/Network/NetworkStore.ts index 59a52dfd01c4..5b93c9adc11a 100644 --- a/src/libs/Network/NetworkStore.ts +++ b/src/libs/Network/NetworkStore.ts @@ -1,4 +1,5 @@ import Onyx from 'react-native-onyx'; +import {READ_COMMANDS, SIDE_EFFECT_REQUEST_COMMANDS} from '@libs/API/types'; import ONYXKEYS from '@src/ONYXKEYS'; import type Credentials from '@src/types/onyx/Credentials'; @@ -95,7 +96,7 @@ function getAuthToken(): string | null { } function isSupportRequest(command: string): boolean { - return ['OpenApp', 'ReconnectApp', 'OpenReport'].includes(command); + return [READ_COMMANDS.OPEN_APP, SIDE_EFFECT_REQUEST_COMMANDS.RECONNECT_APP, SIDE_EFFECT_REQUEST_COMMANDS.OPEN_REPORT].some((cmd) => cmd === command); } function getSupportAuthToken(): string | null { diff --git a/src/libs/actions/Session/index.ts b/src/libs/actions/Session/index.ts index d10763100a8f..4fbeba0abaa6 100644 --- a/src/libs/actions/Session/index.ts +++ b/src/libs/actions/Session/index.ts @@ -616,7 +616,7 @@ function setAccountError(error: string) { const reauthenticatePusher = throttle( () => { Log.info('[Pusher] Re-authenticating and then reconnecting'); - Authentication.reauthenticate('AuthenticatePusher') + Authentication.reauthenticate(SIDE_EFFECT_REQUEST_COMMANDS.AUTHENTICATE_PUSHER) .then(Pusher.reconnect) .catch(() => { console.debug('[PusherConnectionManager]', 'Unable to re-authenticate Pusher because we are offline.'); From 6d21d627cad1101e5b604c6604b3d8f432c16cae Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Tue, 23 Jan 2024 10:28:57 +0100 Subject: [PATCH 384/580] fix: pdf scroll --- .../Pager/AttachmentCarouselPagerContext.ts | 5 +++-- .../AttachmentCarousel/Pager/index.tsx | 22 ++++++++++++------- .../AttachmentCarousel/index.native.js | 2 +- .../BaseAttachmentViewPdf.js | 6 ++--- .../AttachmentViewPdf/index.android.js | 10 +++++---- src/components/MultiGestureCanvas/index.tsx | 4 ++-- 6 files changed, 29 insertions(+), 20 deletions(-) diff --git a/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts b/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts index b2aaea942073..94f503002b4a 100644 --- a/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts +++ b/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts @@ -5,8 +5,9 @@ import type {SharedValue} from 'react-native-reanimated'; type AttachmentCarouselPagerContextValue = { pagerRef: ForwardedRef; - isPagerSwiping: SharedValue; - scrollEnabled: boolean; + isPagerScrolling: SharedValue; + isScrollEnabled: boolean; + setScrollEnabled: (shouldPagerScroll: boolean) => void; onTap: () => void; onScaleChanged: (scale: number) => void; }; diff --git a/src/components/Attachments/AttachmentCarousel/Pager/index.tsx b/src/components/Attachments/AttachmentCarousel/Pager/index.tsx index f4b6d9e918da..dded43516947 100644 --- a/src/components/Attachments/AttachmentCarousel/Pager/index.tsx +++ b/src/components/Attachments/AttachmentCarousel/Pager/index.tsx @@ -27,7 +27,7 @@ type PagerItem = { type AttachmentCarouselPagerProps = { items: PagerItem[]; - scrollEnabled?: boolean; + isZoomedOut?: boolean; renderItem: (props: {item: PagerItem; index: number; isActive: boolean}) => React.ReactNode; initialIndex: number; onTap: () => void; @@ -36,13 +36,18 @@ type AttachmentCarouselPagerProps = { }; function AttachmentCarouselPager( - {items, scrollEnabled = true, renderItem, initialIndex, onTap, onPageSelected, onScaleChanged}: AttachmentCarouselPagerProps, + {items, isZoomedOut = true, renderItem, initialIndex, onTap, onPageSelected, onScaleChanged}: AttachmentCarouselPagerProps, ref: ForwardedRef, ) { const styles = useThemeStyles(); const pagerRef = useRef(null); - const isPagerSwiping = useSharedValue(false); + const isPagerScrolling = useSharedValue(false); + const [isScrollEnabled, setScrollEnabled] = useState(isZoomedOut); + useEffect(() => { + setScrollEnabled(isZoomedOut); + }, [isZoomedOut]); + const activePage = useSharedValue(initialIndex); const [activePageState, setActivePageState] = useState(initialIndex); @@ -52,7 +57,7 @@ function AttachmentCarouselPager( 'worklet'; activePage.value = e.position; - isPagerSwiping.value = e.offset !== 0; + isPagerScrolling.value = e.offset !== 0; }, }, [], @@ -66,12 +71,13 @@ function AttachmentCarouselPager( const contextValue = useMemo( () => ({ pagerRef, - isPagerSwiping, - scrollEnabled, + isPagerScrolling, + isScrollEnabled, + setScrollEnabled, onTap, onScaleChanged, }), - [isPagerSwiping, scrollEnabled, onTap, onScaleChanged], + [isPagerScrolling, isScrollEnabled, onTap, onScaleChanged], ); useImperativeHandle( @@ -87,9 +93,9 @@ function AttachmentCarouselPager( return ( { diff --git a/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.js b/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.js index 1333f641aee9..99f726bc6032 100644 --- a/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.js +++ b/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.js @@ -16,7 +16,7 @@ function BaseAttachmentViewPdf({ style, }) { const attachmentCarouselPagerContext = useContext(AttachmentCarouselPagerContext); - const scrollEnabled = attachmentCarouselPagerContext === null ? 1 : attachmentCarouselPagerContext.scrollEnabled; + const isScrollEnabled = attachmentCarouselPagerContext === null ? true : attachmentCarouselPagerContext.isScrollEnabled; useEffect(() => { if (!attachmentCarouselPagerContext) { @@ -43,11 +43,11 @@ function BaseAttachmentViewPdf({ if (onPressProp !== undefined) { onPressProp(e); } - if (attachmentCarouselPagerContext !== null && scrollEnabled) { + if (attachmentCarouselPagerContext !== null && isScrollEnabled) { attachmentCarouselPagerContext.onTap(e); } }, - [attachmentCarouselPagerContext, scrollEnabled, onPressProp], + [attachmentCarouselPagerContext, isScrollEnabled, onPressProp], ); return ( diff --git a/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.android.js b/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.android.js index 53e5c1ff9ddd..629a3dd19ecf 100644 --- a/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.android.js +++ b/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.android.js @@ -22,19 +22,21 @@ function AttachmentViewPdf(props) { // frozen, which combined with Reanimated using strict mode since 3.6.0 was resulting in errors. // Without strict mode, it would just silently fail. // See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/freeze#description - const isPdfZooming = attachmentCarouselPagerContext !== null ? attachmentCarouselPagerContext.scale !== 1 : undefined; + const isScrollEnabled = attachmentCarouselPagerContext !== null ? attachmentCarouselPagerContext.isScrollEnabled : undefined; const Pan = Gesture.Pan() .manualActivation(true) .onTouchesMove((evt) => { - if (offsetX.value !== 0 && offsetY.value !== 0 && isPdfZooming) { + if (offsetX.value !== 0 && offsetY.value !== 0 && attachmentCarouselPagerContext !== null && isScrollEnabled) { + const {setScrollEnabled} = attachmentCarouselPagerContext; + // if the value of X is greater than Y and the pdf is not zoomed in, // enable the pager scroll so that the user // can swipe to the next attachment otherwise disable it. if (Math.abs(evt.allTouches[0].absoluteX - offsetX.value) > Math.abs(evt.allTouches[0].absoluteY - offsetY.value) && scaleRef.value === 1) { - isPdfZooming.value = true; + setScrollEnabled(true); } else { - isPdfZooming.value = false; + setScrollEnabled(false); } } offsetX.value = evt.allTouches[0].absoluteX; diff --git a/src/components/MultiGestureCanvas/index.tsx b/src/components/MultiGestureCanvas/index.tsx index d8452becf01b..a7eca6baa18f 100644 --- a/src/components/MultiGestureCanvas/index.tsx +++ b/src/components/MultiGestureCanvas/index.tsx @@ -56,7 +56,7 @@ function MultiGestureCanvas({ const { onTap, onScaleChanged: onScaleChangedContext, - isPagerSwiping, + isPagerScrolling: isPagerSwiping, pagerRef, } = useMemo( () => @@ -64,7 +64,7 @@ function MultiGestureCanvas({ onTap: () => {}, onScaleChanged: () => {}, pagerRef: undefined, - isPagerSwiping: isSwipingInPagerFallback, + isPagerScrolling: isSwipingInPagerFallback, }, [attachmentCarouselPagerContext, isSwipingInPagerFallback], ); From c8770bb4ea3b9e72acb91ece7c525acd87053a6c Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Tue, 23 Jan 2024 10:29:43 +0100 Subject: [PATCH 385/580] fix: worklet error --- .../AttachmentView/AttachmentViewPdf/index.android.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.android.js b/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.android.js index 629a3dd19ecf..19719c133f1c 100644 --- a/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.android.js +++ b/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.android.js @@ -1,7 +1,7 @@ import React, {memo, useCallback, useContext} from 'react'; import {StyleSheet, View} from 'react-native'; import {Gesture, GestureDetector} from 'react-native-gesture-handler'; -import Animated, {useSharedValue} from 'react-native-reanimated'; +import Animated, {runOnJS, useSharedValue} from 'react-native-reanimated'; import AttachmentCarouselPagerContext from '@components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext'; import useThemeStyles from '@hooks/useThemeStyles'; import BaseAttachmentViewPdf from './BaseAttachmentViewPdf'; @@ -34,9 +34,9 @@ function AttachmentViewPdf(props) { // enable the pager scroll so that the user // can swipe to the next attachment otherwise disable it. if (Math.abs(evt.allTouches[0].absoluteX - offsetX.value) > Math.abs(evt.allTouches[0].absoluteY - offsetY.value) && scaleRef.value === 1) { - setScrollEnabled(true); + runOnJS(setScrollEnabled)(true); } else { - setScrollEnabled(false); + runOnJS(setScrollEnabled)(false); } } offsetX.value = evt.allTouches[0].absoluteX; From ed0c95df80bfd015cac64f7e74d0acb08e1bd691 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Tue, 23 Jan 2024 10:36:24 +0100 Subject: [PATCH 386/580] Update image typing --- src/components/ImageWithSizeCalculation.tsx | 4 ++-- .../ReportActionItem/ReportActionItemImage.tsx | 6 +++--- src/components/ThumbnailImage.tsx | 4 ++-- src/libs/ReceiptUtils.ts | 9 +++++---- src/libs/tryResolveUrlFromApiRoot.ts | 9 +++++---- 5 files changed, 17 insertions(+), 15 deletions(-) diff --git a/src/components/ImageWithSizeCalculation.tsx b/src/components/ImageWithSizeCalculation.tsx index c65faef53748..634492c8e602 100644 --- a/src/components/ImageWithSizeCalculation.tsx +++ b/src/components/ImageWithSizeCalculation.tsx @@ -1,6 +1,6 @@ import delay from 'lodash/delay'; import React, {useEffect, useRef, useState} from 'react'; -import type {StyleProp, ViewStyle} from 'react-native'; +import {ImageSourcePropType, StyleProp, ViewStyle} from 'react-native'; import {View} from 'react-native'; import useThemeStyles from '@hooks/useThemeStyles'; import Log from '@libs/Log'; @@ -19,7 +19,7 @@ type OnLoadNativeEvent = { type ImageWithSizeCalculationProps = { /** Url for image to display */ - url: string | number; + url: string | ImageSourcePropType; /** Any additional styles to apply */ style?: StyleProp; diff --git a/src/components/ReportActionItem/ReportActionItemImage.tsx b/src/components/ReportActionItem/ReportActionItemImage.tsx index d408f815e70c..d8cbc6b0afcb 100644 --- a/src/components/ReportActionItem/ReportActionItemImage.tsx +++ b/src/components/ReportActionItem/ReportActionItemImage.tsx @@ -1,7 +1,7 @@ import Str from 'expensify-common/lib/str'; import React from 'react'; import type {ReactElement} from 'react'; -import {View} from 'react-native'; +import {ImageSourcePropType, View} from 'react-native'; import AttachmentModal from '@components/AttachmentModal'; import EReceiptThumbnail from '@components/EReceiptThumbnail'; import Image from '@components/Image'; @@ -17,10 +17,10 @@ import type {Transaction} from '@src/types/onyx'; type ReportActionItemImageProps = { /** thumbnail URI for the image */ - thumbnail?: string | number | null; + thumbnail?: string | ImageSourcePropType | null; /** URI for the image or local numeric reference for the image */ - image: string | number; + image: string | ImageSourcePropType; /** whether or not to enable the image preview modal */ enablePreviewModal?: boolean; diff --git a/src/components/ThumbnailImage.tsx b/src/components/ThumbnailImage.tsx index a1778e2feaee..753c4b783b9c 100644 --- a/src/components/ThumbnailImage.tsx +++ b/src/components/ThumbnailImage.tsx @@ -1,6 +1,6 @@ import lodashClamp from 'lodash/clamp'; import React, {useCallback, useState} from 'react'; -import type {StyleProp, ViewStyle} from 'react-native'; +import {ImageSourcePropType, StyleProp, ViewStyle} from 'react-native'; import {Dimensions, View} from 'react-native'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -10,7 +10,7 @@ import ImageWithSizeCalculation from './ImageWithSizeCalculation'; type ThumbnailImageProps = { /** Source URL for the preview image */ - previewSourceURL: string | number; + previewSourceURL: string | ImageSourcePropType; /** Any additional styles to apply */ style?: StyleProp; diff --git a/src/libs/ReceiptUtils.ts b/src/libs/ReceiptUtils.ts index 9c6f10c12935..b1b21b7a3e27 100644 --- a/src/libs/ReceiptUtils.ts +++ b/src/libs/ReceiptUtils.ts @@ -1,4 +1,5 @@ import Str from 'expensify-common/lib/str'; +import {ImageSourcePropType} from 'react-native'; import ReceiptDoc from '@assets/images/receipt-doc.png'; import ReceiptGeneric from '@assets/images/receipt-generic.png'; import ReceiptHTML from '@assets/images/receipt-html.png'; @@ -9,8 +10,8 @@ import type {Transaction} from '@src/types/onyx'; import * as FileUtils from './fileDownload/FileUtils'; type ThumbnailAndImageURI = { - image: number | string; - thumbnail: number | string | null; + image: ImageSourcePropType | string; + thumbnail: ImageSourcePropType | string | null; transaction?: Transaction; isLocalFile?: boolean; }; @@ -29,7 +30,7 @@ type FileNameAndExtension = { */ function getThumbnailAndImageURIs(transaction: Transaction, receiptPath: string | null = null, receiptFileName: string | null = null): ThumbnailAndImageURI { if (Object.hasOwn(transaction?.pendingFields ?? {}, 'waypoints')) { - return {thumbnail: null, image: ReceiptGeneric as string | number, isLocalFile: true}; + return {thumbnail: null, image: ReceiptGeneric, isLocalFile: true}; } // URI to image, i.e. blob:new.expensify.com/9ef3a018-4067-47c6-b29f-5f1bd35f213d or expensify.com/receipts/w_e616108497ef940b7210ec6beb5a462d01a878f4.jpg @@ -67,7 +68,7 @@ function getThumbnailAndImageURIs(transaction: Transaction, receiptPath: string } const isLocalFile = typeof path === 'number' || path.startsWith('blob:') || path.startsWith('file:') || path.startsWith('/'); - return {thumbnail: image as string | number, image: path, isLocalFile}; + return {thumbnail: image, image: path, isLocalFile}; } // eslint-disable-next-line import/prefer-default-export diff --git a/src/libs/tryResolveUrlFromApiRoot.ts b/src/libs/tryResolveUrlFromApiRoot.ts index adf717d500be..c5503bc9476c 100644 --- a/src/libs/tryResolveUrlFromApiRoot.ts +++ b/src/libs/tryResolveUrlFromApiRoot.ts @@ -1,3 +1,4 @@ +import {ImageSourcePropType} from 'react-native'; import Config from '@src/CONFIG'; import type {Request} from '@src/types/onyx'; import * as ApiUtils from './ApiUtils'; @@ -18,12 +19,12 @@ const ORIGIN_PATTERN = new RegExp(`^(${ORIGINS_TO_REPLACE.join('|')})`); * - Unmatched URLs (non expensify) are returned with no modifications */ function tryResolveUrlFromApiRoot(url: string): string; -function tryResolveUrlFromApiRoot(url: number): number; -function tryResolveUrlFromApiRoot(url: string | number): string | number; -function tryResolveUrlFromApiRoot(url: string | number): string | number { +function tryResolveUrlFromApiRoot(url: ImageSourcePropType): number; +function tryResolveUrlFromApiRoot(url: string | ImageSourcePropType): string | ImageSourcePropType; +function tryResolveUrlFromApiRoot(url: string | ImageSourcePropType): string | ImageSourcePropType { // in native, when we import an image asset, it will have a number representation which can be used in `source` of Image // in this case we can skip the url resolving - if (typeof url === 'number') { + if (typeof url !== 'string') { return url; } const apiRoot = ApiUtils.getApiRoot({shouldUseSecure: false} as Request); From 4fcb65b0889ff76327038c2cc6ae86c56196286c Mon Sep 17 00:00:00 2001 From: VickyStash Date: Tue, 23 Jan 2024 10:40:36 +0100 Subject: [PATCH 387/580] Fix lint error --- src/components/ImageWithSizeCalculation.tsx | 2 +- src/components/ReportActionItem/ReportActionItemImage.tsx | 3 ++- src/components/ThumbnailImage.tsx | 2 +- src/libs/ReceiptUtils.ts | 2 +- src/libs/tryResolveUrlFromApiRoot.ts | 2 +- 5 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/components/ImageWithSizeCalculation.tsx b/src/components/ImageWithSizeCalculation.tsx index 634492c8e602..d0559327274a 100644 --- a/src/components/ImageWithSizeCalculation.tsx +++ b/src/components/ImageWithSizeCalculation.tsx @@ -1,6 +1,6 @@ import delay from 'lodash/delay'; import React, {useEffect, useRef, useState} from 'react'; -import {ImageSourcePropType, StyleProp, ViewStyle} from 'react-native'; +import type {ImageSourcePropType, StyleProp, ViewStyle} from 'react-native'; import {View} from 'react-native'; import useThemeStyles from '@hooks/useThemeStyles'; import Log from '@libs/Log'; diff --git a/src/components/ReportActionItem/ReportActionItemImage.tsx b/src/components/ReportActionItem/ReportActionItemImage.tsx index d8cbc6b0afcb..710adeb1589e 100644 --- a/src/components/ReportActionItem/ReportActionItemImage.tsx +++ b/src/components/ReportActionItem/ReportActionItemImage.tsx @@ -1,7 +1,8 @@ import Str from 'expensify-common/lib/str'; import React from 'react'; import type {ReactElement} from 'react'; -import {ImageSourcePropType, View} from 'react-native'; +import {View} from 'react-native'; +import type {ImageSourcePropType} from 'react-native'; import AttachmentModal from '@components/AttachmentModal'; import EReceiptThumbnail from '@components/EReceiptThumbnail'; import Image from '@components/Image'; diff --git a/src/components/ThumbnailImage.tsx b/src/components/ThumbnailImage.tsx index 753c4b783b9c..5950bae5205c 100644 --- a/src/components/ThumbnailImage.tsx +++ b/src/components/ThumbnailImage.tsx @@ -1,6 +1,6 @@ import lodashClamp from 'lodash/clamp'; import React, {useCallback, useState} from 'react'; -import {ImageSourcePropType, StyleProp, ViewStyle} from 'react-native'; +import type {ImageSourcePropType, StyleProp, ViewStyle} from 'react-native'; import {Dimensions, View} from 'react-native'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; diff --git a/src/libs/ReceiptUtils.ts b/src/libs/ReceiptUtils.ts index b1b21b7a3e27..52f64b2defb1 100644 --- a/src/libs/ReceiptUtils.ts +++ b/src/libs/ReceiptUtils.ts @@ -1,5 +1,5 @@ import Str from 'expensify-common/lib/str'; -import {ImageSourcePropType} from 'react-native'; +import type {ImageSourcePropType} from 'react-native'; import ReceiptDoc from '@assets/images/receipt-doc.png'; import ReceiptGeneric from '@assets/images/receipt-generic.png'; import ReceiptHTML from '@assets/images/receipt-html.png'; diff --git a/src/libs/tryResolveUrlFromApiRoot.ts b/src/libs/tryResolveUrlFromApiRoot.ts index c5503bc9476c..8eb5bdba0129 100644 --- a/src/libs/tryResolveUrlFromApiRoot.ts +++ b/src/libs/tryResolveUrlFromApiRoot.ts @@ -1,4 +1,4 @@ -import {ImageSourcePropType} from 'react-native'; +import type {ImageSourcePropType} from 'react-native'; import Config from '@src/CONFIG'; import type {Request} from '@src/types/onyx'; import * as ApiUtils from './ApiUtils'; From 7e2f4b69f1ba1b98d70a99557c05aadcdd6063c5 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Tue, 23 Jan 2024 10:43:46 +0100 Subject: [PATCH 388/580] Cast to string[] --- src/libs/Middleware/SaveResponseInOnyx.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/Middleware/SaveResponseInOnyx.ts b/src/libs/Middleware/SaveResponseInOnyx.ts index f7b37ab66bf5..8e357b0f2251 100644 --- a/src/libs/Middleware/SaveResponseInOnyx.ts +++ b/src/libs/Middleware/SaveResponseInOnyx.ts @@ -7,7 +7,7 @@ import type Middleware from './types'; // If we're executing any of these requests, we don't need to trigger our OnyxUpdates flow to update the current data even if our current value is out of // date because all these requests are updating the app to the most current state. -const requestsToIgnoreLastUpdateID = [READ_COMMANDS.OPEN_APP, SIDE_EFFECT_REQUEST_COMMANDS.RECONNECT_APP, SIDE_EFFECT_REQUEST_COMMANDS.GET_MISSING_ONYX_MESSAGES]; +const requestsToIgnoreLastUpdateID: string[] = [READ_COMMANDS.OPEN_APP, SIDE_EFFECT_REQUEST_COMMANDS.RECONNECT_APP, SIDE_EFFECT_REQUEST_COMMANDS.GET_MISSING_ONYX_MESSAGES]; const SaveResponseInOnyx: Middleware = (requestResponse, request) => requestResponse.then((response = {}) => { From 3d1cc81a1bda976c9fd072288385b4d9f8a33227 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Tue, 23 Jan 2024 10:45:56 +0100 Subject: [PATCH 389/580] Create Policy API params --- .../parameters/AddMembersToWorkspaceParams.ts | 8 ++++++++ .../API/parameters/CreateWorkspaceParams.ts | 17 +++++++++++++++++ .../DeleteMembersFromWorkspaceParams.ts | 6 ++++++ .../parameters/DeleteWorkspaceAvatarParams.ts | 5 +++++ .../API/parameters/DeleteWorkspaceParams.ts | 5 +++++ .../OpenDraftWorkspaceRequestParams.ts | 5 +++++ .../parameters/OpenWorkspaceInvitePageParams.ts | 6 ++++++ .../OpenWorkspaceMembersPageParams.ts | 6 ++++++ src/libs/API/parameters/OpenWorkspaceParams.ts | 6 ++++++ .../OpenWorkspaceReimburseViewParams.ts | 5 +++++ .../parameters/UpdateWorkspaceAvatarParams.ts | 6 ++++++ .../UpdateWorkspaceGeneralSettingsParams.ts | 7 +++++++ 12 files changed, 82 insertions(+) create mode 100644 src/libs/API/parameters/AddMembersToWorkspaceParams.ts create mode 100644 src/libs/API/parameters/CreateWorkspaceParams.ts create mode 100644 src/libs/API/parameters/DeleteMembersFromWorkspaceParams.ts create mode 100644 src/libs/API/parameters/DeleteWorkspaceAvatarParams.ts create mode 100644 src/libs/API/parameters/DeleteWorkspaceParams.ts create mode 100644 src/libs/API/parameters/OpenDraftWorkspaceRequestParams.ts create mode 100644 src/libs/API/parameters/OpenWorkspaceInvitePageParams.ts create mode 100644 src/libs/API/parameters/OpenWorkspaceMembersPageParams.ts create mode 100644 src/libs/API/parameters/OpenWorkspaceParams.ts create mode 100644 src/libs/API/parameters/OpenWorkspaceReimburseViewParams.ts create mode 100644 src/libs/API/parameters/UpdateWorkspaceAvatarParams.ts create mode 100644 src/libs/API/parameters/UpdateWorkspaceGeneralSettingsParams.ts diff --git a/src/libs/API/parameters/AddMembersToWorkspaceParams.ts b/src/libs/API/parameters/AddMembersToWorkspaceParams.ts new file mode 100644 index 000000000000..4e96fd07d301 --- /dev/null +++ b/src/libs/API/parameters/AddMembersToWorkspaceParams.ts @@ -0,0 +1,8 @@ +type AddMembersToWorkspaceParams = { + employees: string; + welcomeNote: string; + policyID: string; + reportCreationData?: string; +}; + +export default AddMembersToWorkspaceParams; diff --git a/src/libs/API/parameters/CreateWorkspaceParams.ts b/src/libs/API/parameters/CreateWorkspaceParams.ts new file mode 100644 index 000000000000..c86598b48953 --- /dev/null +++ b/src/libs/API/parameters/CreateWorkspaceParams.ts @@ -0,0 +1,17 @@ +type CreateWorkspaceParams = { + policyID: string; + announceChatReportID: string; + adminsChatReportID: string; + expenseChatReportID: string; + ownerEmail: string; + makeMeAdmin: boolean; + policyName: string; + type: string; + announceCreatedReportActionID: string; + adminsCreatedReportActionID: string; + expenseCreatedReportActionID: string; + customUnitID: string; + customUnitRateID: string; +}; + +export default CreateWorkspaceParams; diff --git a/src/libs/API/parameters/DeleteMembersFromWorkspaceParams.ts b/src/libs/API/parameters/DeleteMembersFromWorkspaceParams.ts new file mode 100644 index 000000000000..6566d2b917b4 --- /dev/null +++ b/src/libs/API/parameters/DeleteMembersFromWorkspaceParams.ts @@ -0,0 +1,6 @@ +type DeleteMembersFromWorkspaceParams = { + emailList: string; + policyID: string; +}; + +export default DeleteMembersFromWorkspaceParams; diff --git a/src/libs/API/parameters/DeleteWorkspaceAvatarParams.ts b/src/libs/API/parameters/DeleteWorkspaceAvatarParams.ts new file mode 100644 index 000000000000..1e0c26fbb49c --- /dev/null +++ b/src/libs/API/parameters/DeleteWorkspaceAvatarParams.ts @@ -0,0 +1,5 @@ +type DeleteWorkspaceAvatarParams = { + policyID: string; +}; + +export default DeleteWorkspaceAvatarParams; diff --git a/src/libs/API/parameters/DeleteWorkspaceParams.ts b/src/libs/API/parameters/DeleteWorkspaceParams.ts new file mode 100644 index 000000000000..c535e8123d25 --- /dev/null +++ b/src/libs/API/parameters/DeleteWorkspaceParams.ts @@ -0,0 +1,5 @@ +type DeleteWorkspaceParams = { + policyID: string; +}; + +export default DeleteWorkspaceParams; diff --git a/src/libs/API/parameters/OpenDraftWorkspaceRequestParams.ts b/src/libs/API/parameters/OpenDraftWorkspaceRequestParams.ts new file mode 100644 index 000000000000..b9f098da9dae --- /dev/null +++ b/src/libs/API/parameters/OpenDraftWorkspaceRequestParams.ts @@ -0,0 +1,5 @@ +type OpenDraftWorkspaceRequestParams = { + policyID: string; +}; + +export default OpenDraftWorkspaceRequestParams; diff --git a/src/libs/API/parameters/OpenWorkspaceInvitePageParams.ts b/src/libs/API/parameters/OpenWorkspaceInvitePageParams.ts new file mode 100644 index 000000000000..0b622bee75b7 --- /dev/null +++ b/src/libs/API/parameters/OpenWorkspaceInvitePageParams.ts @@ -0,0 +1,6 @@ +type OpenWorkspaceInvitePageParams = { + policyID: string; + clientMemberEmails: string; +}; + +export default OpenWorkspaceInvitePageParams; diff --git a/src/libs/API/parameters/OpenWorkspaceMembersPageParams.ts b/src/libs/API/parameters/OpenWorkspaceMembersPageParams.ts new file mode 100644 index 000000000000..2dab31ac356b --- /dev/null +++ b/src/libs/API/parameters/OpenWorkspaceMembersPageParams.ts @@ -0,0 +1,6 @@ +type OpenWorkspaceMembersPageParams = { + policyID: string; + clientMemberEmails: string; +}; + +export default OpenWorkspaceMembersPageParams; diff --git a/src/libs/API/parameters/OpenWorkspaceParams.ts b/src/libs/API/parameters/OpenWorkspaceParams.ts new file mode 100644 index 000000000000..3ea0d4b3dabe --- /dev/null +++ b/src/libs/API/parameters/OpenWorkspaceParams.ts @@ -0,0 +1,6 @@ +type OpenWorkspaceParams = { + policyID: string; + clientMemberAccountIDs: string; +}; + +export default OpenWorkspaceParams; diff --git a/src/libs/API/parameters/OpenWorkspaceReimburseViewParams.ts b/src/libs/API/parameters/OpenWorkspaceReimburseViewParams.ts new file mode 100644 index 000000000000..317241c8842f --- /dev/null +++ b/src/libs/API/parameters/OpenWorkspaceReimburseViewParams.ts @@ -0,0 +1,5 @@ +type OpenWorkspaceReimburseViewParams = { + policyID: string; +}; + +export default OpenWorkspaceReimburseViewParams; diff --git a/src/libs/API/parameters/UpdateWorkspaceAvatarParams.ts b/src/libs/API/parameters/UpdateWorkspaceAvatarParams.ts new file mode 100644 index 000000000000..a4c1edf83dab --- /dev/null +++ b/src/libs/API/parameters/UpdateWorkspaceAvatarParams.ts @@ -0,0 +1,6 @@ +type UpdateWorkspaceAvatarParams = { + policyID: string; + file: File; +}; + +export default UpdateWorkspaceAvatarParams; diff --git a/src/libs/API/parameters/UpdateWorkspaceGeneralSettingsParams.ts b/src/libs/API/parameters/UpdateWorkspaceGeneralSettingsParams.ts new file mode 100644 index 000000000000..9aeb4be97a43 --- /dev/null +++ b/src/libs/API/parameters/UpdateWorkspaceGeneralSettingsParams.ts @@ -0,0 +1,7 @@ +type UpdateWorkspaceGeneralSettingsParams = { + policyID: string; + workspaceName: string; + currency: string; +}; + +export default UpdateWorkspaceGeneralSettingsParams; From 033aaa1c4430e24c5892d01639200b14bf53b75e Mon Sep 17 00:00:00 2001 From: VickyStash Date: Tue, 23 Jan 2024 10:51:15 +0100 Subject: [PATCH 390/580] Fix lint error --- src/components/ReportActionItem/ReportActionItemImages.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ReportActionItem/ReportActionItemImages.tsx b/src/components/ReportActionItem/ReportActionItemImages.tsx index 06edea95fb89..00b91bf4f862 100644 --- a/src/components/ReportActionItem/ReportActionItemImages.tsx +++ b/src/components/ReportActionItem/ReportActionItemImages.tsx @@ -72,7 +72,7 @@ function ReportActionItemImages({images, size, total, isHovered = false}: Report const borderStyle = shouldShowBorder ? styles.reportActionItemImageBorder : {}; return ( Date: Tue, 23 Jan 2024 11:03:53 +0100 Subject: [PATCH 391/580] Add missing policy params --- .../CreateWorkspaceFromIOUPaymentParams.ts | 20 +++++++++++++++++++ .../UpdateWorkspaceCustomUnitAndRateParams.ts | 8 ++++++++ 2 files changed, 28 insertions(+) create mode 100644 src/libs/API/parameters/CreateWorkspaceFromIOUPaymentParams.ts create mode 100644 src/libs/API/parameters/UpdateWorkspaceCustomUnitAndRateParams.ts diff --git a/src/libs/API/parameters/CreateWorkspaceFromIOUPaymentParams.ts b/src/libs/API/parameters/CreateWorkspaceFromIOUPaymentParams.ts new file mode 100644 index 000000000000..99caf7b6bc3d --- /dev/null +++ b/src/libs/API/parameters/CreateWorkspaceFromIOUPaymentParams.ts @@ -0,0 +1,20 @@ +type CreateWorkspaceFromIOUPaymentParams = { + policyID: string; + announceChatReportID: string; + adminsChatReportID: string; + expenseChatReportID: string; + ownerEmail: string; + makeMeAdmin: boolean; + policyName: string; + type: string; + announceCreatedReportActionID: string; + adminsCreatedReportActionID: string; + expenseCreatedReportActionID: string; + customUnitID: string; + customUnitRateID: string; + iouReportID: string; + memberData: string; + reportActionID: string; +}; + +export default CreateWorkspaceFromIOUPaymentParams; \ No newline at end of file diff --git a/src/libs/API/parameters/UpdateWorkspaceCustomUnitAndRateParams.ts b/src/libs/API/parameters/UpdateWorkspaceCustomUnitAndRateParams.ts new file mode 100644 index 000000000000..22bbd20c7308 --- /dev/null +++ b/src/libs/API/parameters/UpdateWorkspaceCustomUnitAndRateParams.ts @@ -0,0 +1,8 @@ +type UpdateWorkspaceCustomUnitAndRateParams = { + policyID: string; + lastModified: number; + customUnit: string; + customUnitRate: string; +}; + +export default UpdateWorkspaceCustomUnitAndRateParams; From 263b558d31e36e332c04125ecfdfd2d09356d898 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Tue, 23 Jan 2024 11:04:10 +0100 Subject: [PATCH 392/580] Update api mapping after adding Policy --- src/libs/API/parameters/index.ts | 14 ++++++++++++++ src/libs/API/types.ts | 28 ++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index a0a57b1b65d3..e640554bd515 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -100,3 +100,17 @@ export type {default as VerifyIdentityParams} from './VerifyIdentityParams'; export type {default as AcceptWalletTermsParams} from './AcceptWalletTermsParams'; export type {default as ChronosRemoveOOOEventParams} from './ChronosRemoveOOOEventParams'; export type {default as TransferWalletBalanceParams} from './TransferWalletBalanceParams'; +export type {default as DeleteWorkspaceParams} from './DeleteWorkspaceParams'; +export type {default as CreateWorkspaceParams} from './CreateWorkspaceParams'; +export type {default as UpdateWorkspaceGeneralSettingsParams} from './UpdateWorkspaceGeneralSettingsParams'; +export type {default as DeleteWorkspaceAvatarParams} from './DeleteWorkspaceAvatarParams'; +export type {default as UpdateWorkspaceAvatarParams} from './UpdateWorkspaceAvatarParams'; +export type {default as AddMembersToWorkspaceParams} from './AddMembersToWorkspaceParams'; +export type {default as DeleteMembersFromWorkspaceParams} from './DeleteMembersFromWorkspaceParams'; +export type {default as OpenWorkspaceParams} from './OpenWorkspaceParams'; +export type {default as OpenWorkspaceReimburseViewParams} from './OpenWorkspaceReimburseViewParams'; +export type {default as OpenWorkspaceInvitePageParams} from './OpenWorkspaceInvitePageParams'; +export type {default as OpenWorkspaceMembersPageParams} from './OpenWorkspaceMembersPageParams'; +export type {default as OpenDraftWorkspaceRequestParams} from './OpenDraftWorkspaceRequestParams'; +export type {default as UpdateWorkspaceCustomUnitAndRateParams} from './UpdateWorkspaceCustomUnitAndRateParams'; +export type {default as CreateWorkspaceFromIOUPaymentParams} from './CreateWorkspaceFromIOUPaymentParams'; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index cd72efa89f22..e4156a2a582e 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -96,6 +96,15 @@ const WRITE_COMMANDS = { FLAG_COMMENT: 'FlagComment', UPDATE_REPORT_PRIVATE_NOTE: 'UpdateReportPrivateNote', RESOLVE_ACTIONABLE_MENTION_WHISPER: 'ResolveActionableMentionWhisper', + DELETE_WORKSPACE: 'DeleteWorkspace', + DELETE_MEMBERS_FROM_WORKSPACE: 'DeleteMembersFromWorkspace', + ADD_MEMBERS_TO_WORKSPACE: 'AddMembersToWorkspace', + UPDATE_WORKSPACE_AVATAR: 'UpdateWorkspaceAvatar', + DELETE_WORKSPACE_AVATAR: 'DeleteWorkspaceAvatar', + UPDATE_WORKSPACE_GENERAL_SETTINGS: 'UpdateWorkspaceGeneralSettings', + UPDATE_WORKSPACE_CUSTOM_UNIT_AND_RATE: 'UpdateWorkspaceCustomUnitAndRate', + CREATE_WORKSPACE: 'CreateWorkspace', + CREATE_WORKSPACE_FROM_IOU_PAYMENT: 'CreateWorkspaceFromIOUPayment', } as const; type WriteCommand = ValueOf; @@ -189,6 +198,15 @@ type WriteCommandParameters = { [WRITE_COMMANDS.RESOLVE_ACTIONABLE_MENTION_WHISPER]: Parameters.ResolveActionableMentionWhisperParams; [WRITE_COMMANDS.CHRONOS_REMOVE_OOO_EVENT]: Parameters.ChronosRemoveOOOEventParams; [WRITE_COMMANDS.TRANSFER_WALLET_BALANCE]: Parameters.TransferWalletBalanceParams; + [WRITE_COMMANDS.DELETE_WORKSPACE]: Parameters.DeleteWorkspaceParams; + [WRITE_COMMANDS.DELETE_MEMBERS_FROM_WORKSPACE]: Parameters.DeleteMembersFromWorkspaceParams; + [WRITE_COMMANDS.ADD_MEMBERS_TO_WORKSPACE]: Parameters.AddMembersToWorkspaceParams; + [WRITE_COMMANDS.UPDATE_WORKSPACE_AVATAR]: Parameters.UpdateWorkspaceAvatarParams; + [WRITE_COMMANDS.DELETE_WORKSPACE_AVATAR]: Parameters.DeleteWorkspaceAvatarParams; + [WRITE_COMMANDS.UPDATE_WORKSPACE_GENERAL_SETTINGS]: Parameters.UpdateWorkspaceGeneralSettingsParams; + [WRITE_COMMANDS.UPDATE_WORKSPACE_CUSTOM_UNIT_AND_RATE]: Parameters.UpdateWorkspaceCustomUnitAndRateParams; + [WRITE_COMMANDS.CREATE_WORKSPACE]: Parameters.CreateWorkspaceParams; + [WRITE_COMMANDS.CREATE_WORKSPACE_FROM_IOU_PAYMENT]: Parameters.CreateWorkspaceFromIOUPaymentParams; }; const READ_COMMANDS = { @@ -216,6 +234,11 @@ const READ_COMMANDS = { OPEN_ENABLE_PAYMENTS_PAGE: 'OpenEnablePaymentsPage', BEGIN_SIGNIN: 'BeginSignIn', SIGN_IN_WITH_SHORT_LIVED_AUTH_TOKEN: 'SignInWithShortLivedAuthToken', + OPEN_WORKSPACE_REIMBURSE_VIEW: 'OpenWorkspaceReimburseView', + OPEN_WORKSPACE: 'OpenWorkspace', + OPEN_WORKSPACE_MEMBERS_PAGE: 'OpenWorkspaceMembersPage', + OPEN_WORKSPACE_INVITE_PAGE: 'OpenWorkspaceInvitePage', + OPEN_DRAFT_WORKSPACE_REQUEST: 'OpenDraftWorkspaceRequest', } as const; type ReadCommand = ValueOf; @@ -245,6 +268,11 @@ type ReadCommandParameters = { [READ_COMMANDS.OPEN_ENABLE_PAYMENTS_PAGE]: EmptyObject; [READ_COMMANDS.BEGIN_SIGNIN]: Parameters.BeginSignInParams; [READ_COMMANDS.SIGN_IN_WITH_SHORT_LIVED_AUTH_TOKEN]: Parameters.SignInWithShortLivedAuthTokenParams; + [READ_COMMANDS.OPEN_WORKSPACE_REIMBURSE_VIEW]: Parameters.OpenWorkspaceReimburseViewParams; + [READ_COMMANDS.OPEN_WORKSPACE]: Parameters.OpenWorkspaceParams; + [READ_COMMANDS.OPEN_WORKSPACE_MEMBERS_PAGE]: Parameters.OpenWorkspaceMembersPageParams; + [READ_COMMANDS.OPEN_WORKSPACE_INVITE_PAGE]: Parameters.OpenWorkspaceInvitePageParams; + [READ_COMMANDS.OPEN_DRAFT_WORKSPACE_REQUEST]: Parameters.OpenDraftWorkspaceRequestParams; }; const SIDE_EFFECT_REQUEST_COMMANDS = { From d9a1330ead395ba89b8c1b68eb976ce72725748d Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Tue, 23 Jan 2024 11:04:31 +0100 Subject: [PATCH 393/580] Import Policy params in Policy.ts --- src/libs/actions/Policy.ts | 145 +++++++++---------------------------- 1 file changed, 33 insertions(+), 112 deletions(-) diff --git a/src/libs/actions/Policy.ts b/src/libs/actions/Policy.ts index cbbc00dd42fc..73f8a410f12d 100644 --- a/src/libs/actions/Policy.ts +++ b/src/libs/actions/Policy.ts @@ -8,6 +8,23 @@ import type {OnyxCollection, OnyxUpdate} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import type {NullishDeep, OnyxEntry} from 'react-native-onyx/lib/types'; import * as API from '@libs/API'; +import type { + AddMembersToWorkspaceParams, + CreateWorkspaceFromIOUPaymentParams, + CreateWorkspaceParams, + DeleteMembersFromWorkspaceParams, + DeleteWorkspaceAvatarParams, + DeleteWorkspaceParams, + OpenDraftWorkspaceRequestParams, + OpenWorkspaceInvitePageParams, + OpenWorkspaceMembersPageParams, + OpenWorkspaceParams, + OpenWorkspaceReimburseViewParams, + UpdateWorkspaceAvatarParams, + UpdateWorkspaceCustomUnitAndRateParams, + UpdateWorkspaceGeneralSettingsParams, +} from '@libs/API/parameters'; +import {READ_COMMANDS, WRITE_COMMANDS} from '@libs/API/types'; import DateUtils from '@libs/DateUtils'; import * as ErrorUtils from '@libs/ErrorUtils'; import Log from '@libs/Log'; @@ -275,13 +292,9 @@ function deleteWorkspace(policyID: string, reports: Report[], policyName: string }); }); - type DeleteWorkspaceParams = { - policyID: string; - }; - const params: DeleteWorkspaceParams = {policyID}; - API.write('DeleteWorkspace', params, {optimisticData, failureData}); + API.write(WRITE_COMMANDS.DELETE_WORKSPACE, params, {optimisticData, failureData}); // Reset the lastAccessedWorkspacePolicyID if (policyID === lastAccessedWorkspacePolicyID) { @@ -491,17 +504,12 @@ function removeMembers(accountIDs: number[], policyID: string) { }); }); - type DeleteMembersFromWorkspaceParams = { - emailList: string; - policyID: string; - }; - const params: DeleteMembersFromWorkspaceParams = { emailList: accountIDs.map((accountID) => allPersonalDetails?.[accountID]?.login).join(','), policyID, }; - API.write('DeleteMembersFromWorkspace', params, {optimisticData, successData, failureData}); + API.write(WRITE_COMMANDS.DELETE_MEMBERS_FROM_WORKSPACE, params, {optimisticData, successData, failureData}); } /** @@ -668,13 +676,6 @@ function addMembersToWorkspace(invitedEmailsToAccountIDs: Record ...announceRoomMembers.onyxFailureData, ]; - type AddMembersToWorkspaceParams = { - employees: string; - welcomeNote: string; - policyID: string; - reportCreationData?: string; - }; - const params: AddMembersToWorkspaceParams = { employees: JSON.stringify(logins.map((login) => ({email: login}))), welcomeNote: new ExpensiMark().replace(welcomeNote), @@ -683,7 +684,7 @@ function addMembersToWorkspace(invitedEmailsToAccountIDs: Record if (!isEmptyObject(membersChats.reportCreationData)) { params.reportCreationData = JSON.stringify(membersChats.reportCreationData); } - API.write('AddMembersToWorkspace', params, {optimisticData, successData, failureData}); + API.write(WRITE_COMMANDS.ADD_MEMBERS_TO_WORKSPACE, params, {optimisticData, successData, failureData}); } /** @@ -727,17 +728,12 @@ function updateWorkspaceAvatar(policyID: string, file: File) { }, ]; - type UpdateWorkspaceAvatarParams = { - policyID: string; - file: File; - }; - const params: UpdateWorkspaceAvatarParams = { policyID, file, }; - API.write('UpdateWorkspaceAvatar', params, {optimisticData, finallyData, failureData}); + API.write(WRITE_COMMANDS.UPDATE_WORKSPACE_AVATAR, params, {optimisticData, finallyData, failureData}); } /** @@ -782,13 +778,9 @@ function deleteWorkspaceAvatar(policyID: string) { }, ]; - type DeleteWorkspaceAvatarParams = { - policyID: string; - }; - const params: DeleteWorkspaceAvatarParams = {policyID}; - API.write('DeleteWorkspaceAvatar', params, {optimisticData, finallyData, failureData}); + API.write(WRITE_COMMANDS.DELETE_WORKSPACE_AVATAR, params, {optimisticData, finallyData, failureData}); } /** @@ -885,19 +877,13 @@ function updateGeneralSettings(policyID: string, name: string, currency: string) }, ]; - type UpdateWorkspaceGeneralSettingsParams = { - policyID: string; - workspaceName: string; - currency: string; - }; - const params: UpdateWorkspaceGeneralSettingsParams = { policyID, workspaceName: name, currency, }; - API.write('UpdateWorkspaceGeneralSettings', params, { + API.write(WRITE_COMMANDS.UPDATE_WORKSPACE_GENERAL_SETTINGS, params, { optimisticData, finallyData, failureData, @@ -1017,21 +1003,14 @@ function updateWorkspaceCustomUnitAndRate(policyID: string, currentCustomUnit: C const {pendingAction, errors, ...newRates} = newCustomUnitParam.rates ?? {}; newCustomUnitParam.rates = newRates; - type UpdateWorkspaceCustomUnitAndRate = { - policyID: string; - lastModified: number; - customUnit: string; - customUnitRate: string; - }; - - const params: UpdateWorkspaceCustomUnitAndRate = { + const params: UpdateWorkspaceCustomUnitAndRateParams = { policyID, lastModified, customUnit: JSON.stringify(newCustomUnitParam), customUnitRate: JSON.stringify(newCustomUnitParam.rates), }; - API.write('UpdateWorkspaceCustomUnitAndRate', params, {optimisticData, successData, failureData}); + API.write(WRITE_COMMANDS.UPDATE_WORKSPACE_CUSTOM_UNIT_AND_RATE, params, {optimisticData, successData, failureData}); } /** @@ -1416,22 +1395,6 @@ function createWorkspace(policyOwnerEmail = '', makeMeAdmin = false, policyName }, ]; - type CreateWorkspaceParams = { - policyID: string; - announceChatReportID: string; - adminsChatReportID: string; - expenseChatReportID: string; - ownerEmail: string; - makeMeAdmin: boolean; - policyName: string; - type: string; - announceCreatedReportActionID: string; - adminsCreatedReportActionID: string; - expenseCreatedReportActionID: string; - customUnitID: string; - customUnitRateID: string; - }; - const params: CreateWorkspaceParams = { policyID, announceChatReportID, @@ -1448,7 +1411,7 @@ function createWorkspace(policyOwnerEmail = '', makeMeAdmin = false, policyName customUnitRateID, }; - API.write('CreateWorkspace', params, {optimisticData, successData, failureData}); + API.write(WRITE_COMMANDS.CREATE_WORKSPACE, params, {optimisticData, successData, failureData}); return adminsChatReportID; } @@ -1479,13 +1442,9 @@ function openWorkspaceReimburseView(policyID: string) { }, ]; - type OpenWorkspaceReimburseViewParams = { - policyID: string; - }; - const params: OpenWorkspaceReimburseViewParams = {policyID}; - API.read('OpenWorkspaceReimburseView', params, {successData, failureData}); + API.read(READ_COMMANDS.OPEN_WORKSPACE_REIMBURSE_VIEW, params, {successData, failureData}); } /** @@ -1497,17 +1456,12 @@ function openWorkspace(policyID: string, clientMemberAccountIDs: number[]) { return; } - type OpenWorkspaceParams = { - policyID: string; - clientMemberAccountIDs: string; - }; - const params: OpenWorkspaceParams = { policyID, clientMemberAccountIDs: JSON.stringify(clientMemberAccountIDs), }; - API.read('OpenWorkspace', params); + API.read(READ_COMMANDS.OPEN_WORKSPACE, params); } function openWorkspaceMembersPage(policyID: string, clientMemberEmails: string[]) { @@ -1516,17 +1470,12 @@ function openWorkspaceMembersPage(policyID: string, clientMemberEmails: string[] return; } - type OpenWorkspaceMembersPageParams = { - policyID: string; - clientMemberEmails: string; - }; - const params: OpenWorkspaceMembersPageParams = { policyID, clientMemberEmails: JSON.stringify(clientMemberEmails), }; - API.read('OpenWorkspaceMembersPage', params); + API.read(READ_COMMANDS.OPEN_WORKSPACE_MEMBERS_PAGE, params); } function openWorkspaceInvitePage(policyID: string, clientMemberEmails: string[]) { @@ -1535,27 +1484,18 @@ function openWorkspaceInvitePage(policyID: string, clientMemberEmails: string[]) return; } - type OpenWorkspaceInvitePageParams = { - policyID: string; - clientMemberEmails: string; - }; - const params: OpenWorkspaceInvitePageParams = { policyID, clientMemberEmails: JSON.stringify(clientMemberEmails), }; - API.read('OpenWorkspaceInvitePage', params); + API.read(READ_COMMANDS.OPEN_WORKSPACE_INVITE_PAGE, params); } function openDraftWorkspaceRequest(policyID: string) { - type OpenDraftWorkspaceRequestParams = { - policyID: string; - }; - const params: OpenDraftWorkspaceRequestParams = {policyID}; - API.read('OpenDraftWorkspaceRequest', params); + API.read(READ_COMMANDS.OPEN_DRAFT_WORKSPACE_REQUEST, params); } function setWorkspaceInviteMembersDraft(policyID: string, invitedEmailsToAccountIDs: Record) { @@ -2017,26 +1957,7 @@ function createWorkspaceFromIOUPayment(iouReport: Report): string | undefined { value: {[movedReportAction.reportActionID]: null}, }); - type CreateWorkspaceFromIOUPayment = { - policyID: string; - announceChatReportID: string; - adminsChatReportID: string; - expenseChatReportID: string; - ownerEmail: string; - makeMeAdmin: boolean; - policyName: string; - type: string; - announceCreatedReportActionID: string; - adminsCreatedReportActionID: string; - expenseCreatedReportActionID: string; - customUnitID: string; - customUnitRateID: string; - iouReportID: string; - memberData: string; - reportActionID: string; - }; - - const params: CreateWorkspaceFromIOUPayment = { + const params: CreateWorkspaceFromIOUPaymentParams = { policyID, announceChatReportID, adminsChatReportID, @@ -2055,7 +1976,7 @@ function createWorkspaceFromIOUPayment(iouReport: Report): string | undefined { reportActionID: movedReportAction.reportActionID, }; - API.write('CreateWorkspaceFromIOUPayment', params, {optimisticData, successData, failureData}); + API.write(WRITE_COMMANDS.CREATE_WORKSPACE_FROM_IOU_PAYMENT, params, {optimisticData, successData, failureData}); return policyID; } From ea5c6417cedb2638407151c2c710fb3d69c706f8 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Tue, 23 Jan 2024 11:24:32 +0100 Subject: [PATCH 394/580] Add Task parameters --- src/libs/API/parameters/CancelTaskParams.ts | 6 ++++++ src/libs/API/parameters/CompleteTaskParams.ts | 6 ++++++ src/libs/API/parameters/CreateTaskParams.ts | 15 +++++++++++++++ src/libs/API/parameters/EditTaskAssigneeParams.ts | 10 ++++++++++ src/libs/API/parameters/EditTaskParams.ts | 8 ++++++++ 5 files changed, 45 insertions(+) create mode 100644 src/libs/API/parameters/CancelTaskParams.ts create mode 100644 src/libs/API/parameters/CompleteTaskParams.ts create mode 100644 src/libs/API/parameters/CreateTaskParams.ts create mode 100644 src/libs/API/parameters/EditTaskAssigneeParams.ts create mode 100644 src/libs/API/parameters/EditTaskParams.ts diff --git a/src/libs/API/parameters/CancelTaskParams.ts b/src/libs/API/parameters/CancelTaskParams.ts new file mode 100644 index 000000000000..fc753cd2ea5b --- /dev/null +++ b/src/libs/API/parameters/CancelTaskParams.ts @@ -0,0 +1,6 @@ +type CancelTaskParams = { + cancelledTaskReportActionID?: string; + taskReportID?: string; +}; + +export default CancelTaskParams; diff --git a/src/libs/API/parameters/CompleteTaskParams.ts b/src/libs/API/parameters/CompleteTaskParams.ts new file mode 100644 index 000000000000..2312588a6b83 --- /dev/null +++ b/src/libs/API/parameters/CompleteTaskParams.ts @@ -0,0 +1,6 @@ +type CompleteTaskParams = { + taskReportID?: string; + completedTaskReportActionID?: string; +}; + +export default CompleteTaskParams; diff --git a/src/libs/API/parameters/CreateTaskParams.ts b/src/libs/API/parameters/CreateTaskParams.ts new file mode 100644 index 000000000000..0ead163c623b --- /dev/null +++ b/src/libs/API/parameters/CreateTaskParams.ts @@ -0,0 +1,15 @@ +type CreateTaskParams = { + parentReportActionID?: string; + parentReportID?: string; + taskReportID?: string; + createdTaskReportActionID?: string; + title?: string; + description?: string; + assignee?: string; + assigneeAccountID?: number; + assigneeChatReportID?: string; + assigneeChatReportActionID?: string; + assigneeChatCreatedReportActionID?: string; +}; + +export default CreateTaskParams; diff --git a/src/libs/API/parameters/EditTaskAssigneeParams.ts b/src/libs/API/parameters/EditTaskAssigneeParams.ts new file mode 100644 index 000000000000..cfac73067587 --- /dev/null +++ b/src/libs/API/parameters/EditTaskAssigneeParams.ts @@ -0,0 +1,10 @@ +type EditTaskAssigneeParams = { + taskReportID?: string; + assignee?: string; + editedTaskReportActionID?: string; + assigneeChatReportID?: string; + assigneeChatReportActionID?: string; + assigneeChatCreatedReportActionID?: string; +}; + +export default EditTaskAssigneeParams; diff --git a/src/libs/API/parameters/EditTaskParams.ts b/src/libs/API/parameters/EditTaskParams.ts new file mode 100644 index 000000000000..01595b7928c5 --- /dev/null +++ b/src/libs/API/parameters/EditTaskParams.ts @@ -0,0 +1,8 @@ +type EditTaskParams = { + taskReportID?: string; + title?: string; + description?: string; + editedTaskReportActionID?: string; +}; + +export default EditTaskParams; From 010bfa1964a3a520a101e340d5dfebe8f973af31 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Tue, 23 Jan 2024 11:25:19 +0100 Subject: [PATCH 395/580] Add last Task parameters, add task parameters to WRITE_COMMANDS and mappings --- src/libs/API/parameters/ReopenTaskParams.ts | 6 ++ src/libs/API/parameters/index.ts | 6 ++ src/libs/API/types.ts | 12 ++++ src/libs/actions/Task.ts | 71 ++++----------------- 4 files changed, 38 insertions(+), 57 deletions(-) create mode 100644 src/libs/API/parameters/ReopenTaskParams.ts diff --git a/src/libs/API/parameters/ReopenTaskParams.ts b/src/libs/API/parameters/ReopenTaskParams.ts new file mode 100644 index 000000000000..ecdff74504f7 --- /dev/null +++ b/src/libs/API/parameters/ReopenTaskParams.ts @@ -0,0 +1,6 @@ +type ReopenTaskParams = { + taskReportID?: string; + reopenedTaskReportActionID?: string; +}; + +export default ReopenTaskParams; diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index e640554bd515..6bb0935c6835 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -114,3 +114,9 @@ export type {default as OpenWorkspaceMembersPageParams} from './OpenWorkspaceMem export type {default as OpenDraftWorkspaceRequestParams} from './OpenDraftWorkspaceRequestParams'; export type {default as UpdateWorkspaceCustomUnitAndRateParams} from './UpdateWorkspaceCustomUnitAndRateParams'; export type {default as CreateWorkspaceFromIOUPaymentParams} from './CreateWorkspaceFromIOUPaymentParams'; +export type {default as CreateTaskParams} from './CreateTaskParams'; +export type {default as CancelTaskParams} from './CancelTaskParams'; +export type {default as EditTaskAssigneeParams} from './EditTaskAssigneeParams'; +export type {default as EditTaskParams} from './EditTaskParams'; +export type {default as ReopenTaskParams} from './ReopenTaskParams'; +export type {default as CompleteTaskParams} from './CompleteTaskParams'; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index e4156a2a582e..b50f96fa3ea5 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -105,6 +105,12 @@ const WRITE_COMMANDS = { UPDATE_WORKSPACE_CUSTOM_UNIT_AND_RATE: 'UpdateWorkspaceCustomUnitAndRate', CREATE_WORKSPACE: 'CreateWorkspace', CREATE_WORKSPACE_FROM_IOU_PAYMENT: 'CreateWorkspaceFromIOUPayment', + CREATE_TASK: 'CreateTask', + CANCEL_TASK: 'CancelTask', + EDIT_TASK_ASSIGNEE: 'EditTaskAssignee', + EDIT_TASK: 'EditTask', + REOPEN_TASK: 'ReopenTask', + COMPLETE_TASK: 'CompleteTask', } as const; type WriteCommand = ValueOf; @@ -207,6 +213,12 @@ type WriteCommandParameters = { [WRITE_COMMANDS.UPDATE_WORKSPACE_CUSTOM_UNIT_AND_RATE]: Parameters.UpdateWorkspaceCustomUnitAndRateParams; [WRITE_COMMANDS.CREATE_WORKSPACE]: Parameters.CreateWorkspaceParams; [WRITE_COMMANDS.CREATE_WORKSPACE_FROM_IOU_PAYMENT]: Parameters.CreateWorkspaceFromIOUPaymentParams; + [WRITE_COMMANDS.CREATE_TASK]: Parameters.CreateTaskParams; + [WRITE_COMMANDS.CANCEL_TASK]: Parameters.CancelTaskParams; + [WRITE_COMMANDS.EDIT_TASK_ASSIGNEE]: Parameters.EditTaskAssigneeParams; + [WRITE_COMMANDS.EDIT_TASK]: Parameters.EditTaskParams; + [WRITE_COMMANDS.REOPEN_TASK]: Parameters.ReopenTaskParams; + [WRITE_COMMANDS.COMPLETE_TASK]: Parameters.CompleteTaskParams; }; const READ_COMMANDS = { diff --git a/src/libs/actions/Task.ts b/src/libs/actions/Task.ts index c03fa15fe1ae..c1bfcdd6f3e5 100644 --- a/src/libs/actions/Task.ts +++ b/src/libs/actions/Task.ts @@ -3,6 +3,8 @@ import Onyx from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import * as Expensicons from '@components/Icon/Expensicons'; import * as API from '@libs/API'; +import type {CancelTaskParams, CompleteTaskParams, CreateTaskParams, EditTaskAssigneeParams, EditTaskParams, ReopenTaskParams} from '@libs/API/parameters'; +import {WRITE_COMMANDS} from '@libs/API/types'; import DateUtils from '@libs/DateUtils'; import * as ErrorUtils from '@libs/ErrorUtils'; import * as LocalePhoneNumber from '@libs/LocalePhoneNumber'; @@ -226,21 +228,7 @@ function createTaskAndNavigate( clearOutTaskInfo(); - type CreateTaskParameters = { - parentReportActionID?: string; - parentReportID?: string; - taskReportID?: string; - createdTaskReportActionID?: string; - title?: string; - description?: string; - assignee?: string; - assigneeAccountID?: number; - assigneeChatReportID?: string; - assigneeChatReportActionID?: string; - assigneeChatCreatedReportActionID?: string; - }; - - const parameters: CreateTaskParameters = { + const parameters: CreateTaskParams = { parentReportActionID: optimisticAddCommentReport.reportAction.reportActionID, parentReportID, taskReportID: optimisticTaskReport.reportID, @@ -254,7 +242,7 @@ function createTaskAndNavigate( assigneeChatCreatedReportActionID: assigneeChatReportOnyxData?.optimisticChatCreatedReportAction?.reportActionID, }; - API.write('CreateTask', parameters, {optimisticData, successData, failureData}); + API.write(WRITE_COMMANDS.CREATE_TASK, parameters, {optimisticData, successData, failureData}); Navigation.dismissModal(parentReportID); } @@ -316,17 +304,12 @@ function completeTask(taskReport: OnyxEntry) { }, ]; - type CompleteTaskParameters = { - taskReportID?: string; - completedTaskReportActionID?: string; - }; - - const parameters: CompleteTaskParameters = { + const parameters: CompleteTaskParams = { taskReportID, completedTaskReportActionID: completedTaskReportAction.reportActionID, }; - API.write('CompleteTask', parameters, {optimisticData, successData, failureData}); + API.write(WRITE_COMMANDS.COMPLETE_TASK, parameters, {optimisticData, successData, failureData}); } /** @@ -388,17 +371,12 @@ function reopenTask(taskReport: OnyxEntry) { }, ]; - type ReopenTaskParameters = { - taskReportID?: string; - reopenedTaskReportActionID?: string; - }; - - const parameters: ReopenTaskParameters = { + const parameters: ReopenTaskParams = { taskReportID, reopenedTaskReportActionID: reopenedTaskReportAction.reportActionID, }; - API.write('ReopenTask', parameters, {optimisticData, successData, failureData}); + API.write(WRITE_COMMANDS.REOPEN_TASK, parameters, {optimisticData, successData, failureData}); } function editTask(report: OnyxTypes.Report, {title, description}: OnyxTypes.Task) { @@ -461,21 +439,14 @@ function editTask(report: OnyxTypes.Report, {title, description}: OnyxTypes.Task }, ]; - type EditTaskParameters = { - taskReportID?: string; - title?: string; - description?: string; - editedTaskReportActionID?: string; - }; - - const parameters: EditTaskParameters = { + const parameters: EditTaskParams = { taskReportID: report.reportID, title: reportName, description: reportDescription, editedTaskReportActionID: editTaskReportAction.reportActionID, }; - API.write('EditTask', parameters, {optimisticData, successData, failureData}); + API.write(WRITE_COMMANDS.EDIT_TASK, parameters, {optimisticData, successData, failureData}); } function editTaskAssignee(report: OnyxTypes.Report, ownerAccountID: number, assigneeEmail: string, assigneeAccountID = 0, assigneeChatReport: OnyxEntry = null) { @@ -555,16 +526,7 @@ function editTaskAssignee(report: OnyxTypes.Report, ownerAccountID: number, assi failureData.push(...assigneeChatReportOnyxData.failureData); } - type EditTaskAssigneeParameters = { - taskReportID?: string; - assignee?: string; - editedTaskReportActionID?: string; - assigneeChatReportID?: string; - assigneeChatReportActionID?: string; - assigneeChatCreatedReportActionID?: string; - }; - - const parameters: EditTaskAssigneeParameters = { + const parameters: EditTaskAssigneeParams = { taskReportID: report.reportID, assignee: assigneeEmail, editedTaskReportActionID: editTaskReportAction.reportActionID, @@ -573,7 +535,7 @@ function editTaskAssignee(report: OnyxTypes.Report, ownerAccountID: number, assi assigneeChatCreatedReportActionID: assigneeChatReportOnyxData?.optimisticChatCreatedReportAction?.reportActionID, }; - API.write('EditTaskAssignee', parameters, {optimisticData, successData, failureData}); + API.write(WRITE_COMMANDS.EDIT_TASK_ASSIGNEE, parameters, {optimisticData, successData, failureData}); } /** @@ -872,17 +834,12 @@ function deleteTask(taskReportID: string, taskTitle: string, originalStateNum: n }, ]; - type CancelTaskParameters = { - cancelledTaskReportActionID?: string; - taskReportID?: string; - }; - - const parameters: CancelTaskParameters = { + const parameters: CancelTaskParams = { cancelledTaskReportActionID: optimisticReportActionID, taskReportID, }; - API.write('CancelTask', parameters, {optimisticData, successData, failureData}); + API.write(WRITE_COMMANDS.CANCEL_TASK, parameters, {optimisticData, successData, failureData}); if (shouldDeleteTaskReport) { Navigation.goBack(ROUTES.REPORT_WITH_ID.getRoute(parentReport?.reportID ?? '')); From f3b7de586f11d03ab09585ae1e5f083f8107f210 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Tue, 23 Jan 2024 12:47:08 +0100 Subject: [PATCH 396/580] improve pdf --- .../Pager/AttachmentCarouselPagerContext.ts | 3 +- .../AttachmentCarousel/Pager/index.tsx | 16 ++-- .../AttachmentViewImage/index.js | 2 - .../BaseAttachmentViewPdf.js | 5 +- .../AttachmentViewPdf/index.android.js | 91 ++++++++++++------- .../AttachmentView/AttachmentViewPdf/index.js | 3 +- .../Attachments/AttachmentView/index.js | 3 - 7 files changed, 73 insertions(+), 50 deletions(-) diff --git a/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts b/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts index 94f503002b4a..270e0b04909c 100644 --- a/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts +++ b/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts @@ -6,8 +6,7 @@ import type {SharedValue} from 'react-native-reanimated'; type AttachmentCarouselPagerContextValue = { pagerRef: ForwardedRef; isPagerScrolling: SharedValue; - isScrollEnabled: boolean; - setScrollEnabled: (shouldPagerScroll: boolean) => void; + isScrollEnabled: SharedValue; onTap: () => void; onScaleChanged: (scale: number) => void; }; diff --git a/src/components/Attachments/AttachmentCarousel/Pager/index.tsx b/src/components/Attachments/AttachmentCarousel/Pager/index.tsx index dded43516947..2f79f5b0abb2 100644 --- a/src/components/Attachments/AttachmentCarousel/Pager/index.tsx +++ b/src/components/Attachments/AttachmentCarousel/Pager/index.tsx @@ -5,7 +5,7 @@ import type {NativeViewGestureHandlerProps} from 'react-native-gesture-handler'; import {createNativeWrapper} from 'react-native-gesture-handler'; import type {PagerViewProps} from 'react-native-pager-view'; import PagerView from 'react-native-pager-view'; -import Animated, {useSharedValue} from 'react-native-reanimated'; +import Animated, {useAnimatedProps, useSharedValue} from 'react-native-reanimated'; import useThemeStyles from '@hooks/useThemeStyles'; import AttachmentCarouselPagerContext from './AttachmentCarouselPagerContext'; import usePageScrollHandler from './usePageScrollHandler'; @@ -43,10 +43,10 @@ function AttachmentCarouselPager( const pagerRef = useRef(null); const isPagerScrolling = useSharedValue(false); - const [isScrollEnabled, setScrollEnabled] = useState(isZoomedOut); + const isScrollEnabled = useSharedValue(isZoomedOut); useEffect(() => { - setScrollEnabled(isZoomedOut); - }, [isZoomedOut]); + isScrollEnabled.value = isZoomedOut; + }, [isScrollEnabled, isZoomedOut]); const activePage = useSharedValue(initialIndex); const [activePageState, setActivePageState] = useState(initialIndex); @@ -58,6 +58,7 @@ function AttachmentCarouselPager( activePage.value = e.position; isPagerScrolling.value = e.offset !== 0; + isScrollEnabled.value = true; }, }, [], @@ -73,13 +74,16 @@ function AttachmentCarouselPager( pagerRef, isPagerScrolling, isScrollEnabled, - setScrollEnabled, onTap, onScaleChanged, }), [isPagerScrolling, isScrollEnabled, onTap, onScaleChanged], ); + const animatedProps = useAnimatedProps(() => ({ + scrollEnabled: isScrollEnabled.value, + })); + useImperativeHandle( ref, () => ({ @@ -93,7 +97,6 @@ function AttachmentCarouselPager( return ( {items.map((item, index) => ( { if (!attachmentCarouselPagerContext) { @@ -43,7 +43,8 @@ function BaseAttachmentViewPdf({ if (onPressProp !== undefined) { onPressProp(e); } - if (attachmentCarouselPagerContext !== null && isScrollEnabled) { + + if (attachmentCarouselPagerContext !== null && isScrollEnabled.value) { attachmentCarouselPagerContext.onTap(e); } }, diff --git a/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.android.js b/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.android.js index 19719c133f1c..b85242f5f571 100644 --- a/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.android.js +++ b/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.android.js @@ -1,19 +1,25 @@ -import React, {memo, useCallback, useContext} from 'react'; +import React, {memo, useContext, useMemo} from 'react'; import {StyleSheet, View} from 'react-native'; import {Gesture, GestureDetector} from 'react-native-gesture-handler'; -import Animated, {runOnJS, useSharedValue} from 'react-native-reanimated'; +import Animated, {useSharedValue} from 'react-native-reanimated'; import AttachmentCarouselPagerContext from '@components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext'; import useThemeStyles from '@hooks/useThemeStyles'; import BaseAttachmentViewPdf from './BaseAttachmentViewPdf'; import {attachmentViewPdfDefaultProps, attachmentViewPdfPropTypes} from './propTypes'; +// If the user pans less than this threshold, we'll not enable/disable the pager scroll, since the thouch will most probably be a tap. +// If the user moves their finger more than this threshold in the X direction, we'll enable the pager scroll. Otherwise if in the Y direction, we'll disable it. +const SCROLL_THRESHOLD = 10; + +function roundToDecimal(value, decimalPlaces = 0) { + const valueWithExponent = Math.round(`${value}e${decimalPlaces}`); + return Number(`${valueWithExponent}e${-decimalPlaces}`); +} + function AttachmentViewPdf(props) { const styles = useThemeStyles(); - const {onScaleChanged, ...restProps} = props; const attachmentCarouselPagerContext = useContext(AttachmentCarouselPagerContext); - const scaleRef = useSharedValue(1); - const offsetX = useSharedValue(0); - const offsetY = useSharedValue(0); + const scale = useSharedValue(1); // Reanimated freezes all objects captured in the closure of a worklet. // Since Reanimated 3, entire objects are captured instead of just the relevant properties. @@ -22,32 +28,54 @@ function AttachmentViewPdf(props) { // frozen, which combined with Reanimated using strict mode since 3.6.0 was resulting in errors. // Without strict mode, it would just silently fail. // See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/freeze#description - const isScrollEnabled = attachmentCarouselPagerContext !== null ? attachmentCarouselPagerContext.isScrollEnabled : undefined; + const isScrollEnabled = attachmentCarouselPagerContext === null ? undefined : attachmentCarouselPagerContext.isScrollEnabled; + + const offsetX = useSharedValue(0); + const offsetY = useSharedValue(0); + const isPanRunning = useSharedValue(false); const Pan = Gesture.Pan() .manualActivation(true) .onTouchesMove((evt) => { - if (offsetX.value !== 0 && offsetY.value !== 0 && attachmentCarouselPagerContext !== null && isScrollEnabled) { - const {setScrollEnabled} = attachmentCarouselPagerContext; + if (offsetX.value !== 0 && offsetY.value !== 0 && isScrollEnabled) { + const translateX = Math.abs(evt.allTouches[0].absoluteX - offsetX.value); + const translateY = Math.abs(evt.allTouches[0].absoluteY - offsetY.value); + + const allowEnablingScroll = !isPanRunning.value || isScrollEnabled.value; // if the value of X is greater than Y and the pdf is not zoomed in, // enable the pager scroll so that the user // can swipe to the next attachment otherwise disable it. - if (Math.abs(evt.allTouches[0].absoluteX - offsetX.value) > Math.abs(evt.allTouches[0].absoluteY - offsetY.value) && scaleRef.value === 1) { - runOnJS(setScrollEnabled)(true); - } else { - runOnJS(setScrollEnabled)(false); + if (translateX > translateY && translateX > SCROLL_THRESHOLD && scale.value === 1 && allowEnablingScroll) { + isScrollEnabled.value = true; + } else if (translateY > SCROLL_THRESHOLD) { + isScrollEnabled.value = false; } } + + isPanRunning.value = true; offsetX.value = evt.allTouches[0].absoluteX; offsetY.value = evt.allTouches[0].absoluteY; + }) + .onTouchesUp(() => { + isPanRunning.value = false; + isScrollEnabled.value = true; }); - const updateScale = useCallback( - (scale) => { - scaleRef.value = scale; - }, - [scaleRef], + const Content = useMemo( + () => ( + { + // The react-native-pdf library's onScaleChanged event will sometimes give us scale values of e.g. 0.99... instead of 1, + // even though we're not pinching/zooming + // Rounding the scale value to 2 decimal place fixes this issue, since pinching will still be possible but very small pinches are ignored. + scale.value = roundToDecimal(newScale, 2); + }} + /> + ), + [props, scale], ); return ( @@ -55,21 +83,18 @@ function AttachmentViewPdf(props) { collapsable={false} style={styles.flex1} > - - - { - updateScale(scale); - onScaleChanged(); - }} - /> - - + {attachmentCarouselPagerContext === null ? ( + Content + ) : ( + + + {Content} + + + )} ); } diff --git a/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.js b/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.js index c3d1423b17c9..d6a402613c34 100644 --- a/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.js +++ b/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.js @@ -2,7 +2,7 @@ import React, {memo} from 'react'; import PDFView from '@components/PDFView'; import {attachmentViewPdfDefaultProps, attachmentViewPdfPropTypes} from './propTypes'; -function AttachmentViewPdf({file, encryptedSourceUrl, isFocused, onPress, onScaleChanged, onToggleKeyboard, onLoadComplete, errorLabelStyles, style}) { +function AttachmentViewPdf({file, encryptedSourceUrl, isFocused, onPress, onToggleKeyboard, onLoadComplete, errorLabelStyles, style}) { return ( diff --git a/src/components/Attachments/AttachmentView/index.js b/src/components/Attachments/AttachmentView/index.js index b0060afdb813..3b90385f9e9b 100755 --- a/src/components/Attachments/AttachmentView/index.js +++ b/src/components/Attachments/AttachmentView/index.js @@ -67,7 +67,6 @@ function AttachmentView({ shouldShowLoadingSpinnerIcon, shouldShowDownloadIcon, containerStyles, - onScaleChanged, onToggleKeyboard, translate, isFocused, @@ -141,7 +140,6 @@ function AttachmentView({ carouselItemIndex={carouselItemIndex} carouselActiveItemIndex={carouselActiveItemIndex} onPress={onPress} - onScaleChanged={onScaleChanged} onToggleKeyboard={onToggleKeyboard} onLoadComplete={() => !loadComplete && setLoadComplete(true)} errorLabelStyles={isUsedInAttachmentModal ? [styles.textLabel, styles.textLarge] : [styles.cursorAuto]} @@ -174,7 +172,6 @@ function AttachmentView({ carouselActiveItemIndex={carouselActiveItemIndex} isImage={isImage} onPress={onPress} - onScaleChanged={onScaleChanged} onError={() => { setImageError(true); }} From cc5bb3328fc69b6f0d77f89f4c9748b27bb72d61 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Tue, 23 Jan 2024 13:08:34 +0100 Subject: [PATCH 397/580] remove unused prop --- .../AttachmentView/AttachmentViewPdf/index.android.js | 2 +- src/components/ImageView/index.native.js | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.android.js b/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.android.js index b85242f5f571..0ab8c2b07f56 100644 --- a/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.android.js +++ b/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.android.js @@ -68,7 +68,7 @@ function AttachmentViewPdf(props) { // eslint-disable-next-line react/jsx-props-no-spreading {...props} onScaleChanged={(newScale) => { - // The react-native-pdf library's onScaleChanged event will sometimes give us scale values of e.g. 0.99... instead of 1, + // The react-native-pdf's onScaleChanged event will sometimes give us scale values of e.g. 0.99... instead of 1, // even though we're not pinching/zooming // Rounding the scale value to 2 decimal place fixes this issue, since pinching will still be possible but very small pinches are ignored. scale.value = roundToDecimal(newScale, 2); diff --git a/src/components/ImageView/index.native.js b/src/components/ImageView/index.native.js index ba10162ec1e2..8af91f6eb604 100644 --- a/src/components/ImageView/index.native.js +++ b/src/components/ImageView/index.native.js @@ -28,7 +28,7 @@ const defaultProps = { style: {}, }; -function ImageView({isAuthTokenRequired, url, onScaleChanged, style, zoomRange, onError, isUsedInCarousel, isSingleCarouselItem, carouselItemIndex, carouselActiveItemIndex}) { +function ImageView({isAuthTokenRequired, url, style, zoomRange, onError, isUsedInCarousel, isSingleCarouselItem, carouselItemIndex, carouselActiveItemIndex}) { const hasSiblingCarouselItems = isUsedInCarousel && !isSingleCarouselItem; return ( @@ -36,7 +36,6 @@ function ImageView({isAuthTokenRequired, url, onScaleChanged, style, zoomRange, uri={url} zoomRange={zoomRange} isAuthTokenRequired={isAuthTokenRequired} - onScaleChanged={onScaleChanged} onError={onError} index={carouselItemIndex} activeIndex={carouselActiveItemIndex} From 4b8f1831fed3352a74ec604e3f2ae619433706d9 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Tue, 23 Jan 2024 13:21:29 +0100 Subject: [PATCH 398/580] fix: console errors --- .../Attachments/AttachmentView/AttachmentViewImage/index.js | 4 ++-- .../AttachmentView/AttachmentViewImage/propTypes.js | 2 ++ src/components/Attachments/AttachmentView/index.js | 6 +++++- src/components/Attachments/AttachmentView/propTypes.js | 3 --- src/components/ImageView/propTypes.js | 4 ---- 5 files changed, 9 insertions(+), 10 deletions(-) diff --git a/src/components/Attachments/AttachmentView/AttachmentViewImage/index.js b/src/components/Attachments/AttachmentView/AttachmentViewImage/index.js index 1a6b8b360097..14c60458b044 100755 --- a/src/components/Attachments/AttachmentView/AttachmentViewImage/index.js +++ b/src/components/Attachments/AttachmentView/AttachmentViewImage/index.js @@ -13,7 +13,7 @@ const propTypes = { }; function AttachmentViewImage({ - source, + url, file, isAuthTokenRequired, isUsedInCarousel, @@ -31,7 +31,7 @@ function AttachmentViewImage({ const children = ( Date: Tue, 23 Jan 2024 13:30:19 +0100 Subject: [PATCH 399/580] Reuse ContextMenuAnchor type, use TranslationPaths type --- src/components/ReportActionItem/ReportPreview.tsx | 5 ++--- src/components/ReportActionItem/TaskPreview.tsx | 5 ++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/components/ReportActionItem/ReportPreview.tsx b/src/components/ReportActionItem/ReportPreview.tsx index 42097cb84f4e..bb021fe52db1 100644 --- a/src/components/ReportActionItem/ReportPreview.tsx +++ b/src/components/ReportActionItem/ReportPreview.tsx @@ -26,14 +26,13 @@ import * as TransactionUtils from '@libs/TransactionUtils'; import type {ContextMenuAnchor} from '@pages/home/report/ContextMenu/ReportActionContextMenu'; import * as IOU from '@userActions/IOU'; import CONST from '@src/CONST'; +import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type {Policy, Report, ReportAction, Session, Transaction, TransactionViolations} from '@src/types/onyx'; import type {PaymentMethodType} from '@src/types/onyx/OriginalMessage'; import ReportActionItemImages from './ReportActionItemImages'; -type PaymentVerbTranslationPath = 'iou.payerSpent' | 'iou.payerOwes' | 'iou.payerPaid'; - type ReportPreviewOnyxProps = { /** The policy tied to the money request report */ policy: OnyxEntry; @@ -202,7 +201,7 @@ function ReportPreview({ return translate('iou.managerApproved', {manager: payerOrApproverName}); } const managerName = isPolicyExpenseChat ? ReportUtils.getPolicyName(chatReport) : ReportUtils.getDisplayNameForParticipant(managerID, true); - let paymentVerb: PaymentVerbTranslationPath = hasNonReimbursableTransactions ? 'iou.payerSpent' : 'iou.payerOwes'; + let paymentVerb: TranslationPaths = hasNonReimbursableTransactions ? 'iou.payerSpent' : 'iou.payerOwes'; if (iouSettled || iouReport?.isWaitingOnBankAccount) { paymentVerb = 'iou.payerPaid'; } diff --git a/src/components/ReportActionItem/TaskPreview.tsx b/src/components/ReportActionItem/TaskPreview.tsx index cbd166d79d3a..16af68d9677c 100644 --- a/src/components/ReportActionItem/TaskPreview.tsx +++ b/src/components/ReportActionItem/TaskPreview.tsx @@ -1,7 +1,5 @@ import Str from 'expensify-common/lib/str'; import React from 'react'; -// eslint-disable-next-line no-restricted-imports -import type {Text as RNText} from 'react-native'; import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx'; @@ -24,6 +22,7 @@ import * as LocalePhoneNumber from '@libs/LocalePhoneNumber'; import Navigation from '@libs/Navigation/Navigation'; import * as ReportUtils from '@libs/ReportUtils'; import * as TaskUtils from '@libs/TaskUtils'; +import type {ContextMenuAnchor} from '@pages/home/report/ContextMenu/ReportActionContextMenu'; import * as Session from '@userActions/Session'; import * as Task from '@userActions/Task'; import CONST from '@src/CONST'; @@ -65,7 +64,7 @@ type TaskPreviewProps = WithCurrentUserPersonalDetailsProps & chatReportID: string; /** Popover context menu anchor, used for showing context menu */ - contextMenuAnchor: RNText | null; + contextMenuAnchor: ContextMenuAnchor; /** Callback for updating context menu active state, used for showing context menu */ checkIfContextMenuActive: () => void; From 75287d8ec968656256cb47808b161084e27ac1bd Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Tue, 23 Jan 2024 13:39:20 +0100 Subject: [PATCH 400/580] 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 b8fc65304dabc2ed3819a82b6fe0d8ff7d775642 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Tue, 23 Jan 2024 13:54:02 +0100 Subject: [PATCH 401/580] Fix formatting --- .../CreateWorkspaceFromIOUPaymentParams.ts | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src/libs/API/parameters/CreateWorkspaceFromIOUPaymentParams.ts b/src/libs/API/parameters/CreateWorkspaceFromIOUPaymentParams.ts index 99caf7b6bc3d..761a6c2f5008 100644 --- a/src/libs/API/parameters/CreateWorkspaceFromIOUPaymentParams.ts +++ b/src/libs/API/parameters/CreateWorkspaceFromIOUPaymentParams.ts @@ -1,20 +1,20 @@ type CreateWorkspaceFromIOUPaymentParams = { - policyID: string; - announceChatReportID: string; - adminsChatReportID: string; - expenseChatReportID: string; - ownerEmail: string; - makeMeAdmin: boolean; - policyName: string; - type: string; - announceCreatedReportActionID: string; - adminsCreatedReportActionID: string; - expenseCreatedReportActionID: string; - customUnitID: string; - customUnitRateID: string; - iouReportID: string; - memberData: string; - reportActionID: string; + policyID: string; + announceChatReportID: string; + adminsChatReportID: string; + expenseChatReportID: string; + ownerEmail: string; + makeMeAdmin: boolean; + policyName: string; + type: string; + announceCreatedReportActionID: string; + adminsCreatedReportActionID: string; + expenseCreatedReportActionID: string; + customUnitID: string; + customUnitRateID: string; + iouReportID: string; + memberData: string; + reportActionID: string; }; -export default CreateWorkspaceFromIOUPaymentParams; \ No newline at end of file +export default CreateWorkspaceFromIOUPaymentParams; 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 402/580] 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 403/580] 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 d766e84ad1f1af2aacb10b71e00cbb13a17e1a5c Mon Sep 17 00:00:00 2001 From: s-alves10 Date: Tue, 23 Jan 2024 08:35:20 -0600 Subject: [PATCH 404/580] fix: call signOutAndRedirectToSignIn with no parameter --- src/pages/home/sidebar/SignInButton.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/home/sidebar/SignInButton.js b/src/pages/home/sidebar/SignInButton.js index 9edcc9584dbd..f89deb6f65b2 100644 --- a/src/pages/home/sidebar/SignInButton.js +++ b/src/pages/home/sidebar/SignInButton.js @@ -16,14 +16,14 @@ function SignInButton() { Session.signOutAndRedirectToSignIn()} >