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 = [
{