diff --git a/android/app/build.gradle b/android/app/build.gradle index f224d895e2fa..04d711009c10 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -91,8 +91,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1001040801 - versionName "1.4.8-1" + versionCode 1001040902 + versionName "1.4.9-2" } flavorDimensions "default" diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index 7c3fbf13697a..07afc5a85593 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -19,7 +19,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.4.8 + 1.4.9 CFBundleSignature ???? CFBundleURLTypes @@ -40,7 +40,7 @@ CFBundleVersion - 1.4.8.1 + 1.4.9.2 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 0d2561b67b74..a434ffdc5757 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 1.4.8 + 1.4.9 CFBundleSignature ???? CFBundleVersion - 1.4.8.1 + 1.4.9.2 diff --git a/package-lock.json b/package-lock.json index 51dc9df3a5f0..6e5b51fa4526 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "1.4.8-1", + "version": "1.4.9-2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.4.8-1", + "version": "1.4.9-2", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index ac02f2db5f82..8191454ef138 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.4.8-1", + "version": "1.4.9-2", "author": "Expensify, Inc.", "homepage": "https://new.expensify.com", "description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.", diff --git a/src/CONST.ts b/src/CONST.ts index 283195562e49..ddedb550f368 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -2807,7 +2807,7 @@ const CONST = { HORIZONTAL_SPACER: { DEFAULT_BORDER_BOTTOM_WIDTH: 1, DEFAULT_MARGIN_VERTICAL: 8, - HIDDEN_MARGIN_VERTICAL: 0, + HIDDEN_MARGIN_VERTICAL: 4, HIDDEN_BORDER_BOTTOM_WIDTH: 0, }, diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/AnchorRenderer.js b/src/components/HTMLEngineProvider/HTMLRenderers/AnchorRenderer.js index 8f1406439be9..49642308a357 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/AnchorRenderer.js +++ b/src/components/HTMLEngineProvider/HTMLRenderers/AnchorRenderer.js @@ -62,7 +62,7 @@ function AnchorRenderer(props) { key={props.key} displayName={displayName} // Only pass the press handler for internal links. For public links or whitelisted internal links fallback to default link handling - onPress={internalNewExpensifyPath || internalExpensifyPath ? Link.openLink : undefined} + onPress={internalNewExpensifyPath || internalExpensifyPath ? () => Link.openLink(attrHref, environmentURL, isAttachment) : undefined} > diff --git a/src/components/MoneyRequestConfirmationList.js b/src/components/MoneyRequestConfirmationList.js index 1b4967a9c54c..da3c19f48d1b 100755 --- a/src/components/MoneyRequestConfirmationList.js +++ b/src/components/MoneyRequestConfirmationList.js @@ -699,7 +699,13 @@ function MoneyRequestConfirmationList(props) { title={props.iouCategory} description={translate('common.category')} numberOfLinesTitle={2} - onPress={() => Navigation.navigate(ROUTES.MONEY_REQUEST_CATEGORY.getRoute(props.iouType, props.reportID))} + onPress={() => { + if (props.isEditingSplitBill) { + Navigation.navigate(ROUTES.EDIT_SPLIT_BILL.getRoute(props.reportID, props.reportActionID, CONST.EDIT_REQUEST_FIELD.CATEGORY)); + return; + } + Navigation.navigate(ROUTES.MONEY_REQUEST_CATEGORY.getRoute(props.iouType, props.reportID)); + }} style={[styles.moneyRequestMenuItem]} titleStyle={styles.flex1} disabled={didConfirm} @@ -713,7 +719,13 @@ function MoneyRequestConfirmationList(props) { title={props.iouTag} description={policyTagListName} numberOfLinesTitle={2} - onPress={() => Navigation.navigate(ROUTES.MONEY_REQUEST_TAG.getRoute(props.iouType, props.reportID))} + onPress={() => { + if (props.isEditingSplitBill) { + Navigation.navigate(ROUTES.EDIT_SPLIT_BILL.getRoute(props.reportID, props.reportActionID, CONST.EDIT_REQUEST_FIELD.TAG)); + return; + } + Navigation.navigate(ROUTES.MONEY_REQUEST_TAG.getRoute(props.iouType, props.reportID)); + }} style={[styles.moneyRequestMenuItem]} disabled={didConfirm} interactive={!props.isReadOnly} diff --git a/src/components/ReportActionItem/MoneyRequestView.js b/src/components/ReportActionItem/MoneyRequestView.js index 0249a9f5bb11..80725a1e2531 100644 --- a/src/components/ReportActionItem/MoneyRequestView.js +++ b/src/components/ReportActionItem/MoneyRequestView.js @@ -29,6 +29,7 @@ import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import * as ReportUtils from '@libs/ReportUtils'; import * as TransactionUtils from '@libs/TransactionUtils'; import AnimatedEmptyStateBackground from '@pages/home/report/AnimatedEmptyStateBackground'; +import reportActionPropTypes from '@pages/home/report/reportActionPropTypes'; import iouReportPropTypes from '@pages/iouReportPropTypes'; import reportPropTypes from '@pages/reportPropTypes'; import * as StyleUtils from '@styles/StyleUtils'; @@ -51,6 +52,9 @@ const propTypes = { /** The expense report or iou report (only will have a value if this is a transaction thread) */ parentReport: iouReportPropTypes, + /** The actions from the parent report */ + parentReportActions: PropTypes.objectOf(PropTypes.shape(reportActionPropTypes)), + /** Collection of categories attached to a policy */ policyCategories: PropTypes.objectOf(categoryPropTypes), @@ -65,6 +69,7 @@ const propTypes = { const defaultProps = { parentReport: {}, + parentReportActions: {}, policyCategories: {}, transaction: { amount: 0, @@ -74,13 +79,13 @@ const defaultProps = { policyTags: {}, }; -function MoneyRequestView({report, parentReport, policyCategories, shouldShowHorizontalRule, transaction, policyTags, policy}) { +function MoneyRequestView({report, parentReport, parentReportActions, policyCategories, shouldShowHorizontalRule, transaction, policyTags, policy}) { const theme = useTheme(); const styles = useThemeStyles(); const {isSmallScreenWidth} = useWindowDimensions(); const {translate} = useLocalize(); const {canUseViolations} = usePermissions(); - const parentReportAction = ReportActionsUtils.getParentReportAction(report); + const parentReportAction = parentReportActions[report.parentReportActionID] || {}; const moneyRequestReport = parentReport; const { created: transactionDate, @@ -313,8 +318,8 @@ MoneyRequestView.displayName = 'MoneyRequestView'; export default compose( withCurrentUserPersonalDetails, withOnyx({ - parentReport: { - key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT}${report.parentReportID}`, + session: { + key: ONYXKEYS.SESSION, }, policy: { key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY}${report.policyID}`, @@ -322,18 +327,24 @@ export default compose( policyCategories: { key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${report.policyID}`, }, - session: { - key: ONYXKEYS.SESSION, + policyTags: { + key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY_TAGS}${report.policyID}`, }, + parentReport: { + key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT}${report.parentReportID}`, + }, + parentReportActions: { + key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report ? report.parentReportID : '0'}`, + canEvict: false, + }, + }), + withOnyx({ transaction: { - key: ({report}) => { - const parentReportAction = ReportActionsUtils.getParentReportAction(report); + key: ({report, parentReportActions}) => { + const parentReportAction = parentReportActions[report.parentReportActionID]; const transactionID = lodashGet(parentReportAction, ['originalMessage', 'IOUTransactionID'], 0); return `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`; }, }, - policyTags: { - key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY_TAGS}${report.policyID}`, - }, }), )(MoneyRequestView); diff --git a/src/components/SpacerView.js b/src/components/SpacerView.js index 60ff7fd85e55..9509219c0ac7 100644 --- a/src/components/SpacerView.js +++ b/src/components/SpacerView.js @@ -1,6 +1,7 @@ import PropTypes from 'prop-types'; import React from 'react'; import Animated, {useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated'; +import usePrevious from '@hooks/usePrevious'; import stylePropTypes from '@styles/stylePropTypes'; import * as StyleUtils from '@styles/StyleUtils'; import CONST from '@src/CONST'; @@ -23,22 +24,30 @@ const defaultProps = { }; function SpacerView({shouldShow = true, style = []}) { - const marginVertical = useSharedValue(CONST.HORIZONTAL_SPACER.DEFAULT_MARGIN_VERTICAL); - const borderBottomWidth = useSharedValue(CONST.HORIZONTAL_SPACER.DEFAULT_BORDER_BOTTOM_WIDTH); + const marginVertical = useSharedValue(shouldShow ? CONST.HORIZONTAL_SPACER.DEFAULT_MARGIN_VERTICAL : CONST.HORIZONTAL_SPACER.HIDDEN_MARGIN_VERTICAL); + const borderBottomWidth = useSharedValue(shouldShow ? CONST.HORIZONTAL_SPACER.DEFAULT_BORDER_BOTTOM_WIDTH : CONST.HORIZONTAL_SPACER.HIDDEN_BORDER_BOTTOM_WIDTH); + const prevShouldShow = usePrevious(shouldShow); + + const duration = CONST.ANIMATED_TRANSITION; const animatedStyles = useAnimatedStyle(() => ({ - marginVertical: marginVertical.value, - borderBottomWidth: borderBottomWidth.value, + borderBottomWidth: withTiming(borderBottomWidth.value, {duration}), + marginTop: withTiming(marginVertical.value, {duration}), + marginBottom: withTiming(marginVertical.value, {duration}), })); React.useEffect(() => { - const duration = CONST.ANIMATED_TRANSITION; + if (shouldShow === prevShouldShow) { + return; + } const values = { marginVertical: shouldShow ? CONST.HORIZONTAL_SPACER.DEFAULT_MARGIN_VERTICAL : CONST.HORIZONTAL_SPACER.HIDDEN_MARGIN_VERTICAL, borderBottomWidth: shouldShow ? CONST.HORIZONTAL_SPACER.DEFAULT_BORDER_BOTTOM_WIDTH : CONST.HORIZONTAL_SPACER.HIDDEN_BORDER_BOTTOM_WIDTH, }; - marginVertical.value = withTiming(values.marginVertical, {duration}); - borderBottomWidth.value = withTiming(values.borderBottomWidth, {duration}); - }, [shouldShow, borderBottomWidth, marginVertical]); + marginVertical.value = values.marginVertical; + borderBottomWidth.value = values.borderBottomWidth; + + // eslint-disable-next-line react-hooks/exhaustive-deps -- we only need to trigger when shouldShow prop is changed + }, [shouldShow, prevShouldShow]); return ; } diff --git a/src/languages/en.ts b/src/languages/en.ts index a276de4e0f7c..817f06f6b344 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -1547,12 +1547,6 @@ export default { invitePeople: 'Invite new members', genericFailureMessage: 'An error occurred inviting the user to the workspace, please try again.', pleaseEnterValidLogin: `Please ensure the email or phone number is valid (e.g. ${CONST.EXAMPLE_PHONE_NUMBER}).`, - user: 'user', - users: 'users', - invited: 'invited', - removed: 'removed', - to: 'to', - from: 'from', }, inviteMessage: { inviteMessageTitle: 'Add message', diff --git a/src/languages/es.ts b/src/languages/es.ts index 290d80a6f65d..b219021daa0f 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -1569,12 +1569,6 @@ export default { invitePeople: 'Invitar nuevos miembros', genericFailureMessage: 'Se produjo un error al invitar al usuario al espacio de trabajo. Vuelva a intentarlo..', pleaseEnterValidLogin: `Asegúrese de que el correo electrónico o el número de teléfono sean válidos (p. ej. ${CONST.EXAMPLE_PHONE_NUMBER}).`, - user: 'usuario', - users: 'usuarios', - invited: 'invitó', - removed: 'eliminó', - to: 'a', - from: 'de', }, inviteMessage: { inviteMessageTitle: 'Añadir un mensaje', diff --git a/src/libs/Localize/index.ts b/src/libs/Localize/index.ts index 77c34ebdc576..488ff0d9b98a 100644 --- a/src/libs/Localize/index.ts +++ b/src/libs/Localize/index.ts @@ -1,7 +1,6 @@ import * as RNLocalize from 'react-native-localize'; import Onyx from 'react-native-onyx'; import Log from '@libs/Log'; -import {MessageElementBase, MessageTextElement} from '@libs/MessageElement'; import Config from '@src/CONFIG'; import CONST from '@src/CONST'; import translations from '@src/languages/translations'; @@ -122,48 +121,15 @@ function translateIfPhraseKey(message: MaybePhraseKey): string { } } -function getPreferredListFormat(): Intl.ListFormat { - if (!CONJUNCTION_LIST_FORMATS_FOR_LOCALES) { - init(); - } - - return CONJUNCTION_LIST_FORMATS_FOR_LOCALES[BaseLocaleListener.getPreferredLocale()]; -} - /** * Format an array into a string with comma and "and" ("a dog, a cat and a chicken") */ -function formatList(components: string[]) { - const listFormat = getPreferredListFormat(); - return listFormat.format(components); -} - -function formatMessageElementList(elements: readonly E[]): ReadonlyArray { - const listFormat = getPreferredListFormat(); - const parts = listFormat.formatToParts(elements.map((e) => e.content)); - const resultElements: Array = []; - - let nextElementIndex = 0; - for (const part of parts) { - if (part.type === 'element') { - /** - * The standard guarantees that all input elements will be present in the constructed parts, each exactly - * once, and without any modifications: https://tc39.es/ecma402/#sec-createpartsfromlist - */ - const element = elements[nextElementIndex++]; - - resultElements.push(element); - } else { - const literalElement: MessageTextElement = { - kind: 'text', - content: part.value, - }; - - resultElements.push(literalElement); - } +function arrayToString(anArray: string[]) { + if (!CONJUNCTION_LIST_FORMATS_FOR_LOCALES) { + init(); } - - return resultElements; + const listFormat = CONJUNCTION_LIST_FORMATS_FOR_LOCALES[BaseLocaleListener.getPreferredLocale()]; + return listFormat.format(anArray); } /** @@ -173,5 +139,5 @@ function getDevicePreferredLocale(): string { return RNLocalize.findBestAvailableLanguage([CONST.LOCALES.EN, CONST.LOCALES.ES])?.languageTag ?? CONST.LOCALES.DEFAULT; } -export {translate, translateLocal, translateIfPhraseKey, formatList, formatMessageElementList, getDevicePreferredLocale}; +export {translate, translateLocal, translateIfPhraseKey, arrayToString, getDevicePreferredLocale}; export type {PhraseParameters, Phrase, MaybePhraseKey}; diff --git a/src/libs/MessageElement.ts b/src/libs/MessageElement.ts deleted file mode 100644 index 584d7e1e289a..000000000000 --- a/src/libs/MessageElement.ts +++ /dev/null @@ -1,11 +0,0 @@ -type MessageElementBase = { - readonly kind: string; - readonly content: string; -}; - -type MessageTextElement = { - readonly kind: 'text'; - readonly content: string; -} & MessageElementBase; - -export type {MessageElementBase, MessageTextElement}; diff --git a/src/libs/Navigation/AppNavigator/AuthScreens.tsx b/src/libs/Navigation/AppNavigator/AuthScreens.tsx index a78b38728136..5e8a9f502dc5 100644 --- a/src/libs/Navigation/AppNavigator/AuthScreens.tsx +++ b/src/libs/Navigation/AppNavigator/AuthScreens.tsx @@ -13,6 +13,7 @@ import type {AuthScreensParamList} from '@navigation/types'; import DemoSetupPage from '@pages/DemoSetupPage'; import NotFoundPage from '@pages/ErrorPage/NotFoundPage'; import DesktopSignInRedirectPage from '@pages/signin/DesktopSignInRedirectPage'; +import SearchInputManager from '@pages/workspace/SearchInputManager'; import useThemeStyles from '@styles/useThemeStyles'; import * as App from '@userActions/App'; import * as Download from '@userActions/Download'; @@ -123,6 +124,8 @@ const modalScreenListeners = { Modal.setModalVisibility(true); }, beforeRemove: () => { + // Clear search input (WorkspaceInvitePage) when modal is closed + SearchInputManager.searchInput = ''; Modal.setModalVisibility(false); }, }; diff --git a/src/libs/PersonalDetailsUtils.js b/src/libs/PersonalDetailsUtils.js index 8a4151391453..560480dcec9d 100644 --- a/src/libs/PersonalDetailsUtils.js +++ b/src/libs/PersonalDetailsUtils.js @@ -197,18 +197,6 @@ function getFormattedAddress(privatePersonalDetails) { return formattedAddress.trim().replace(/,$/, ''); } -/** - * @param {Object} personalDetail - details object - * @returns {String | undefined} - The effective display name - */ -function getEffectiveDisplayName(personalDetail) { - if (personalDetail) { - return LocalePhoneNumber.formatPhoneNumber(personalDetail.login) || personalDetail.displayName; - } - - return undefined; -} - export { getDisplayNameOrDefault, getPersonalDetailsByIDs, @@ -218,5 +206,4 @@ export { getFormattedAddress, getFormattedStreet, getStreetLines, - getEffectiveDisplayName, }; diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index ff36a2ac3401..6dc735ebd8b7 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -5,17 +5,14 @@ import OnyxUtils from 'react-native-onyx/lib/utils'; import {ValueOf} from 'type-fest'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import {ActionName, ChangeLog} from '@src/types/onyx/OriginalMessage'; +import {ActionName} from '@src/types/onyx/OriginalMessage'; import Report from '@src/types/onyx/Report'; -import ReportAction, {Message, ReportActions} from '@src/types/onyx/ReportAction'; +import ReportAction, {ReportActions} from '@src/types/onyx/ReportAction'; import {EmptyObject, isEmptyObject} from '@src/types/utils/EmptyObject'; import * as CollectionUtils from './CollectionUtils'; import * as Environment from './Environment/Environment'; import isReportMessageAttachment from './isReportMessageAttachment'; -import * as Localize from './Localize'; import Log from './Log'; -import {MessageElementBase, MessageTextElement} from './MessageElement'; -import * as PersonalDetailsUtils from './PersonalDetailsUtils'; type LastVisibleMessage = { lastMessageTranslationKey?: string; @@ -23,19 +20,6 @@ type LastVisibleMessage = { lastMessageHtml?: string; }; -type MemberChangeMessageUserMentionElement = { - readonly kind: 'userMention'; - readonly accountID: number; -} & MessageElementBase; - -type MemberChangeMessageRoomReferenceElement = { - readonly kind: 'roomReference'; - readonly roomName: string; - readonly roomID: number; -} & MessageElementBase; - -type MemberChangeMessageElement = MessageTextElement | MemberChangeMessageUserMentionElement | MemberChangeMessageRoomReferenceElement; - const allReports: OnyxCollection = {}; Onyx.connect({ key: ONYXKEYS.COLLECTION.REPORT, @@ -116,7 +100,7 @@ function isReimbursementQueuedAction(reportAction: OnyxEntry) { return reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.REIMBURSEMENTQUEUED; } -function isMemberChangeAction(reportAction: OnyxEntry) { +function isChannelLogMemberAction(reportAction: OnyxEntry) { return ( reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.ROOMCHANGELOG.INVITE_TO_ROOM || reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.ROOMCHANGELOG.REMOVE_FROM_ROOM || @@ -125,10 +109,6 @@ function isMemberChangeAction(reportAction: OnyxEntry) { ); } -function isInviteMemberAction(reportAction: OnyxEntry) { - return reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.ROOMCHANGELOG.INVITE_TO_ROOM || reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICYCHANGELOG.INVITE_TO_ROOM; -} - function isReimbursementDeQueuedAction(reportAction: OnyxEntry): boolean { return reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.REIMBURSEMENTDEQUEUED; } @@ -179,14 +159,14 @@ function isTransactionThread(parentReportAction: OnyxEntry): boole * This gives us a stable order even in the case of multiple reportActions created on the same millisecond * */ -function getSortedReportActions(reportActions: ReportAction[] | null, shouldSortInDescendingOrder = false): ReportAction[] { +function getSortedReportActions(reportActions: ReportAction[] | null, shouldSortInDescendingOrder = false, shouldMarkTheFirstItemAsNewest = false): ReportAction[] { if (!Array.isArray(reportActions)) { throw new Error(`ReportActionsUtils.getSortedReportActions requires an array, received ${typeof reportActions}`); } const invertedMultiplier = shouldSortInDescendingOrder ? -1 : 1; - return reportActions?.filter(Boolean).sort((first, second) => { + const sortedActions = reportActions?.filter(Boolean).sort((first, second) => { // First sort by timestamp if (first.created !== second.created) { return (first.created < second.created ? -1 : 1) * invertedMultiplier; @@ -206,6 +186,16 @@ function getSortedReportActions(reportActions: ReportAction[] | null, shouldSort // will be consistent across all users and devices return (first.reportActionID < second.reportActionID ? -1 : 1) * invertedMultiplier; }); + + // If shouldMarkTheFirstItemAsNewest is true, label the first reportAction as isNewestReportAction + if (shouldMarkTheFirstItemAsNewest && sortedActions?.length > 0) { + sortedActions[0] = { + ...sortedActions[0], + isNewestReportAction: true, + }; + } + + return sortedActions; } /** @@ -467,12 +457,12 @@ function filterOutDeprecatedReportActions(reportActions: ReportActions | null): * to ensure they will always be displayed in the same order (in case multiple actions have the same timestamp). * This is all handled with getSortedReportActions() which is used by several other methods to keep the code DRY. */ -function getSortedReportActionsForDisplay(reportActions: ReportActions | null): ReportAction[] { +function getSortedReportActionsForDisplay(reportActions: ReportActions | null, shouldMarkTheFirstItemAsNewest = false): ReportAction[] { const filteredReportActions = Object.entries(reportActions ?? {}) .filter(([key, reportAction]) => shouldReportActionBeVisible(reportAction, key)) .map((entry) => entry[1]); const baseURLAdjustedReportActions = filteredReportActions.map((reportAction) => replaceBaseURL(reportAction)); - return getSortedReportActions(baseURLAdjustedReportActions, true); + return getSortedReportActions(baseURLAdjustedReportActions, true, shouldMarkTheFirstItemAsNewest); } /** @@ -659,89 +649,6 @@ function isNotifiableReportAction(reportAction: OnyxEntry): boolea return actions.includes(reportAction.actionName); } -function getMemberChangeMessageElements(reportAction: OnyxEntry): readonly MemberChangeMessageElement[] { - const isInviteAction = isInviteMemberAction(reportAction); - - // Currently, we only render messages when members are invited - const verb = isInviteAction ? Localize.translateLocal('workspace.invite.invited') : Localize.translateLocal('workspace.invite.removed'); - - const originalMessage = reportAction?.originalMessage as ChangeLog; - const targetAccountIDs: number[] = originalMessage?.targetAccountIDs ?? []; - const personalDetails = PersonalDetailsUtils.getPersonalDetailsByIDs(targetAccountIDs, 0); - - const mentionElements = targetAccountIDs.map((accountID): MemberChangeMessageUserMentionElement => { - const personalDetail = personalDetails.find((personal) => personal.accountID === accountID); - const handleText = PersonalDetailsUtils.getEffectiveDisplayName(personalDetail) ?? Localize.translateLocal('common.hidden'); - - return { - kind: 'userMention', - content: `@${handleText}`, - accountID, - }; - }); - - const buildRoomElements = (): readonly MemberChangeMessageElement[] => { - const roomName = originalMessage?.roomName; - - if (roomName) { - const preposition = isInviteAction ? ` ${Localize.translateLocal('workspace.invite.to')} ` : ` ${Localize.translateLocal('workspace.invite.from')} `; - - if (originalMessage.reportID) { - return [ - { - kind: 'text', - content: preposition, - }, - { - kind: 'roomReference', - roomName, - roomID: originalMessage.reportID, - content: roomName, - }, - ]; - } - } - - return []; - }; - - return [ - { - kind: 'text', - content: `${verb} `, - }, - ...Localize.formatMessageElementList(mentionElements), - ...buildRoomElements(), - ]; -} - -function getMemberChangeMessageFragment(reportAction: OnyxEntry): Message { - const messageElements: readonly MemberChangeMessageElement[] = getMemberChangeMessageElements(reportAction); - const html = messageElements - .map((messageElement) => { - switch (messageElement.kind) { - case 'userMention': - return ``; - case 'roomReference': - return `${messageElement.roomName}`; - default: - return messageElement.content; - } - }) - .join(''); - - return { - html: `${html}`, - text: reportAction?.message ? reportAction?.message[0].text : '', - type: CONST.REPORT.MESSAGE.TYPE.COMMENT, - }; -} - -function getMemberChangeMessagePlainText(reportAction: OnyxEntry): string { - const messageElements = getMemberChangeMessageElements(reportAction); - return messageElements.map((element) => element.content).join(''); -} - /** * Helper method to determine if the provided accountID has made a request on the specified report. * @@ -804,9 +711,7 @@ export { shouldReportActionBeVisibleAsLastAction, hasRequestFromCurrentAccount, getFirstVisibleReportActionID, - isMemberChangeAction, - getMemberChangeMessageFragment, - getMemberChangeMessagePlainText, + isChannelLogMemberAction, isReimbursementDeQueuedAction, }; diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index b50b4611a249..1266f145de30 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -18,7 +18,7 @@ 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 {Errors, Icon, PendingAction} from '@src/types/onyx/OnyxCommon'; -import {IOUMessage, OriginalMessageActionName} from '@src/types/onyx/OriginalMessage'; +import {ChangeLog, IOUMessage, OriginalMessageActionName} from '@src/types/onyx/OriginalMessage'; import {Message, ReportActions} from '@src/types/onyx/ReportAction'; import {Receipt, WaypointCollection} from '@src/types/onyx/Transaction'; import DeepValueOf from '@src/types/utils/DeepValueOf'; @@ -3858,7 +3858,7 @@ function getWhisperDisplayNames(participantAccountIDs?: number[]): string | unde * Show subscript on workspace chats / threads and expense requests */ function shouldReportShowSubscript(report: OnyxEntry): boolean { - if (isArchivedRoom(report)) { + if (isArchivedRoom(report) && !isWorkspaceThread(report)) { return false; } @@ -4174,6 +4174,44 @@ function getIOUReportActionDisplayMessage(reportAction: OnyxEntry) }); } +/** + * Return room channel log display message + */ +function getChannelLogMemberMessage(reportAction: OnyxEntry): string { + const verb = + reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.ROOMCHANGELOG.INVITE_TO_ROOM || reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICYCHANGELOG.INVITE_TO_ROOM + ? 'invited' + : 'removed'; + + const mentions = (reportAction?.originalMessage as ChangeLog)?.targetAccountIDs?.map(() => { + const personalDetail = allPersonalDetails?.accountID; + const displayNameOrLogin = LocalePhoneNumber.formatPhoneNumber(personalDetail?.login ?? '') || (personalDetail?.displayName ?? '') || Localize.translateLocal('common.hidden'); + return `@${displayNameOrLogin}`; + }); + + const lastMention = mentions?.pop(); + let message = ''; + + if (mentions?.length === 0) { + message = `${verb} ${lastMention}`; + } else if (mentions?.length === 1) { + message = `${verb} ${mentions?.[0]} and ${lastMention}`; + } else { + message = `${verb} ${mentions?.join(', ')}, and ${lastMention}`; + } + + const roomName = (reportAction?.originalMessage as ChangeLog)?.roomName ?? ''; + if (roomName) { + const preposition = + reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.ROOMCHANGELOG.INVITE_TO_ROOM || reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICYCHANGELOG.INVITE_TO_ROOM + ? ' to' + : ' from'; + message += `${preposition} ${roomName}`; + } + + return message; +} + /** * Checks if a report is a group chat. * @@ -4408,6 +4446,7 @@ export { getReimbursementQueuedActionMessage, getReimbursementDeQueuedActionMessage, getPersonalDetailsForAccountID, + getChannelLogMemberMessage, getRoom, shouldDisableWelcomeMessage, navigateToPrivateNotes, diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index 6e382e11b49b..bace29e06d28 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -375,17 +375,17 @@ function getOptionData( const targetAccountIDs = lastAction?.originalMessage?.targetAccountIDs ?? []; const verb = lastAction.actionName === CONST.REPORT.ACTIONS.TYPE.ROOMCHANGELOG.INVITE_TO_ROOM || lastAction.actionName === CONST.REPORT.ACTIONS.TYPE.POLICYCHANGELOG.INVITE_TO_ROOM - ? Localize.translate(preferredLocale, 'workspace.invite.invited') - : Localize.translate(preferredLocale, 'workspace.invite.removed'); - const users = Localize.translate(preferredLocale, targetAccountIDs.length > 1 ? 'workspace.invite.users' : 'workspace.invite.user'); + ? 'invited' + : 'removed'; + const users = targetAccountIDs.length > 1 ? 'users' : 'user'; result.alternateText = `${verb} ${targetAccountIDs.length} ${users}`; const roomName = lastAction?.originalMessage?.roomName ?? ''; if (roomName) { const preposition = lastAction.actionName === CONST.REPORT.ACTIONS.TYPE.ROOMCHANGELOG.INVITE_TO_ROOM || lastAction.actionName === CONST.REPORT.ACTIONS.TYPE.POLICYCHANGELOG.INVITE_TO_ROOM - ? ` ${Localize.translate(preferredLocale, 'workspace.invite.to')}` - : ` ${Localize.translate(preferredLocale, 'workspace.invite.from')}`; + ? ' to' + : ' from'; result.alternateText += `${preposition} ${roomName}`; } } else if (lastAction?.actionName !== CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW && lastActorDisplayName && lastMessageTextFromReport) { diff --git a/src/libs/ValidationUtils.ts b/src/libs/ValidationUtils.ts index 0adacac4035a..388020bc0d6d 100644 --- a/src/libs/ValidationUtils.ts +++ b/src/libs/ValidationUtils.ts @@ -30,6 +30,17 @@ function validateCardNumber(value: string): boolean { return sum % 10 === 0; } +/** + * Validating that this is a valid address (PO boxes are not allowed) + */ +function isValidAddress(value: string): boolean { + if (!CONST.REGEX.ANY_VALUE.test(value)) { + return false; + } + + return !CONST.REGEX.PO_BOX.test(value); +} + /** * Validate date fields */ @@ -193,6 +204,40 @@ function isValidWebsite(url: string): boolean { return new RegExp(`^${URL_REGEX_WITH_REQUIRED_PROTOCOL}$`, 'i').test(url) && isLowerCase; } +function validateIdentity(identity: Record): Record { + const requiredFields = ['firstName', 'lastName', 'street', 'city', 'zipCode', 'state', 'ssnLast4', 'dob']; + const errors: Record = {}; + + // Check that all required fields are filled + requiredFields.forEach((fieldName) => { + if (isRequiredFulfilled(identity[fieldName])) { + return; + } + errors[fieldName] = true; + }); + + if (!isValidAddress(identity.street)) { + errors.street = true; + } + + if (!isValidZipCode(identity.zipCode)) { + errors.zipCode = true; + } + + // dob field has multiple validations/errors, we are handling it temporarily like this. + if (!isValidDate(identity.dob) || !meetsMaximumAgeRequirement(identity.dob)) { + errors.dob = true; + } else if (!meetsMinimumAgeRequirement(identity.dob)) { + errors.dobAge = true; + } + + if (!isValidSSNLastFour(identity.ssnLast4)) { + errors.ssnLast4 = true; + } + + return errors; +} + function isValidUSPhone(phoneNumber = '', isCountryCodeOptional?: boolean): boolean { const phone = phoneNumber || ''; const regionCode = isCountryCodeOptional ? CONST.COUNTRY.US : undefined; @@ -259,51 +304,6 @@ function isValidPersonName(value: string) { return /^[^\d^!#$%*=<>;{}"]+$/.test(value); } -/** - * Validating that this is a valid address (PO boxes are not allowed) - */ -function isValidAddress(value: string): boolean { - if (!isValidLegalName(value)) { - return false; - } - - return !CONST.REGEX.PO_BOX.test(value); -} - -function validateIdentity(identity: Record): Record { - const requiredFields = ['firstName', 'lastName', 'street', 'city', 'zipCode', 'state', 'ssnLast4', 'dob']; - const errors: Record = {}; - - // Check that all required fields are filled - requiredFields.forEach((fieldName) => { - if (isRequiredFulfilled(identity[fieldName])) { - return; - } - errors[fieldName] = true; - }); - - if (!isValidAddress(identity.street)) { - errors.street = true; - } - - if (!isValidZipCode(identity.zipCode)) { - errors.zipCode = true; - } - - // dob field has multiple validations/errors, we are handling it temporarily like this. - if (!isValidDate(identity.dob) || !meetsMaximumAgeRequirement(identity.dob)) { - errors.dob = true; - } else if (!meetsMinimumAgeRequirement(identity.dob)) { - errors.dobAge = true; - } - - if (!isValidSSNLastFour(identity.ssnLast4)) { - errors.ssnLast4 = true; - } - - return errors; -} - /** * Checks if the provided string includes any of the provided reserved words */ @@ -384,6 +384,7 @@ export { meetsMinimumAgeRequirement, meetsMaximumAgeRequirement, getAgeRequirementError, + isValidAddress, isValidDate, isValidPastDate, isValidSecurityCode, @@ -395,6 +396,7 @@ export { getFieldRequiredErrors, isValidUSPhone, isValidWebsite, + validateIdentity, isValidTwoFactorCode, isNumericWithSpecialChars, isValidRoutingNumber, @@ -407,8 +409,6 @@ export { isValidValidateCode, isValidDisplayName, isValidLegalName, - isValidAddress, - validateIdentity, doesContainReservedWord, isNumeric, isValidAccountRoute, diff --git a/src/libs/actions/Report.js b/src/libs/actions/Report.js index 6161fd2066ff..e5bac3182b0d 100644 --- a/src/libs/actions/Report.js +++ b/src/libs/actions/Report.js @@ -2044,6 +2044,11 @@ function leaveRoom(reportID, isWorkspaceMemberLeavingWorkspaceRoom = false) { value: { stateNum: CONST.REPORT.STATE_NUM.SUBMITTED, statusNum: CONST.REPORT.STATUS.CLOSED, + chatType: report.chatType, + parentReportID: report.parentReportID, + parentReportActionID: report.parentReportActionID, + policyID: report.policyID, + type: report.type, }, }, ]; diff --git a/src/pages/EditSplitBillPage.js b/src/pages/EditSplitBillPage.js index c4e47e2d4c35..3e5a5e7f5d53 100644 --- a/src/pages/EditSplitBillPage.js +++ b/src/pages/EditSplitBillPage.js @@ -13,9 +13,12 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import EditRequestAmountPage from './EditRequestAmountPage'; +import EditRequestCategoryPage from './EditRequestCategoryPage'; import EditRequestCreatedPage from './EditRequestCreatedPage'; import EditRequestDescriptionPage from './EditRequestDescriptionPage'; import EditRequestMerchantPage from './EditRequestMerchantPage'; +import EditRequestTagPage from './EditRequestTagPage'; +import reportPropTypes from './reportPropTypes'; const propTypes = { /** Route from navigation */ @@ -38,13 +41,16 @@ const propTypes = { /** The draft transaction that holds data to be persisted on the current transaction */ draftTransaction: transactionPropTypes, + + /** The report currently being used */ + report: reportPropTypes.isRequired, }; const defaultProps = { draftTransaction: undefined, }; -function EditSplitBillPage({route, transaction, draftTransaction}) { +function EditSplitBillPage({route, transaction, draftTransaction, report}) { const fieldToEdit = lodashGet(route, ['params', 'field'], ''); const reportID = lodashGet(route, ['params', 'reportID'], ''); const reportActionID = lodashGet(route, ['params', 'reportActionID'], ''); @@ -55,6 +61,8 @@ function EditSplitBillPage({route, transaction, draftTransaction}) { comment: transactionDescription, merchant: transactionMerchant, created: transactionCreated, + category: transactionCategory, + tag: transactionTag, } = draftTransaction ? ReportUtils.getTransactionDetails(draftTransaction) : ReportUtils.getTransactionDetails(transaction); const defaultCurrency = lodashGet(route, 'params.currency', '') || transactionCurrency; @@ -130,6 +138,30 @@ function EditSplitBillPage({route, transaction, draftTransaction}) { ); } + if (fieldToEdit === CONST.EDIT_REQUEST_FIELD.CATEGORY) { + return ( + { + setDraftSplitTransaction({category: transactionChanges.category.trim()}); + }} + /> + ); + } + + if (fieldToEdit === CONST.EDIT_REQUEST_FIELD.TAG) { + return ( + { + setDraftSplitTransaction({tag: transactionChanges.tag.trim()}); + }} + /> + ); + } + return ; } @@ -142,6 +174,9 @@ export default compose( key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${route.params.reportID}`, canEvict: false, }, + report: { + key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT}${route.params.reportID}`, + }, }), // eslint-disable-next-line rulesdir/no-multiple-onyx-in-file withOnyx({ diff --git a/src/pages/ReimbursementAccount/CompanyStep.js b/src/pages/ReimbursementAccount/CompanyStep.js index d7d622d309d6..f1d62eef89ae 100644 --- a/src/pages/ReimbursementAccount/CompanyStep.js +++ b/src/pages/ReimbursementAccount/CompanyStep.js @@ -88,10 +88,6 @@ function CompanyStep({reimbursementAccount, reimbursementAccountDraft, getDefaul ]; const errors = ValidationUtils.getFieldRequiredErrors(values, requiredFields); - if (values.companyName && !ValidationUtils.isValidLegalName(values.companyName)) { - errors.companyName = 'bankAccount.error.companyName'; - } - if (values.addressStreet && !ValidationUtils.isValidAddress(values.addressStreet)) { errors.addressStreet = 'bankAccount.error.addressStreet'; } @@ -100,10 +96,6 @@ function CompanyStep({reimbursementAccount, reimbursementAccountDraft, getDefaul errors.addressZipCode = 'bankAccount.error.zipCode'; } - if (values.addressCity && !ValidationUtils.isValidLegalName(values.addressCity)) { - errors.addressCity = 'bankAccount.error.addressCity'; - } - if (values.companyPhone && !ValidationUtils.isValidUSPhone(values.companyPhone, true)) { errors.companyPhone = 'bankAccount.error.phoneNumber'; } diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index e9b79c41dcd7..8db899a8f73f 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -477,7 +477,7 @@ export default compose( reportActions: { key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${getReportID(route)}`, canEvict: false, - selector: ReportActionsUtils.getSortedReportActionsForDisplay, + selector: (reportActions) => ReportActionsUtils.getSortedReportActionsForDisplay(reportActions, true), }, report: { key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT}${getReportID(route)}`, diff --git a/src/pages/home/report/ContextMenu/ContextMenuActions.js b/src/pages/home/report/ContextMenu/ContextMenuActions.js index 6c645bc87486..4f35926c5957 100644 --- a/src/pages/home/report/ContextMenu/ContextMenuActions.js +++ b/src/pages/home/report/ContextMenu/ContextMenuActions.js @@ -281,8 +281,8 @@ export default [ } else if (ReportActionsUtils.isMoneyRequestAction(reportAction)) { const displayMessage = ReportUtils.getIOUReportActionDisplayMessage(reportAction); Clipboard.setString(displayMessage); - } else if (ReportActionsUtils.isMemberChangeAction(reportAction)) { - const logMessage = ReportActionsUtils.getMemberChangeMessagePlainText(reportAction); + } else if (ReportActionsUtils.isChannelLogMemberAction(reportAction)) { + const logMessage = ReportUtils.getChannelLogMemberMessage(reportAction); Clipboard.setString(logMessage); } else if (content) { const parser = new ExpensiMark(); diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.js index 92bb370155c9..9fc7eb2513d4 100644 --- a/src/pages/home/report/ReportActionItem.js +++ b/src/pages/home/report/ReportActionItem.js @@ -568,15 +568,9 @@ function ReportActionItem(props) { }; if (props.action.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED) { - let content = ( - - ); const parentReportAction = ReportActionsUtils.getParentReportAction(props.report); if (ReportActionsUtils.isTransactionThread(parentReportAction)) { - content = ( + return ( @@ -602,22 +596,21 @@ function ReportActionItem(props) { ); - } else { - content = ( - <> - - - - - - ); } + return ( + <> + + + + + + ); } if (ReportUtils.isExpenseReport(props.report) || ReportUtils.isIOUReport(props.report)) { - content = ( + return ( {content}; + return ( + + ); } if (props.action.actionName === CONST.REPORT.ACTIONS.TYPE.RENAMED) { return ; diff --git a/src/pages/home/report/ReportActionItemDraft.js b/src/pages/home/report/ReportActionItemDraft.js deleted file mode 100644 index 9b3839aa78f2..000000000000 --- a/src/pages/home/report/ReportActionItemDraft.js +++ /dev/null @@ -1,22 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import {View} from 'react-native'; -import useThemeStyles from '@styles/useThemeStyles'; - -const propTypes = { - /** Children view component for this action item */ - children: PropTypes.node.isRequired, -}; - -function ReportActionItemDraft(props) { - const styles = useThemeStyles(); - return ( - - {props.children} - - ); -} - -ReportActionItemDraft.propTypes = propTypes; -ReportActionItemDraft.displayName = 'ReportActionItemDraft'; -export default ReportActionItemDraft; diff --git a/src/pages/home/report/ReportActionItemDraft.tsx b/src/pages/home/report/ReportActionItemDraft.tsx new file mode 100644 index 000000000000..b46af5401ee4 --- /dev/null +++ b/src/pages/home/report/ReportActionItemDraft.tsx @@ -0,0 +1,17 @@ +import React from 'react'; +import {View} from 'react-native'; +import useThemeStyles from '@styles/useThemeStyles'; +import ChildrenProps from '@src/types/utils/ChildrenProps'; + +function ReportActionItemDraft({children}: ChildrenProps) { + const styles = useThemeStyles(); + + return ( + + {children} + + ); +} + +ReportActionItemDraft.displayName = 'ReportActionItemDraft'; +export default ReportActionItemDraft; diff --git a/src/pages/home/report/ReportActionItemMessage.js b/src/pages/home/report/ReportActionItemMessage.js index 46e0438f250a..2265530f29a1 100644 --- a/src/pages/home/report/ReportActionItemMessage.js +++ b/src/pages/home/report/ReportActionItemMessage.js @@ -8,7 +8,6 @@ import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import * as ReportUtils from '@libs/ReportUtils'; import useThemeStyles from '@styles/useThemeStyles'; import CONST from '@src/CONST'; -import TextCommentFragment from './comment/TextCommentFragment'; import ReportActionItemFragment from './ReportActionItemFragment'; import reportActionPropTypes from './reportActionPropTypes'; @@ -41,20 +40,6 @@ function ReportActionItemMessage(props) { const styles = useThemeStyles(); const fragments = _.compact(props.action.previousMessage || props.action.message); const isIOUReport = ReportActionsUtils.isMoneyRequestAction(props.action); - if (ReportActionsUtils.isMemberChangeAction(props.action)) { - const fragment = ReportActionsUtils.getMemberChangeMessageFragment(props.action); - - return ( - - ); - } - let iouMessage; if (isIOUReport) { const iouReportID = lodashGet(props.action, 'originalMessage.IOUReportID'); diff --git a/src/pages/home/report/ReportActionsList.js b/src/pages/home/report/ReportActionsList.js index 317fa86f23f3..183665891929 100644 --- a/src/pages/home/report/ReportActionsList.js +++ b/src/pages/home/report/ReportActionsList.js @@ -156,7 +156,7 @@ function ReportActionsList({ const readActionSkipped = useRef(false); const hasHeaderRendered = useRef(false); const hasFooterRendered = useRef(false); - const reportActionSize = useRef(sortedReportActions.length); + const lastVisibleActionCreatedRef = useRef(report.lastVisibleActionCreated); const lastReadTimeRef = useRef(report.lastReadTime); const linkedReportActionID = lodashGet(route, 'params.reportActionID', ''); @@ -198,15 +198,15 @@ function ReportActionsList({ } } - if (currentUnreadMarker || reportActionSize.current === sortedReportActions.length) { + if (currentUnreadMarker || lastVisibleActionCreatedRef.current === report.lastVisibleActionCreated) { return; } cacheUnreadMarkers.delete(report.reportID); - reportActionSize.current = sortedReportActions.length; + lastVisibleActionCreatedRef.current = report.lastVisibleActionCreated; setCurrentUnreadMarker(null); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [sortedReportActions.length, report.reportID]); + }, [report.lastVisibleActionCreated, report.reportID]); useEffect(() => { if (!userActiveSince.current || report.reportID !== prevReportID) { @@ -339,7 +339,10 @@ function ReportActionsList({ shouldDisplay = isCurrentMessageUnread && (!nextMessage || !isMessageUnread(nextMessage, lastReadTimeRef.current)); if (shouldDisplay && !messageManuallyMarkedUnread) { const isWithinVisibleThreshold = scrollingVerticalOffset.current < MSG_VISIBLE_THRESHOLD ? reportAction.created < userActiveSince.current : true; - shouldDisplay = reportAction.actorAccountID !== Report.getCurrentUserAccountID() && isWithinVisibleThreshold; + // Prevent displaying a new marker line when report action is of type "REPORTPREVIEW" and last actor is the current user + shouldDisplay = + (ReportActionsUtils.isReportPreviewAction(reportAction) ? !reportAction.childLastActorAccountID : reportAction.actorAccountID) !== Report.getCurrentUserAccountID() && + isWithinVisibleThreshold; } if (shouldDisplay) { cacheUnreadMarkers.set(report.reportID, reportAction.reportActionID); diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index 607d98039070..e7b293babdf5 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -96,6 +96,7 @@ function ReportActionsView(props) { const isFocused = useIsFocused(); const reportID = props.report.reportID; + const hasNewestReportAction = lodashGet(props.reportActions[0], 'isNewestReportAction'); /** * @returns {Boolean} @@ -200,7 +201,7 @@ function ReportActionsView(props) { const loadNewerChats = useMemo( () => _.throttle(({distanceFromStart}) => { - if (props.isLoadingNewerReportActions || props.isLoadingInitialReportActions) { + if (props.isLoadingNewerReportActions || props.isLoadingInitialReportActions || hasNewestReportAction) { return; } @@ -222,7 +223,7 @@ function ReportActionsView(props) { const newestReportAction = _.first(props.reportActions); Report.getNewerActions(reportID, newestReportAction.reportActionID); }, 500), - [props.isLoadingNewerReportActions, props.isLoadingInitialReportActions, props.reportActions, reportID], + [props.isLoadingNewerReportActions, props.isLoadingInitialReportActions, props.reportActions, reportID, hasNewestReportAction], ); /** diff --git a/src/pages/workspace/SearchInputManager.js b/src/pages/workspace/SearchInputManager.js new file mode 100644 index 000000000000..599f7cca6cf9 --- /dev/null +++ b/src/pages/workspace/SearchInputManager.js @@ -0,0 +1,5 @@ +// eslint-disable-next-line prefer-const +let searchInput = ''; +export default { + searchInput, +}; diff --git a/src/pages/workspace/WorkspaceInvitePage.js b/src/pages/workspace/WorkspaceInvitePage.js index 4bef69c82414..b18c234ea44d 100644 --- a/src/pages/workspace/WorkspaceInvitePage.js +++ b/src/pages/workspace/WorkspaceInvitePage.js @@ -21,6 +21,7 @@ import * as Policy from '@userActions/Policy'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import SearchInputManager from './SearchInputManager'; import {policyDefaultProps, policyPropTypes} from './withPolicy'; import withPolicyAndFullscreenLoading from './withPolicyAndFullscreenLoading'; @@ -75,6 +76,13 @@ function WorkspaceInvitePage(props) { Policy.openWorkspaceInvitePage(props.route.params.policyID, _.keys(policyMemberEmailsToAccountIDs)); }; + useEffect(() => { + if (!SearchInputManager.searchInput) { + return; + } + setSearchTerm(SearchInputManager.searchInput); + }, []); + useEffect(() => { Policy.clearErrors(props.route.params.policyID); openWorkspaceInvitePage(); @@ -255,7 +263,10 @@ function WorkspaceInvitePage(props) { sections={sections} textInputLabel={translate('optionsSelector.nameEmailOrPhoneNumber')} textInputValue={searchTerm} - onChangeText={setSearchTerm} + onChangeText={(value) => { + SearchInputManager.searchInput = value; + setSearchTerm(value); + }} headerMessage={headerMessage} onSelectRow={toggleOption} onConfirm={inviteUser} diff --git a/src/pages/workspace/WorkspaceMembersPage.js b/src/pages/workspace/WorkspaceMembersPage.js index 09b613350705..d5cdbcfc69d8 100644 --- a/src/pages/workspace/WorkspaceMembersPage.js +++ b/src/pages/workspace/WorkspaceMembersPage.js @@ -1,3 +1,4 @@ +import {useIsFocused} from '@react-navigation/native'; import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; @@ -32,6 +33,7 @@ import * as Policy from '@userActions/Policy'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import SearchInputManager from './SearchInputManager'; import {policyDefaultProps, policyPropTypes} from './withPolicy'; import withPolicyAndFullscreenLoading from './withPolicyAndFullscreenLoading'; @@ -85,6 +87,17 @@ function WorkspaceMembersPage(props) { const isOfflineAndNoMemberDataAvailable = _.isEmpty(props.policyMembers) && props.network.isOffline; const prevPersonalDetails = usePrevious(props.personalDetails); + const isFocusedScreen = useIsFocused(); + + useEffect(() => { + if (!SearchInputManager.searchInput) { + return; + } + setSearchValue(SearchInputManager.searchInput); + }, [isFocusedScreen]); + + useEffect(() => () => (SearchInputManager.searchInput = ''), []); + /** * Get filtered personalDetails list with current policyMembers * @param {Object} policyMembers @@ -466,7 +479,10 @@ function WorkspaceMembersPage(props) { sections={[{data, indexOffset: 0, isDisabled: false}]} textInputLabel={props.translate('optionsSelector.findMember')} textInputValue={searchValue} - onChangeText={setSearchValue} + onChangeText={(value) => { + SearchInputManager.searchInput = value; + setSearchValue(value); + }} headerMessage={getHeaderMessage()} headerContent={getHeaderContent()} onSelectRow={(item) => toggleUser(item.accountID)} diff --git a/src/types/onyx/OriginalMessage.ts b/src/types/onyx/OriginalMessage.ts index 72ea275e3ba3..f76fbd5ffd7d 100644 --- a/src/types/onyx/OriginalMessage.ts +++ b/src/types/onyx/OriginalMessage.ts @@ -140,7 +140,6 @@ type ChronosOOOTimestamp = { type ChangeLog = { targetAccountIDs?: number[]; roomName?: string; - reportID?: number; }; type ChronosOOOEvent = { diff --git a/src/types/onyx/ReportAction.ts b/src/types/onyx/ReportAction.ts index 64e1eb0b7c88..a0e90f4e9c34 100644 --- a/src/types/onyx/ReportAction.ts +++ b/src/types/onyx/ReportAction.ts @@ -115,12 +115,13 @@ type ReportActionBase = { childStateNum?: ValueOf; childLastReceiptTransactionIDs?: string; childLastMoneyRequestComment?: string; + childLastActorAccountID?: number; timestamp?: number; reportActionTimestamp?: number; childMoneyRequestCount?: number; isFirstItem?: boolean; - /** Informations about attachments of report action */ + /** Information about attachments of report action */ attachmentInfo?: (File & {source: string; uri: string}) | Record; /** Receipt tied to report action */ @@ -138,6 +139,9 @@ type ReportActionBase = { isAttachment?: boolean; childRecentReceiptTransactionIDs?: Record; reportID?: string; + + /** We manually add this field while sorting to detect the end of the list */ + isNewestReportAction?: boolean; }; type ReportAction = ReportActionBase & OriginalMessage; diff --git a/tests/unit/LocalizeTests.js b/tests/unit/LocalizeTests.js index 7693a0a4a88d..4c89d587fc06 100644 --- a/tests/unit/LocalizeTests.js +++ b/tests/unit/LocalizeTests.js @@ -15,7 +15,7 @@ describe('localize', () => { afterEach(() => Onyx.clear()); - describe('formatList', () => { + describe('arrayToString', () => { test.each([ [ [], @@ -52,9 +52,9 @@ describe('localize', () => { [CONST.LOCALES.ES]: 'rory, vit e ionatan', }, ], - ])('formatList(%s)', (input, {[CONST.LOCALES.DEFAULT]: expectedOutput, [CONST.LOCALES.ES]: expectedOutputES}) => { - expect(Localize.formatList(input)).toBe(expectedOutput); - return Onyx.set(ONYXKEYS.NVP_PREFERRED_LOCALE, CONST.LOCALES.ES).then(() => expect(Localize.formatList(input)).toBe(expectedOutputES)); + ])('arrayToSpokenList(%s)', (input, {[CONST.LOCALES.DEFAULT]: expectedOutput, [CONST.LOCALES.ES]: expectedOutputES}) => { + expect(Localize.arrayToString(input)).toBe(expectedOutput); + return Onyx.set(ONYXKEYS.NVP_PREFERRED_LOCALE, CONST.LOCALES.ES).then(() => expect(Localize.arrayToString(input)).toBe(expectedOutputES)); }); }); }); diff --git a/tests/unit/ReportActionsUtilsTest.js b/tests/unit/ReportActionsUtilsTest.js index 9973515c44de..b8b6eb5e7673 100644 --- a/tests/unit/ReportActionsUtilsTest.js +++ b/tests/unit/ReportActionsUtilsTest.js @@ -185,10 +185,71 @@ describe('ReportActionsUtils', () => { message: [{html: 'I have changed the task'}], }, ]; + const result = ReportActionsUtils.getSortedReportActionsForDisplay(input); input.pop(); expect(result).toStrictEqual(input); }); + + describe('getSortedReportActionsForDisplay with marked the first reportAction', () => { + it('should filter out non-whitelisted actions', () => { + const input = [ + { + created: '2022-11-13 22:27:01.825', + reportActionID: '8401445780099176', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + message: [{html: 'Hello world'}], + }, + { + created: '2022-11-12 22:27:01.825', + reportActionID: '6401435781022176', + actionName: CONST.REPORT.ACTIONS.TYPE.CREATED, + message: [{html: 'Hello world'}], + }, + { + created: '2022-11-11 22:27:01.825', + reportActionID: '2962390724708756', + actionName: CONST.REPORT.ACTIONS.TYPE.IOU, + message: [{html: 'Hello world'}], + }, + { + created: '2022-11-10 22:27:01.825', + reportActionID: '1609646094152486', + actionName: CONST.REPORT.ACTIONS.TYPE.RENAMED, + message: [{html: 'Hello world'}], + }, + { + created: '2022-11-09 22:27:01.825', + reportActionID: '8049485084562457', + actionName: CONST.REPORT.ACTIONS.TYPE.POLICYCHANGELOG.UPDATE_FIELD, + message: [{html: 'updated the Approval Mode from "Submit and Approve" to "Submit and Close"'}], + }, + { + created: '2022-11-08 22:27:06.825', + reportActionID: '1661970171066216', + actionName: CONST.REPORT.ACTIONS.TYPE.REIMBURSEMENTQUEUED, + message: [{html: 'Waiting for the bank account'}], + }, + { + created: '2022-11-06 22:27:08.825', + reportActionID: '1661970171066220', + actionName: CONST.REPORT.ACTIONS.TYPE.TASKEDITED, + message: [{html: 'I have changed the task'}], + }, + ]; + + const resultWithoutNewestFlag = ReportActionsUtils.getSortedReportActionsForDisplay(input); + const resultWithNewestFlag = ReportActionsUtils.getSortedReportActionsForDisplay(input, true); + input.pop(); + // Mark the newest report action as the newest report action + resultWithoutNewestFlag[0] = { + ...resultWithoutNewestFlag[0], + isNewestReportAction: true, + }; + expect(resultWithoutNewestFlag).toStrictEqual(resultWithNewestFlag); + }); + }); + it('should filter out closed actions', () => { const input = [ {