Skip to content

Commit

Permalink
Merge branch 'main' of github.com:kubabutkiewicz/expensify-app into f…
Browse files Browse the repository at this point in the history
…ix-regression-after-task-migration
  • Loading branch information
kubabutkiewicz committed Jan 26, 2024
2 parents 44b5f59 + e485d9c commit 8483009
Show file tree
Hide file tree
Showing 25 changed files with 355 additions and 191 deletions.
2 changes: 1 addition & 1 deletion contributingGuides/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ Additionally if you want to discuss an idea with the open source community witho
```
11. [Open a pull request](https://docs.github.com/en/free-pro-team@latest/github/collaborating-with-issues-and-pull-requests/creating-a-pull-request-from-a-fork), and make sure to fill in the required fields.
12. An Expensify engineer and a member from the Contributor-Plus team will be assigned to your pull request automatically to review.
13. Daily updates on weekdays are highly recommended. If you know you won’t be able to provide updates for > 1 week, please comment on the PR or issue how long you plan to be out so that we may plan accordingly. We understand everyone needs a little vacation here and there. Any issue that doesn't receive an update for 1 full week may be considered abandoned and the original contract terminated.
13. Daily updates on weekdays are highly recommended. If you know you won’t be able to provide updates within 48 hours, please comment on the PR or issue stating how long you plan to be out so that we may plan accordingly. We understand everyone needs a little vacation here and there. Any issue that doesn't receive an update for 5 days (including weekend days) may be considered abandoned and the original contract terminated.
#### Submit your pull request for final review
14. When you are ready to submit your pull request for final review, make sure the following checks pass:
Expand Down
7 changes: 0 additions & 7 deletions src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3057,13 +3057,6 @@ const CONST = {
*/
MAX_OPTIONS_SELECTOR_PAGE_LENGTH: 500,

/**
* Performance test setup - run the same test multiple times to get a more accurate result
*/
PERFORMANCE_TESTS: {
RUNS: 20,
},

/**
* Bank account names
*/
Expand Down
5 changes: 5 additions & 0 deletions src/components/SelectionList/BaseSelectionList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,11 @@ function BaseSelectionList<TItem extends User | RadioItem>(
return;
}

// scroll is unnecessary if multiple options cannot be selected
if (!canSelectMultiple) {
return;
}

// set the focus on the first item when the sections list is changed
if (sections.length > 0) {
updateAndScrollToFocusedIndex(0);
Expand Down
28 changes: 27 additions & 1 deletion src/libs/ComposerUtils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,14 @@ function insertText(text: string, selection: Selection, textToInsert: string): s
return text.slice(0, selection.start) + textToInsert + text.slice(selection.end, text.length);
}

/**
* Insert a white space at given index of text
* @param text - text that needs whitespace to be appended to
*/
function insertWhiteSpaceAtIndex(text: string, index: number) {
return `${text.slice(0, index)} ${text.slice(index)}`;
}

/**
* Check whether we can skip trigger hotkeys on some specific devices.
*/
Expand All @@ -23,4 +31,22 @@ function canSkipTriggerHotkeys(isSmallScreenWidth: boolean, isKeyboardShown: boo
return (isSmallScreenWidth && DeviceCapabilities.canUseTouchScreen()) || isKeyboardShown;
}

export {getNumberOfLines, updateNumberOfLines, insertText, canSkipTriggerHotkeys};
/**
* Finds the length of common suffix between two texts
*/
function findCommonSuffixLength(str1: string, str2: string, cursorPosition: number) {
let commonSuffixLength = 0;
const minLength = Math.min(str1.length - cursorPosition, str2.length);

for (let i = 1; i <= minLength; i++) {
if (str1.charAt(str1.length - i) === str2.charAt(str2.length - i)) {
commonSuffixLength++;
} else {
break;
}
}

return commonSuffixLength;
}

export {getNumberOfLines, updateNumberOfLines, insertText, canSkipTriggerHotkeys, insertWhiteSpaceAtIndex, findCommonSuffixLength};
2 changes: 1 addition & 1 deletion src/libs/OptionsListUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1563,7 +1563,7 @@ function getOptions(
if (includePersonalDetails) {
// Next loop over all personal details removing any that are selectedUsers or recentChats
allPersonalDetailsOptions.forEach((personalDetailOption) => {
if (optionsToExclude.some((optionToExclude) => optionToExclude.login === personalDetailOption.login)) {
if (optionsToExclude.some((optionToExclude) => optionToExclude.login === addSMSDomainIfPhoneNumber(personalDetailOption.login ?? ''))) {
return;
}
const {searchText, participantsList, isChatRoom} = personalDetailOption;
Expand Down
39 changes: 30 additions & 9 deletions src/libs/actions/IOU.js
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,29 @@ function getReceiptError(receipt, filename, isScanRequest = true) {
: ErrorUtils.getMicroSecondOnyxErrorObject({error: CONST.IOU.RECEIPT_ERROR, source: receipt.source, filename});
}

/**
* Return the object to update hasOutstandingChildRequest
* @param {Object} [policy]
* @param {Boolean} needsToBeManuallySubmitted
* @returns {Object}
*/
function getOutstandingChildRequest(policy, needsToBeManuallySubmitted) {
if (!needsToBeManuallySubmitted) {
return {
hasOutstandingChildRequest: false,
};
}

if (PolicyUtils.isPolicyAdmin(policy)) {
return {
hasOutstandingChildRequest: true,
};
}

// We don't need to update hasOutstandingChildRequest in this case
return {};
}

/**
* Builds the Onyx data for a money request.
*
Expand All @@ -329,7 +352,7 @@ function getReceiptError(receipt, filename, isScanRequest = true) {
* @param {Object} policy - May be undefined, an empty object, or an object matching the Policy type (src/types/onyx/Policy.ts)
* @param {Array} policyTags
* @param {Array} policyCategories
* @param {Boolean} hasOutstandingChildRequest
* @param {Boolean} needsToBeManuallySubmitted
* @returns {Array} - An array containing the optimistic data, success data, and failure data.
*/
function buildOnyxDataForMoneyRequest(
Expand All @@ -348,9 +371,10 @@ function buildOnyxDataForMoneyRequest(
policy,
policyTags,
policyCategories,
hasOutstandingChildRequest = false,
needsToBeManuallySubmitted = true,
) {
const isScanRequest = TransactionUtils.isScanRequest(transaction);
const outstandingChildRequest = getOutstandingChildRequest(needsToBeManuallySubmitted, policy);
const optimisticData = [
{
// Use SET for new reports because it doesn't exist yet, is faster and we need the data to be available when we navigate to the chat page
Expand All @@ -361,7 +385,7 @@ function buildOnyxDataForMoneyRequest(
lastReadTime: DateUtils.getDBTime(),
lastMessageTranslationKey: '',
iouReportID: iouReport.reportID,
hasOutstandingChildRequest,
...outstandingChildRequest,
...(isNewChatReport ? {pendingFields: {createChat: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD}} : {}),
},
},
Expand Down Expand Up @@ -506,6 +530,7 @@ function buildOnyxDataForMoneyRequest(
iouReportID: chatReport.iouReportID,
lastReadTime: chatReport.lastReadTime,
pendingFields: null,
hasOutstandingChildRequest: chatReport.hasOutstandingChildRequest,
...(isNewChatReport
? {
errorFields: {
Expand Down Expand Up @@ -687,7 +712,7 @@ function getMoneyRequestInformation(
let iouReport = isNewIOUReport ? null : allReports[`${ONYXKEYS.COLLECTION.REPORT}${chatReport.iouReportID}`];

// Check if the Scheduled Submit is enabled in case of expense report
let needsToBeManuallySubmitted = false;
let needsToBeManuallySubmitted = true;
let isFromPaidPolicy = false;
if (isPolicyExpenseChat) {
isFromPaidPolicy = PolicyUtils.isPaidGroupPolicy(policy);
Expand Down Expand Up @@ -806,10 +831,6 @@ function getMoneyRequestInformation(
}
: undefined;

// The policy expense chat should have the GBR only when its a paid policy and the scheduled submit is turned off
// so the employee has to submit to their manager manually.
const hasOutstandingChildRequest = isPolicyExpenseChat && needsToBeManuallySubmitted;

// STEP 5: Build Onyx Data
const [optimisticData, successData, failureData] = buildOnyxDataForMoneyRequest(
chatReport,
Expand All @@ -827,7 +848,7 @@ function getMoneyRequestInformation(
policy,
policyTags,
policyCategories,
hasOutstandingChildRequest,
needsToBeManuallySubmitted,
);

return {
Expand Down
34 changes: 32 additions & 2 deletions src/libs/actions/Report.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type {Route} from '@src/ROUTES';
import ROUTES from '@src/ROUTES';
import type {PersonalDetails, PersonalDetailsList, ReportActionReactions, ReportUserIsTyping} from '@src/types/onyx';
import type {PersonalDetails, PersonalDetailsList, ReportActionReactions, ReportMetadata, ReportUserIsTyping} from '@src/types/onyx';
import type {Decision, OriginalMessageIOU} from '@src/types/onyx/OriginalMessage';
import type {NotificationPreference, WriteCapability} from '@src/types/onyx/Report';
import type Report from '@src/types/onyx/Report';
Expand Down Expand Up @@ -125,6 +125,13 @@ Onyx.connect({
},
});

let reportMetadata: OnyxCollection<ReportMetadata> = {};
Onyx.connect({
key: ONYXKEYS.COLLECTION.REPORT_METADATA,
waitForCollectionCallback: true,
callback: (value) => (reportMetadata = value),
});

const allReports: OnyxCollection<Report> = {};
let conciergeChatReportID: string | undefined;
const typingWatchTimers: Record<string, NodeJS.Timeout> = {};
Expand Down Expand Up @@ -2172,7 +2179,30 @@ function leaveRoom(reportID: string, isWorkspaceMemberLeavingWorkspaceRoom = fal

API.write('LeaveRoom', parameters, {optimisticData, successData, failureData});

if (isWorkspaceMemberLeavingWorkspaceRoom) {
const sortedReportsByLastRead = ReportUtils.sortReportsByLastRead(Object.values(allReports ?? {}) as Report[], reportMetadata);

// We want to filter out the current report, hidden reports and empty chats
const filteredReportsByLastRead = sortedReportsByLastRead.filter(
(sortedReport) =>
sortedReport?.reportID !== reportID &&
sortedReport?.notificationPreference !== CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN &&
ReportUtils.shouldReportBeInOptionList({
report: sortedReport,
currentReportId: '',
isInGSDMode: false,
betas: [],
policies: {},
excludeEmptyChats: true,
doesReportHaveViolations: false,
}),
);
const lastAccessedReportID = filteredReportsByLastRead.at(-1)?.reportID;

if (lastAccessedReportID) {
// We should call Navigation.goBack to pop the current route first before navigating to Concierge.
Navigation.goBack(ROUTES.HOME);
Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(lastAccessedReportID));
} else {
const participantAccountIDs = PersonalDetailsUtils.getAccountIDsByLogins([CONST.EMAIL.CONCIERGE]);
const chat = ReportUtils.getChatByParticipants(participantAccountIDs);
if (chat?.reportID) {
Expand Down
6 changes: 3 additions & 3 deletions src/libs/calculateAnchorPosition.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/* eslint-disable no-console */
import type {View} from 'react-native';
/* eslint-disable no-restricted-imports */
import type {Text as RNText, View} from 'react-native';
import type {ValueOf} from 'type-fest';
import CONST from '@src/CONST';
import type {AnchorPosition} from '@src/styles';
Expand All @@ -13,7 +13,7 @@ type AnchorOrigin = {
/**
* Gets the x,y position of the passed in component for the purpose of anchoring another component to it.
*/
export default function calculateAnchorPosition(anchorComponent: View, anchorOrigin?: AnchorOrigin): Promise<AnchorPosition> {
export default function calculateAnchorPosition(anchorComponent: View | RNText, anchorOrigin?: AnchorOrigin): Promise<AnchorPosition> {
return new Promise((resolve) => {
if (!anchorComponent) {
return resolve({horizontal: 0, vertical: 0});
Expand Down
5 changes: 4 additions & 1 deletion src/pages/RoomInvitePage.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,10 @@ function RoomInvitePage(props) {

// Any existing participants and Expensify emails should not be eligible for invitation
const excludedUsers = useMemo(
() => [...PersonalDetailsUtils.getLoginsByAccountIDs(lodashGet(props.report, 'visibleChatMemberAccountIDs', [])), ...CONST.EXPENSIFY_EMAILS],
() =>
_.map([...PersonalDetailsUtils.getLoginsByAccountIDs(lodashGet(props.report, 'visibleChatMemberAccountIDs', [])), ...CONST.EXPENSIFY_EMAILS], (participant) =>
OptionsListUtils.addSMSDomainIfPhoneNumber(participant),
),
[props.report],
);

Expand Down
58 changes: 48 additions & 10 deletions src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import lodashIsEqual from 'lodash/isEqual';
import type {MutableRefObject, RefObject} from 'react';
import React, {memo, useMemo, useRef, useState} from 'react';
import {InteractionManager, View} from 'react-native';
// eslint-disable-next-line no-restricted-imports
import type {GestureResponderEvent, Text as RNText, View as ViewType} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
import {withOnyx} from 'react-native-onyx';
import type {ContextMenuItemHandle} from '@components/ContextMenuItem';
Expand All @@ -12,15 +14,16 @@ import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
import useStyleUtils from '@hooks/useStyleUtils';
import useWindowDimensions from '@hooks/useWindowDimensions';
import * as ReportUtils from '@libs/ReportUtils';
import * as Session from '@userActions/Session';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type {Beta, ReportAction, ReportActions} from '@src/types/onyx';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
import type {ContextMenuActionPayload} from './ContextMenuActions';
import type {ContextMenuAction, ContextMenuActionPayload} from './ContextMenuActions';
import ContextMenuActions from './ContextMenuActions';
import type {ContextMenuType} from './ReportActionContextMenu';
import {hideContextMenu} from './ReportActionContextMenu';
import {hideContextMenu, showContextMenu} from './ReportActionContextMenu';

type BaseReportActionContextMenuOnyxProps = {
/** Beta features list */
Expand Down Expand Up @@ -78,7 +81,11 @@ type BaseReportActionContextMenuProps = BaseReportActionContextMenuOnyxProps & {
/** Content Ref */
contentRef?: RefObject<View>;

/** Function to check if context menu is active */
checkIfContextMenuActive?: () => void;

/** List of disabled actions */
disabledActions?: ContextMenuAction[];
};

type MenuItemRefs = Record<string, ContextMenuItemHandle | null>;
Expand All @@ -100,6 +107,7 @@ function BaseReportActionContextMenu({
betas,
reportActions,
checkIfContextMenuActive,
disabledActions = [],
}: BaseReportActionContextMenuProps) {
const StyleUtils = useStyleUtils();
const {translate} = useLocalize();
Expand All @@ -117,13 +125,22 @@ function BaseReportActionContextMenu({
}, [reportActions, reportActionID]);

const shouldEnableArrowNavigation = !isMini && (isVisible || shouldKeepOpen);
let filteredContextMenuActions = ContextMenuActions.filter((contextAction) =>
contextAction.shouldShow(type, reportAction, isArchivedRoom, betas, anchor, isChronosReport, reportID, isPinnedChat, isUnreadChat, !!isOffline, isMini),
let filteredContextMenuActions = ContextMenuActions.filter(
(contextAction) =>
!disabledActions.includes(contextAction) &&
contextAction.shouldShow(type, reportAction, isArchivedRoom, betas, anchor, isChronosReport, reportID, isPinnedChat, isUnreadChat, !!isOffline, isMini),
);
filteredContextMenuActions =
isMini && filteredContextMenuActions.length > CONST.MINI_CONTEXT_MENU_MAX_ITEMS
? ([...filteredContextMenuActions.slice(0, CONST.MINI_CONTEXT_MENU_MAX_ITEMS - 1), filteredContextMenuActions.at(-1)] as typeof filteredContextMenuActions)
: filteredContextMenuActions;

if (isMini) {
const menuAction = filteredContextMenuActions.at(-1);
const otherActions = filteredContextMenuActions.slice(0, -1);
if (otherActions.length > CONST.MINI_CONTEXT_MENU_MAX_ITEMS && menuAction) {
filteredContextMenuActions = otherActions.slice(0, CONST.MINI_CONTEXT_MENU_MAX_ITEMS - 1);
filteredContextMenuActions.push(menuAction);
} else {
filteredContextMenuActions = otherActions;
}
}

// Context menu actions that are not rendered as menu items are excluded from arrow navigation
const nonMenuItemActionIndexes = filteredContextMenuActions.map((contextAction, index) =>
Expand Down Expand Up @@ -172,6 +189,28 @@ function BaseReportActionContextMenu({
{isActive: shouldEnableArrowNavigation},
);

const openOverflowMenu = (event: GestureResponderEvent | MouseEvent) => {
const originalReportID = ReportUtils.getOriginalReportID(reportID, reportAction);
const originalReport = ReportUtils.getReport(originalReportID);
showContextMenu(
CONST.CONTEXT_MENU_TYPES.REPORT_ACTION,
event,
selection,
anchor?.current as ViewType | RNText | null,
reportID,
reportAction?.reportActionID,
originalReportID,
draftMessage,
checkIfContextMenuActive,
checkIfContextMenuActive,
ReportUtils.isArchivedRoom(originalReport),
ReportUtils.chatIncludesChronos(originalReport),
undefined,
undefined,
filteredContextMenuActions,
);
};

return (
(isVisible || shouldKeepOpen) && (
<View
Expand All @@ -188,8 +227,7 @@ function BaseReportActionContextMenu({
close: () => setShouldKeepOpen(false),
openContextMenu: () => setShouldKeepOpen(true),
interceptAnonymousUser,
anchor,
checkIfContextMenuActive,
openOverflowMenu,
};

if ('renderContent' in contextAction) {
Expand Down
Loading

0 comments on commit 8483009

Please sign in to comment.