diff --git a/android/app/build.gradle b/android/app/build.gradle index dc72b8825ae6..0b3bad0d8676 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -98,8 +98,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1001044004 - versionName "1.4.40-4" + versionCode 1001044005 + versionName "1.4.40-5" } flavorDimensions "default" diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index 2c48680bf3bd..facd7194172a 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -40,7 +40,7 @@ CFBundleVersion - 1.4.40.4 + 1.4.40.5 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index b8b2f7e2383b..2d7838cfcdec 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -19,6 +19,6 @@ CFBundleSignature ???? CFBundleVersion - 1.4.40.4 + 1.4.40.5 diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index 897ee8140144..86503558bfe1 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -13,7 +13,7 @@ CFBundleShortVersionString 1.4.40 CFBundleVersion - 1.4.40.4 + 1.4.40.5 NSExtension NSExtensionPointIdentifier diff --git a/package-lock.json b/package-lock.json index f37598a02ae0..09e89b9c484c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "1.4.40-4", + "version": "1.4.40-5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.4.40-4", + "version": "1.4.40-5", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index eb64e5b0c7ce..105db98bf9b1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.4.40-4", + "version": "1.4.40-5", "author": "Expensify, Inc.", "homepage": "https://new.expensify.com", "description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.", diff --git a/src/App.tsx b/src/App.tsx index 7c1ead1d86d3..52baedc79421 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -6,6 +6,7 @@ import Onyx from 'react-native-onyx'; import {PickerStateProvider} from 'react-native-picker-select'; import {SafeAreaProvider} from 'react-native-safe-area-context'; import '../wdyr'; +import ActiveElementRoleProvider from './components/ActiveElementRoleProvider'; import ActiveWorkspaceContextProvider from './components/ActiveWorkspace/ActiveWorkspaceProvider'; import ColorSchemeWrapper from './components/ColorSchemeWrapper'; import ComposeProviders from './components/ComposeProviders'; @@ -78,6 +79,7 @@ function App({url}: AppProps) { PickerStateProvider, EnvironmentProvider, CustomStatusBarAndBackgroundContextProvider, + ActiveElementRoleProvider, ActiveWorkspaceContextProvider, ]} > diff --git a/src/CONST.ts b/src/CONST.ts index f1b7e74bbde4..a939f0942839 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -1413,9 +1413,9 @@ const CONST = { OWNER_EMAIL_FAKE: '_FAKE_', OWNER_ACCOUNT_ID_FAKE: 0, REIMBURSEMENT_CHOICES: { - REIMBURSEMENT_YES: 'reimburseYes', // Direct - REIMBURSEMENT_NO: 'reimburseNo', // None - REIMBURSEMENT_MANUAL: 'reimburseManual', // Indirect + REIMBURSEMENT_YES: 'reimburseYes', + REIMBURSEMENT_NO: 'reimburseNo', + REIMBURSEMENT_MANUAL: 'reimburseManual', }, ID_FAKE: '_FAKE_', EMPTY: 'EMPTY', diff --git a/src/components/ActiveElementRoleProvider/index.native.tsx b/src/components/ActiveElementRoleProvider/index.native.tsx new file mode 100644 index 000000000000..4a9f2290b2b0 --- /dev/null +++ b/src/components/ActiveElementRoleProvider/index.native.tsx @@ -0,0 +1,20 @@ +import React from 'react'; +import type {ActiveElementRoleContextValue, ActiveElementRoleProps} from './types'; + +const ActiveElementRoleContext = React.createContext({ + role: null, +}); + +function ActiveElementRoleProvider({children}: ActiveElementRoleProps) { + const value = React.useMemo( + () => ({ + role: null, + }), + [], + ); + + return {children}; +} + +export default ActiveElementRoleProvider; +export {ActiveElementRoleContext}; diff --git a/src/components/ActiveElementRoleProvider/index.tsx b/src/components/ActiveElementRoleProvider/index.tsx new file mode 100644 index 000000000000..630af8618c08 --- /dev/null +++ b/src/components/ActiveElementRoleProvider/index.tsx @@ -0,0 +1,40 @@ +import React, {useEffect, useState} from 'react'; +import type {ActiveElementRoleContextValue, ActiveElementRoleProps} from './types'; + +const ActiveElementRoleContext = React.createContext({ + role: null, +}); + +function ActiveElementRoleProvider({children}: ActiveElementRoleProps) { + const [activeRoleRef, setRole] = useState(document?.activeElement?.role ?? null); + + const handleFocusIn = () => { + setRole(document?.activeElement?.role ?? null); + }; + + const handleFocusOut = () => { + setRole(null); + }; + + useEffect(() => { + document.addEventListener('focusin', handleFocusIn); + document.addEventListener('focusout', handleFocusOut); + + return () => { + document.removeEventListener('focusin', handleFocusIn); + document.removeEventListener('focusout', handleFocusOut); + }; + }, []); + + const value = React.useMemo( + () => ({ + role: activeRoleRef, + }), + [activeRoleRef], + ); + + return {children}; +} + +export default ActiveElementRoleProvider; +export {ActiveElementRoleContext}; diff --git a/src/components/ActiveElementRoleProvider/types.ts b/src/components/ActiveElementRoleProvider/types.ts new file mode 100644 index 000000000000..f22343b12550 --- /dev/null +++ b/src/components/ActiveElementRoleProvider/types.ts @@ -0,0 +1,9 @@ +type ActiveElementRoleContextValue = { + role: string | null; +}; + +type ActiveElementRoleProps = { + children: React.ReactNode; +}; + +export type {ActiveElementRoleContextValue, ActiveElementRoleProps}; diff --git a/src/components/Button/index.tsx b/src/components/Button/index.tsx index f4b6e8b23ecf..1961829b6aa7 100644 --- a/src/components/Button/index.tsx +++ b/src/components/Button/index.tsx @@ -265,14 +265,16 @@ function Button( return ( <> - + {pressOnEnter && ( + + )} { diff --git a/src/components/LHNOptionsList/LHNOptionsList.tsx b/src/components/LHNOptionsList/LHNOptionsList.tsx index a55d329f39ee..27f424ad1b70 100644 --- a/src/components/LHNOptionsList/LHNOptionsList.tsx +++ b/src/components/LHNOptionsList/LHNOptionsList.tsx @@ -1,6 +1,6 @@ import {FlashList} from '@shopify/flash-list'; import type {ReactElement} from 'react'; -import React, {useCallback} from 'react'; +import React, {memo, useCallback} from 'react'; import {StyleSheet, View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import withCurrentReportID from '@components/withCurrentReportID'; @@ -158,7 +158,7 @@ export default withCurrentReportID( transactionViolations: { key: ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, }, - })(LHNOptionsList), + })(memo(LHNOptionsList)), ); export type {LHNOptionsListProps}; diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index bfe5a7ff2a75..0e08aed214a6 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -57,10 +57,9 @@ function MoneyReportHeader({session, policy, chatReport, nextStep, report: money const isAutoReimbursable = ReportUtils.canBeAutoReimbursed(moneyRequestReport, policy); const isPaidGroupPolicy = ReportUtils.isPaidGroupPolicy(moneyRequestReport); const isManager = ReportUtils.isMoneyRequestReport(moneyRequestReport) && session?.accountID === moneyRequestReport.managerID; - const isReimburser = session?.email === policy?.reimburserEmail; const isPayer = isPaidGroupPolicy ? // In a group policy, the admin approver can pay the report directly by skipping the approval step - isReimburser && (isApproved || isManager) + isPolicyAdmin && (isApproved || isManager) : isPolicyAdmin || (ReportUtils.isMoneyRequestReport(moneyRequestReport) && isManager); const isDraft = ReportUtils.isDraftExpenseReport(moneyRequestReport); const isOnInstantSubmitPolicy = PolicyUtils.isInstantSubmitEnabled(policy); diff --git a/src/components/OnyxProvider.tsx b/src/components/OnyxProvider.tsx index 124f3558df90..d14aec90fa10 100644 --- a/src/components/OnyxProvider.tsx +++ b/src/components/OnyxProvider.tsx @@ -10,11 +10,12 @@ const [withPersonalDetails, PersonalDetailsProvider, , usePersonalDetails] = cre const [withCurrentDate, CurrentDateProvider] = createOnyxContext(ONYXKEYS.CURRENT_DATE); const [withReportActionsDrafts, ReportActionsDraftsProvider] = createOnyxContext(ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS); const [withBlockedFromConcierge, BlockedFromConciergeProvider] = createOnyxContext(ONYXKEYS.NVP_BLOCKED_FROM_CONCIERGE); -const [withBetas, BetasProvider, BetasContext] = createOnyxContext(ONYXKEYS.BETAS); +const [withBetas, BetasProvider, BetasContext, useBetas] = createOnyxContext(ONYXKEYS.BETAS); const [withReportCommentDrafts, ReportCommentDraftsProvider] = createOnyxContext(ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT); const [withPreferredTheme, PreferredThemeProvider, PreferredThemeContext] = createOnyxContext(ONYXKEYS.PREFERRED_THEME); const [withFrequentlyUsedEmojis, FrequentlyUsedEmojisProvider, , useFrequentlyUsedEmojis] = createOnyxContext(ONYXKEYS.FREQUENTLY_USED_EMOJIS); const [withPreferredEmojiSkinTone, PreferredEmojiSkinToneProvider, PreferredEmojiSkinToneContext] = createOnyxContext(ONYXKEYS.PREFERRED_EMOJI_SKIN_TONE); +const [, SessionProvider, , useSession] = createOnyxContext(ONYXKEYS.SESSION); type OnyxProviderProps = { /** Rendered child component */ @@ -35,6 +36,7 @@ function OnyxProvider(props: OnyxProviderProps) { PreferredThemeProvider, FrequentlyUsedEmojisProvider, PreferredEmojiSkinToneProvider, + SessionProvider, ]} > {props.children} @@ -59,8 +61,10 @@ export { withReportCommentDrafts, withPreferredTheme, PreferredThemeContext, + useBetas, withFrequentlyUsedEmojis, useFrequentlyUsedEmojis, withPreferredEmojiSkinTone, PreferredEmojiSkinToneContext, + useSession, }; diff --git a/src/components/OptionsList/BaseOptionsList.tsx b/src/components/OptionsList/BaseOptionsList.tsx index 4a9df89ae644..575df128894a 100644 --- a/src/components/OptionsList/BaseOptionsList.tsx +++ b/src/components/OptionsList/BaseOptionsList.tsx @@ -1,6 +1,6 @@ import isEqual from 'lodash/isEqual'; import type {ForwardedRef} from 'react'; -import React, {forwardRef, memo, useEffect, useRef} from 'react'; +import React, {forwardRef, memo, useEffect, useMemo, useRef} from 'react'; import type {SectionListRenderItem} from 'react-native'; import {View} from 'react-native'; import OptionRow from '@components/OptionRow'; @@ -31,7 +31,7 @@ function BaseOptionsList( shouldHaveOptionSeparator = false, showTitleTooltip = false, optionHoveredStyle, - contentContainerStyles, + contentContainerStyles: contentContainerStylesProp, sectionHeaderStyle, showScrollIndicator = true, listContainerStyles: listContainerStylesProp, @@ -51,6 +51,7 @@ function BaseOptionsList( nestedScrollEnabled = true, bounces = true, renderFooterContent, + safeAreaPaddingBottomStyle, }: BaseOptionListProps, ref: ForwardedRef, ) { @@ -64,7 +65,8 @@ function BaseOptionsList( const previousSections = usePrevious(sections); const didLayout = useRef(false); - const listContainerStyles = listContainerStylesProp ?? [styles.flex1]; + const listContainerStyles = useMemo(() => listContainerStylesProp ?? [styles.flex1], [listContainerStylesProp, styles.flex1]); + const contentContainerStyles = useMemo(() => [safeAreaPaddingBottomStyle, contentContainerStylesProp], [contentContainerStylesProp, safeAreaPaddingBottomStyle]); /** * This helper function is used to memoize the computation needed for getItemLayout. It is run whenever section data changes. diff --git a/src/components/OptionsList/index.native.tsx b/src/components/OptionsList/index.native.tsx index bdcd0418a940..d4977f25f366 100644 --- a/src/components/OptionsList/index.native.tsx +++ b/src/components/OptionsList/index.native.tsx @@ -10,7 +10,7 @@ function OptionsList(props: OptionsListProps, ref: ForwardedRef // eslint-disable-next-line react/jsx-props-no-spreading {...props} ref={ref} - onScrollBeginDrag={() => Keyboard.dismiss()} + onScrollBeginDrag={Keyboard.dismiss} /> ); } diff --git a/src/components/ReportActionItem/ReportPreview.tsx b/src/components/ReportActionItem/ReportPreview.tsx index 4066776711b1..cfe06b2c0a62 100644 --- a/src/components/ReportActionItem/ReportPreview.tsx +++ b/src/components/ReportActionItem/ReportPreview.tsx @@ -122,7 +122,6 @@ function ReportPreview({ const {totalDisplaySpend, reimbursableSpend} = ReportUtils.getMoneyRequestSpendBreakdown(iouReport); const policyType = policy?.type; const isAutoReimbursable = ReportUtils.canBeAutoReimbursed(iouReport, policy); - const isReimburser = session?.email === policy?.reimburserEmail; const iouSettled = ReportUtils.isSettled(iouReportID); const iouCanceled = ReportUtils.isArchivedRoom(chatReport); @@ -213,7 +212,7 @@ function ReportPreview({ const isPolicyAdmin = policyType !== CONST.POLICY.TYPE.PERSONAL && policy?.role === CONST.POLICY.ROLE.ADMIN; const isPayer = isPaidGroupPolicy ? // In a paid group policy, the admin approver can pay the report directly by skipping the approval step - isReimburser && (isApproved || isCurrentUserManager) + isPolicyAdmin && (isApproved || isCurrentUserManager) : isPolicyAdmin || (isMoneyRequestReport && isCurrentUserManager); const isOnInstantSubmitPolicy = PolicyUtils.isInstantSubmitEnabled(policy); const isOnSubmitAndClosePolicy = PolicyUtils.isSubmitAndClose(policy); diff --git a/src/components/withCurrentUserPersonalDetails.tsx b/src/components/withCurrentUserPersonalDetails.tsx index 9406c8634c1b..313bcad74f35 100644 --- a/src/components/withCurrentUserPersonalDetails.tsx +++ b/src/components/withCurrentUserPersonalDetails.tsx @@ -1,26 +1,17 @@ import type {ComponentType, ForwardedRef, RefAttributes} from 'react'; -import React, {useMemo} from 'react'; -import type {OnyxEntry} from 'react-native-onyx'; -import {withOnyx} from 'react-native-onyx'; +import React from 'react'; +import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import getComponentDisplayName from '@libs/getComponentDisplayName'; import personalDetailsPropType from '@pages/personalDetailsPropType'; -import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; -import type {PersonalDetails, Session} from '@src/types/onyx'; -import {usePersonalDetails} from './OnyxProvider'; +import type {PersonalDetails} from '@src/types/onyx'; type CurrentUserPersonalDetails = PersonalDetails | Record; -type OnyxProps = { - /** Session of the current user */ - session: OnyxEntry; -}; - type HOCProps = { currentUserPersonalDetails: CurrentUserPersonalDetails; }; -type WithCurrentUserPersonalDetailsProps = OnyxProps & HOCProps; +type WithCurrentUserPersonalDetailsProps = HOCProps; // TODO: remove when all components that use it will be migrated to TS const withCurrentUserPersonalDetailsPropTypes = { @@ -33,15 +24,9 @@ const withCurrentUserPersonalDetailsDefaultProps: HOCProps = { export default function ( WrappedComponent: ComponentType>, -): ComponentType & RefAttributes, keyof OnyxProps>> { +): ComponentType & RefAttributes> { function WithCurrentUserPersonalDetails(props: Omit, ref: ForwardedRef) { - const personalDetails = usePersonalDetails() ?? CONST.EMPTY_OBJECT; - const accountID = props.session?.accountID ?? 0; - const accountPersonalDetails = personalDetails?.[accountID]; - const currentUserPersonalDetails: CurrentUserPersonalDetails = useMemo( - () => (accountPersonalDetails ? {...accountPersonalDetails, accountID} : {}) as CurrentUserPersonalDetails, - [accountPersonalDetails, accountID], - ); + const currentUserPersonalDetails = useCurrentUserPersonalDetails(); return ( & RefAttributes, OnyxProps>({ - session: { - key: ONYXKEYS.SESSION, - }, - })(withCurrentUserPersonalDetails); + return React.forwardRef(WithCurrentUserPersonalDetails); } export {withCurrentUserPersonalDetailsPropTypes, withCurrentUserPersonalDetailsDefaultProps}; diff --git a/src/hooks/useActiveElementRole/index.native.ts b/src/hooks/useActiveElementRole/index.native.ts deleted file mode 100644 index 4278014f02a8..000000000000 --- a/src/hooks/useActiveElementRole/index.native.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type UseActiveElementRole from './types'; - -/** - * Native doesn't have the DOM, so we just return null. - */ -const useActiveElementRole: UseActiveElementRole = () => null; - -export default useActiveElementRole; diff --git a/src/hooks/useActiveElementRole/index.ts b/src/hooks/useActiveElementRole/index.ts index a98999105ac8..98ae285f92b0 100644 --- a/src/hooks/useActiveElementRole/index.ts +++ b/src/hooks/useActiveElementRole/index.ts @@ -1,4 +1,5 @@ -import {useEffect, useRef} from 'react'; +import {useContext} from 'react'; +import {ActiveElementRoleContext} from '@components/ActiveElementRoleProvider'; import type UseActiveElementRole from './types'; /** @@ -6,27 +7,9 @@ import type UseActiveElementRole from './types'; * On native, we just return null. */ const useActiveElementRole: UseActiveElementRole = () => { - const activeRoleRef = useRef(document?.activeElement?.role); + const {role} = useContext(ActiveElementRoleContext); - const handleFocusIn = () => { - activeRoleRef.current = document?.activeElement?.role; - }; - - const handleFocusOut = () => { - activeRoleRef.current = null; - }; - - useEffect(() => { - document.addEventListener('focusin', handleFocusIn); - document.addEventListener('focusout', handleFocusOut); - - return () => { - document.removeEventListener('focusin', handleFocusIn); - document.removeEventListener('focusout', handleFocusOut); - }; - }, []); - - return activeRoleRef.current; + return role; }; export default useActiveElementRole; diff --git a/src/hooks/useActiveElementRole/types.ts b/src/hooks/useActiveElementRole/types.ts index c31b8ab7ddbf..f6884548785f 100644 --- a/src/hooks/useActiveElementRole/types.ts +++ b/src/hooks/useActiveElementRole/types.ts @@ -1,3 +1,3 @@ -type UseActiveElementRole = () => string | null | undefined; +type UseActiveElementRole = () => string | null; export default UseActiveElementRole; diff --git a/src/hooks/useCurrentUserPersonalDetails.ts b/src/hooks/useCurrentUserPersonalDetails.ts new file mode 100644 index 000000000000..da3c2b18bd83 --- /dev/null +++ b/src/hooks/useCurrentUserPersonalDetails.ts @@ -0,0 +1,21 @@ +import {useMemo} from 'react'; +import {usePersonalDetails, useSession} from '@components/OnyxProvider'; +import CONST from '@src/CONST'; +import type {PersonalDetails} from '@src/types/onyx'; + +type CurrentUserPersonalDetails = PersonalDetails | Record; + +function useCurrentUserPersonalDetails() { + const session = useSession(); + const personalDetails = usePersonalDetails() ?? CONST.EMPTY_OBJECT; + const accountID = session?.accountID ?? 0; + const accountPersonalDetails = personalDetails?.[accountID]; + const currentUserPersonalDetails: CurrentUserPersonalDetails = useMemo( + () => (accountPersonalDetails ? {...accountPersonalDetails, accountID} : {}) as CurrentUserPersonalDetails, + [accountPersonalDetails, accountID], + ); + + return currentUserPersonalDetails; +} + +export default useCurrentUserPersonalDetails; diff --git a/src/languages/es.ts b/src/languages/es.ts index 5da1aa3a9dc6..6d853604aa20 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -1759,7 +1759,7 @@ export default { trackDistanceCopy: 'Configura la tarifa y unidad usadas para medir distancias.', trackDistanceRate: 'Tarifa', trackDistanceUnit: 'Unidad', - trackDistanceChooseUnit: 'Elija una unidad predeterminada para rastrear.', + trackDistanceChooseUnit: 'Elige una unidad predeterminada de medida.', kilometers: 'Kilómetros', miles: 'Millas', unlockNextDayReimbursements: 'Desbloquea reembolsos diarios', diff --git a/src/libs/focusEditAfterCancelDelete/index.native.ts b/src/libs/focusEditAfterCancelDelete/index.native.ts new file mode 100755 index 000000000000..17bafabc5790 --- /dev/null +++ b/src/libs/focusEditAfterCancelDelete/index.native.ts @@ -0,0 +1,8 @@ +import {InteractionManager} from 'react-native'; +import type FocusEditAfterCancelDelete from './types'; + +const focusEditAfterCancelDelete: FocusEditAfterCancelDelete = (textInputRef) => { + InteractionManager.runAfterInteractions(() => textInputRef?.focus()); +}; + +export default focusEditAfterCancelDelete; diff --git a/src/libs/focusEditAfterCancelDelete/index.ts b/src/libs/focusEditAfterCancelDelete/index.ts new file mode 100755 index 000000000000..541c0ef1aaef --- /dev/null +++ b/src/libs/focusEditAfterCancelDelete/index.ts @@ -0,0 +1,5 @@ +import type FocusEditAfterCancelDelete from './types'; + +const focusEditAfterCancelDelete: FocusEditAfterCancelDelete = () => {}; + +export default focusEditAfterCancelDelete; diff --git a/src/libs/focusEditAfterCancelDelete/types.ts b/src/libs/focusEditAfterCancelDelete/types.ts new file mode 100755 index 000000000000..ee479203f890 --- /dev/null +++ b/src/libs/focusEditAfterCancelDelete/types.ts @@ -0,0 +1,5 @@ +import type {TextInput} from 'react-native'; + +type FocusEditAfterCancelDelete = (inputRef: TextInput | HTMLTextAreaElement | null) => void; + +export default FocusEditAfterCancelDelete; diff --git a/src/pages/ShareCodePage.tsx b/src/pages/ShareCodePage.tsx index 138aa729fcc2..e4e2a90c4157 100644 --- a/src/pages/ShareCodePage.tsx +++ b/src/pages/ShareCodePage.tsx @@ -12,8 +12,7 @@ import QRShareWithDownload from '@components/QRShare/QRShareWithDownload'; import type QRShareWithDownloadHandle from '@components/QRShare/QRShareWithDownload/types'; import ScreenWrapper from '@components/ScreenWrapper'; import Section from '@components/Section'; -import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalDetails'; -import type {WithCurrentUserPersonalDetailsProps} from '@components/withCurrentUserPersonalDetails'; +import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useEnvironment from '@hooks/useEnvironment'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -28,9 +27,9 @@ import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; import type {Report, Session} from '@src/types/onyx'; -type ShareCodePageOnyxProps = WithCurrentUserPersonalDetailsProps & { +type ShareCodePageOnyxProps = { /** Session info for the currently logged in user. */ - session: OnyxEntry; + session?: OnyxEntry; /** The report currently being looked at */ report?: OnyxEntry; @@ -38,12 +37,13 @@ type ShareCodePageOnyxProps = WithCurrentUserPersonalDetailsProps & { type ShareCodePageProps = ShareCodePageOnyxProps; -function ShareCodePage({report, session, currentUserPersonalDetails}: ShareCodePageProps) { +function ShareCodePage({report, session}: ShareCodePageProps) { const themeStyles = useThemeStyles(); const {translate} = useLocalize(); const {environmentURL} = useEnvironment(); const qrCodeRef = useRef(null); const {isSmallScreenWidth} = useWindowDimensions(); + const currentUserPersonalDetails = useCurrentUserPersonalDetails(); const isReport = !!report?.reportID; @@ -145,4 +145,4 @@ function ShareCodePage({report, session, currentUserPersonalDetails}: ShareCodeP ShareCodePage.displayName = 'ShareCodePage'; -export default withCurrentUserPersonalDetails(ShareCodePage); +export default ShareCodePage; diff --git a/src/pages/home/report/ReportActionItemMessageEdit.tsx b/src/pages/home/report/ReportActionItemMessageEdit.tsx index e97aa0338f90..427c6ccdbfc4 100644 --- a/src/pages/home/report/ReportActionItemMessageEdit.tsx +++ b/src/pages/home/report/ReportActionItemMessageEdit.tsx @@ -3,7 +3,7 @@ import Str from 'expensify-common/lib/str'; import lodashDebounce from 'lodash/debounce'; import type {ForwardedRef} from 'react'; import React, {forwardRef, useCallback, useEffect, useMemo, useRef, useState} from 'react'; -import {InteractionManager, Keyboard, View} from 'react-native'; +import {Keyboard, View} from 'react-native'; import type {NativeSyntheticEvent, TextInput, TextInputFocusEventData, TextInputKeyPressEventData} from 'react-native'; import type {Emoji} from '@assets/emojis/types'; import Composer from '@components/Composer'; @@ -25,6 +25,7 @@ import * as Browser from '@libs/Browser'; import * as ComposerUtils from '@libs/ComposerUtils'; import * as EmojiUtils from '@libs/EmojiUtils'; import focusComposerWithDelay from '@libs/focusComposerWithDelay'; +import focusEditAfterCancelDelete from '@libs/focusEditAfterCancelDelete'; import onyxSubscribe from '@libs/onyxSubscribe'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; @@ -317,14 +318,7 @@ function ReportActionItemMessageEdit( // When user tries to save the empty message, it will delete it. Prompt the user to confirm deleting. if (!trimmedNewDraft) { textInputRef.current?.blur(); - ReportActionContextMenu.showDeleteModal( - reportID, - action, - true, - deleteDraft, - // eslint-disable-next-line @typescript-eslint/no-misused-promises - () => InteractionManager.runAfterInteractions(() => textInputRef.current?.focus()), - ); + ReportActionContextMenu.showDeleteModal(reportID, action, true, deleteDraft, () => focusEditAfterCancelDelete(textInputRef.current)); return; } Report.editReportComment(reportID, action, trimmedNewDraft); diff --git a/src/pages/home/report/ReportActionsList.js b/src/pages/home/report/ReportActionsList.js index 2f9e3222206b..1da806b9c269 100644 --- a/src/pages/home/report/ReportActionsList.js +++ b/src/pages/home/report/ReportActionsList.js @@ -1,7 +1,7 @@ import {useRoute} from '@react-navigation/native'; import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; -import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; +import React, {memo, useCallback, useEffect, useMemo, useRef, useState} from 'react'; import {DeviceEventEmitter, InteractionManager} from 'react-native'; import Animated, {useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated'; import _ from 'underscore'; @@ -515,4 +515,4 @@ ReportActionsList.propTypes = propTypes; ReportActionsList.defaultProps = defaultProps; ReportActionsList.displayName = 'ReportActionsList'; -export default compose(withWindowDimensions, withCurrentUserPersonalDetails)(ReportActionsList); +export default compose(withWindowDimensions, withCurrentUserPersonalDetails)(memo(ReportActionsList)); diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index 62ff9426b4ae..064187855b57 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -1,7 +1,7 @@ import {useIsFocused} from '@react-navigation/native'; import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; -import React, {useContext, useEffect, useMemo, useRef} from 'react'; +import React, {useCallback, useContext, useEffect, useMemo, useRef} from 'react'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; import networkPropTypes from '@components/networkPropTypes'; @@ -173,25 +173,25 @@ function ReportActionsView(props) { } }, [props.report.pendingFields, didSubscribeToReportTypingEvents, reportID]); + const oldestReportAction = useMemo(() => _.last(props.reportActions), [props.reportActions]); + /** * Retrieves the next set of report actions for the chat once we are nearing the end of what we are currently * displaying. */ - const loadOlderChats = () => { + const loadOlderChats = useCallback(() => { // Only fetch more if we are neither already fetching (so that we don't initiate duplicate requests) nor offline. if (props.network.isOffline || props.isLoadingOlderReportActions) { return; } - const oldestReportAction = _.last(props.reportActions); - // Don't load more chats if we're already at the beginning of the chat history if (!oldestReportAction || oldestReportAction.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED) { return; } // Retrieve the next REPORT.ACTIONS.LIMIT sized page of comments Report.getOlderActions(reportID, oldestReportAction.reportActionID); - }; + }, [props.isLoadingOlderReportActions, props.network.isOffline, oldestReportAction, reportID]); /** * Retrieves the next set of report actions for the chat once we are nearing the end of what we are currently @@ -228,7 +228,7 @@ function ReportActionsView(props) { /** * Runs when the FlatList finishes laying out */ - const recordTimeToMeasureItemLayout = () => { + const recordTimeToMeasureItemLayout = useCallback(() => { if (didLayout.current) { return; } @@ -243,7 +243,7 @@ function ReportActionsView(props) { } else { Performance.markEnd(CONST.TIMING.SWITCH_REPORT); } - }; + }, [hasCachedActions]); // Comments have not loaded at all yet do nothing if (!_.size(props.reportActions)) { diff --git a/src/pages/home/sidebar/SidebarLinks.js b/src/pages/home/sidebar/SidebarLinks.js index 2fe44639e184..e78e656e0b7e 100644 --- a/src/pages/home/sidebar/SidebarLinks.js +++ b/src/pages/home/sidebar/SidebarLinks.js @@ -1,7 +1,7 @@ /* eslint-disable rulesdir/onyx-props-must-have-default */ import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; -import React, {useCallback, useEffect, useRef} from 'react'; +import React, {useCallback, useEffect, useMemo, useRef} from 'react'; import {InteractionManager, StyleSheet, View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; @@ -131,6 +131,9 @@ function SidebarLinks({onLinkClick, insets, optionListItems, isLoading, priority const viewMode = priorityMode === CONST.PRIORITY_MODE.GSD ? CONST.OPTION_MODE.COMPACT : CONST.OPTION_MODE.DEFAULT; + // eslint-disable-next-line react-hooks/exhaustive-deps + const contentContainerStyles = useMemo(() => StyleSheet.flatten([styles.sidebarListContainer, {paddingBottom: StyleUtils.getSafeAreaMargins(insets).marginBottom}]), [insets]); + return ( { + const options = useMemo(() => { const {recentReports, personalDetails, userToInvite, currentUserOption} = OptionsListUtils.getFilteredOptions( - props.reports, + reports, allPersonalDetails, - props.betas, - searchValue.trim(), + betas, + debouncedSearchValue.trim(), [], CONST.EXPENSIFY_EMAILS, false, @@ -100,50 +76,56 @@ function TaskAssigneeSelectorModal(props) { true, ); - setHeaderMessage(OptionsListUtils.getHeaderMessage(recentReports?.length + personalDetails?.length !== 0 || currentUserOption, Boolean(userToInvite), searchValue)); + const headerMessage = OptionsListUtils.getHeaderMessage(recentReports?.length + personalDetails?.length !== 0 || currentUserOption, Boolean(userToInvite), debouncedSearchValue); - setFilteredUserToInvite(userToInvite); - setFilteredRecentReports(recentReports); - setFilteredPersonalDetails(personalDetails); - setFilteredCurrentUserOption(currentUserOption); if (isLoading) { setIsLoading(false); } - }, [props, searchValue, allPersonalDetails, isLoading]); - useEffect(() => { - const debouncedSearch = _.debounce(updateOptions, 200); - debouncedSearch(); - return () => { - debouncedSearch.cancel(); + return { + userToInvite, + recentReports, + personalDetails, + currentUserOption, + headerMessage, }; - }, [updateOptions]); + }, [debouncedSearchValue, allPersonalDetails, isLoading, betas, reports]); + + return {...options, isLoading, searchValue, debouncedSearchValue, setSearchValue}; +} + +function TaskAssigneeSelectorModal({reports, task, rootParentReportPolicy}) { + const styles = useThemeStyles(); + const route = useRoute(); + const {translate} = useLocalize(); + const session = useSession(); + const currentUserPersonalDetails = useCurrentUserPersonalDetails(); + const {userToInvite, recentReports, personalDetails, currentUserOption, isLoading, searchValue, setSearchValue, headerMessage} = useOptions({reports, task}); const onChangeText = (newSearchTerm = '') => { setSearchValue(newSearchTerm); }; const report = useMemo(() => { - if (!props.route.params || !props.route.params.reportID) { + if (!route.params || !route.params.reportID) { return null; } - return props.reports[`${ONYXKEYS.COLLECTION.REPORT}${props.route.params.reportID}`]; - }, [props.reports, props.route.params]); - - if (report && !ReportUtils.isTaskReport(report)) { - Navigation.isNavigationReady().then(() => { - Navigation.dismissModal(report.reportID); - }); - } + if (report && !ReportUtils.isTaskReport(report)) { + Navigation.isNavigationReady().then(() => { + Navigation.dismissModal(report.reportID); + }); + } + return reports[`${ONYXKEYS.COLLECTION.REPORT}${route.params.reportID}`]; + }, [reports, route]); const sections = useMemo(() => { const sectionsList = []; let indexOffset = 0; - if (filteredCurrentUserOption) { + if (currentUserOption) { sectionsList.push({ - title: props.translate('newTaskPage.assignMe'), - data: [filteredCurrentUserOption], + title: translate('newTaskPage.assignMe'), + data: [currentUserOption], shouldShow: true, indexOffset, }); @@ -151,31 +133,31 @@ function TaskAssigneeSelectorModal(props) { } sectionsList.push({ - title: props.translate('common.recents'), - data: filteredRecentReports, - shouldShow: filteredRecentReports?.length > 0, + title: translate('common.recents'), + data: recentReports, + shouldShow: recentReports?.length > 0, indexOffset, }); - indexOffset += filteredRecentReports?.length; + indexOffset += recentReports?.length; sectionsList.push({ - title: props.translate('common.contacts'), - data: filteredPersonalDetails, - shouldShow: filteredPersonalDetails?.length > 0, + title: translate('common.contacts'), + data: personalDetails, + shouldShow: personalDetails?.length > 0, indexOffset, }); - indexOffset += filteredPersonalDetails?.length; + indexOffset += personalDetails?.length; - if (filteredUserToInvite) { + if (userToInvite) { sectionsList.push({ - data: [filteredUserToInvite], + data: [userToInvite], shouldShow: true, indexOffset, }); } return sectionsList; - }, [filteredCurrentUserOption, filteredPersonalDetails, filteredRecentReports, filteredUserToInvite, props]); + }, [currentUserOption, personalDetails, recentReports, userToInvite, translate]); const selectReport = useCallback( (option) => { @@ -189,20 +171,22 @@ function TaskAssigneeSelectorModal(props) { const assigneeChatReport = Task.setAssigneeValue(option.login, option.accountID, report.reportID, OptionsListUtils.isCurrentUser(option)); // Pass through the selected assignee - Task.editTaskAssignee(report, props.session.accountID, option.login, option.accountID, assigneeChatReport); + Task.editTaskAssignee(report, session.accountID, option.login, option.accountID, assigneeChatReport); } - Navigation.dismissModalWithReport(report); + Navigation.dismissModal(report.reportID); // If there's no report, we're creating a new task } else if (option.accountID) { - Task.setAssigneeValue(option.login, option.accountID, props.task.shareDestination, OptionsListUtils.isCurrentUser(option)); + Task.setAssigneeValue(option.login, option.accountID, task.shareDestination, OptionsListUtils.isCurrentUser(option)); Navigation.goBack(ROUTES.NEW_TASK); } }, - [props.session.accountID, props.task.shareDestination, report], + [session.accountID, task.shareDestination, report], ); + const handleBackButtonPress = useCallback(() => (lodashGet(route.params, 'reportID') ? Navigation.dismissModal() : Navigation.goBack(ROUTES.NEW_TASK)), [route.params]); + const isOpen = ReportUtils.isOpenTaskReport(report); - const canModifyTask = Task.canModifyTask(report, props.currentUserPersonalDetails.accountID); + const canModifyTask = Task.canModifyTask(report, currentUserPersonalDetails.accountID, lodashGet(rootParentReportPolicy, 'role', '')); const isTaskNonEditable = ReportUtils.isTaskReport(report) && (!canModifyTask || !isOpen); return ( @@ -213,21 +197,19 @@ function TaskAssigneeSelectorModal(props) { {({didScreenTransitionEnd, safeAreaPaddingBottomStyle}) => ( (lodashGet(props.route.params, 'reportID') ? Navigation.dismissModal() : Navigation.goBack(ROUTES.NEW_TASK))} + title={translate('task.assignee')} + onBackButtonPress={handleBackButtonPress} /> - @@ -241,20 +223,22 @@ TaskAssigneeSelectorModal.propTypes = propTypes; TaskAssigneeSelectorModal.defaultProps = defaultProps; export default compose( - withLocalize, - withCurrentUserPersonalDetails, withOnyx({ reports: { key: ONYXKEYS.COLLECTION.REPORT, }, - betas: { - key: ONYXKEYS.BETAS, - }, task: { key: ONYXKEYS.TASK, }, - session: { - key: ONYXKEYS.SESSION, + }), + withOnyx({ + rootParentReportPolicy: { + key: ({reports, route}) => { + const report = reports[`${ONYXKEYS.COLLECTION.REPORT}${route.params?.reportID || '0'}`]; + const rootParentReport = ReportUtils.getRootParentReport(report); + return `${ONYXKEYS.COLLECTION.POLICY}${rootParentReport ? rootParentReport.policyID : '0'}`; + }, + selector: (policy) => lodashPick(policy, ['role']), }, }), )(TaskAssigneeSelectorModal); diff --git a/src/pages/workspace/WorkspacesListPage.tsx b/src/pages/workspace/WorkspacesListPage.tsx index 86b3d2aae51f..461bfd7eb5b3 100755 --- a/src/pages/workspace/WorkspacesListPage.tsx +++ b/src/pages/workspace/WorkspacesListPage.tsx @@ -169,21 +169,21 @@ function WorkspacesListPage({policies, allPolicyMembers, reimbursementAccount, r } return ( - - - {({hovered}) => ( + {({hovered}) => ( + - )} - - + + )} + ); }, [isSmallScreenWidth, styles.mb3, styles.mh5, styles.ph5, styles.hoveredComponentBG, translate], diff --git a/src/pages/workspace/WorkspacesListRow.tsx b/src/pages/workspace/WorkspacesListRow.tsx index 2f74d265c004..f16dfd9f0aa1 100644 --- a/src/pages/workspace/WorkspacesListRow.tsx +++ b/src/pages/workspace/WorkspacesListRow.tsx @@ -44,11 +44,17 @@ type WorkspacesListRowProps = WithCurrentUserPersonalDetailsProps & { * component will return null to prevent layout from jumping on initial render and when parent width changes. */ layoutWidth?: ValueOf; - /** Additional styles applied to the row */ + /** Custom styles applied to the row */ rowStyles?: StyleProp; + /** Additional styles from OfflineWithFeedback applied to the row */ + style?: StyleProp; + /** The type of brick road indicator to show. */ brickRoadIndicator?: ValueOf; + + /** Determines if three dots menu should be shown or not */ + shouldDisableThreeDotsMenu?: boolean; }; type BrickRoadIndicatorIconProps = { @@ -89,7 +95,9 @@ function WorkspacesListRow({ currentUserPersonalDetails, layoutWidth = CONST.LAYOUT_WIDTH.NONE, rowStyles, + style, brickRoadIndicator, + shouldDisableThreeDotsMenu, }: WorkspacesListRowProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); @@ -120,8 +128,10 @@ function WorkspacesListRow({ const isWide = layoutWidth === CONST.LAYOUT_WIDTH.WIDE; const isNarrow = layoutWidth === CONST.LAYOUT_WIDTH.NARROW; + const isDeleted = style && Array.isArray(style) ? style.includes(styles.offlineFeedback.deleted) : false; + return ( - + {title} @@ -143,6 +153,7 @@ function WorkspacesListRow({ )} @@ -158,13 +169,13 @@ function WorkspacesListRow({ {PersonalDetailsUtils.getDisplayNameOrDefault(ownerDetails)} {ownerDetails.login} @@ -182,13 +193,13 @@ function WorkspacesListRow({ {userFriendlyWorkspaceType} {translate('workspace.common.plan')} @@ -215,6 +226,7 @@ function WorkspacesListRow({ anchorAlignment={{horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.RIGHT, vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.TOP}} iconStyles={[styles.mr2]} shouldOverlay + disabled={shouldDisableThreeDotsMenu} /> diff --git a/src/types/onyx/Policy.ts b/src/types/onyx/Policy.ts index afdc236ca043..e63e87adfa2a 100644 --- a/src/types/onyx/Policy.ts +++ b/src/types/onyx/Policy.ts @@ -162,9 +162,6 @@ type Policy = { /** When tax tracking is enabled */ isTaxTrackingEnabled?: boolean; - /** The email of the reimburser set when reimbursement is direct */ - reimburserEmail?: string; - /** ReportID of the admins room for this workspace */ chatReportIDAdmins?: number;