diff --git a/android/app/build.gradle b/android/app/build.gradle index 812f1008777d..f714ed005740 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 1001044903 - versionName "1.4.49-3" + versionCode 1001044904 + versionName "1.4.49-4" } flavorDimensions "default" diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index 602cb1de71ba..712909df6b61 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -40,7 +40,7 @@ CFBundleVersion - 1.4.49.3 + 1.4.49.4 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 61ac30356852..b1a4aa336ab8 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -19,6 +19,6 @@ CFBundleSignature ???? CFBundleVersion - 1.4.49.3 + 1.4.49.4 diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index 8b48846258a3..c90bc159062b 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -13,7 +13,7 @@ CFBundleShortVersionString 1.4.49 CFBundleVersion - 1.4.49.3 + 1.4.49.4 NSExtension NSExtensionPointIdentifier diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 491ec28b59e5..c93dfba50f5a 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -35,9 +35,6 @@ PODS: - EXAV (13.10.4): - ExpoModulesCore - ReactCommon/turbomodule/core - - EXImageLoader (4.6.0): - - ExpoModulesCore - - React-Core - Expo (50.0.4): - ExpoModulesCore - ExpoImage (1.10.1): @@ -46,9 +43,6 @@ PODS: - SDWebImageAVIFCoder (~> 0.10.1) - SDWebImageSVGCoder (~> 1.7.0) - SDWebImageWebPCoder (~> 0.13.0) - - ExpoImageManipulator (11.8.0): - - EXImageLoader - - ExpoModulesCore - ExpoModulesCore (1.11.8): - glog - RCT-Folly (= 2022.05.16.00) @@ -1181,6 +1175,8 @@ PODS: - React-Core - react-native-geolocation (3.0.6): - React-Core + - react-native-image-manipulator (1.0.5): + - React - react-native-image-picker (7.0.3): - React-Core - react-native-key-command (1.0.6): @@ -1479,10 +1475,8 @@ DEPENDENCIES: - BVLinearGradient (from `../node_modules/react-native-linear-gradient`) - DoubleConversion (from `../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec`) - EXAV (from `../node_modules/expo-av/ios`) - - EXImageLoader (from `../node_modules/expo-image-loader/ios`) - Expo (from `../node_modules/expo`) - ExpoImage (from `../node_modules/expo-image/ios`) - - ExpoImageManipulator (from `../node_modules/expo-image-manipulator/ios`) - ExpoModulesCore (from `../node_modules/expo-modules-core`) - FBLazyVector (from `../node_modules/react-native/Libraries/FBLazyVector`) - FBReactNativeSpec (from `../node_modules/react-native/React/FBReactNativeSpec`) @@ -1542,6 +1536,7 @@ DEPENDENCIES: - react-native-config (from `../node_modules/react-native-config`) - react-native-document-picker (from `../node_modules/react-native-document-picker`) - "react-native-geolocation (from `../node_modules/@react-native-community/geolocation`)" + - "react-native-image-manipulator (from `../node_modules/@oguzhnatly/react-native-image-manipulator`)" - react-native-image-picker (from `../node_modules/react-native-image-picker`) - react-native-key-command (from `../node_modules/react-native-key-command`) - react-native-launch-arguments (from `../node_modules/react-native-launch-arguments`) @@ -1663,14 +1658,10 @@ EXTERNAL SOURCES: :podspec: "../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec" EXAV: :path: "../node_modules/expo-av/ios" - EXImageLoader: - :path: "../node_modules/expo-image-loader/ios" Expo: :path: "../node_modules/expo" ExpoImage: :path: "../node_modules/expo-image/ios" - ExpoImageManipulator: - :path: "../node_modules/expo-image-manipulator/ios" ExpoModulesCore: :path: "../node_modules/expo-modules-core" FBLazyVector: @@ -1740,6 +1731,8 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native-document-picker" react-native-geolocation: :path: "../node_modules/@react-native-community/geolocation" + react-native-image-manipulator: + :path: "../node_modules/@oguzhnatly/react-native-image-manipulator" react-native-image-picker: :path: "../node_modules/react-native-image-picker" react-native-key-command: @@ -1867,10 +1860,8 @@ SPEC CHECKSUMS: CocoaAsyncSocket: 065fd1e645c7abab64f7a6a2007a48038fdc6a99 DoubleConversion: fea03f2699887d960129cc54bba7e52542b6f953 EXAV: 09a4d87fa6b113fbb0ada3aade6799f78271cb44 - EXImageLoader: 55080616b2fe9da19ef8c7f706afd9814e279b6b Expo: 1e3bcf9dd99de57a636127057f6b488f0609681a ExpoImage: 1cdaa65a6a70bb01067e21ad1347ff2d973885f5 - ExpoImageManipulator: c1d7cb865eacd620a35659f3da34c70531f10b59 ExpoModulesCore: 96d1751929ad10622773bb729ab28a8423f0dd0c FBLazyVector: fbc4957d9aa695250b55d879c1d86f79d7e69ab4 FBReactNativeSpec: 86de768f89901ef6ed3207cd686362189d64ac88 @@ -1944,6 +1935,7 @@ SPEC CHECKSUMS: react-native-config: 7cd105e71d903104e8919261480858940a6b9c0e react-native-document-picker: 3599b238843369026201d2ef466df53f77ae0452 react-native-geolocation: 0f7fe8a4c2de477e278b0365cce27d089a8c5903 + react-native-image-manipulator: c48f64221cfcd46e9eec53619c4c0374f3328a56 react-native-image-picker: 2381c008bbb09e72395a2d043c147b11bd1523d9 react-native-key-command: 5af6ee30ff4932f78da6a2109017549042932aa5 react-native-launch-arguments: 5f41e0abf88a15e3c5309b8875d6fd5ac43df49d @@ -2007,7 +1999,7 @@ SPEC CHECKSUMS: SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17 Turf: 13d1a92d969ca0311bbc26e8356cca178ce95da2 VisionCamera: 0a6794d1974aed5d653d0d0cb900493e2583e35a - Yoga: 13c8ef87792450193e117976337b8527b49e8c03 + Yoga: e64aa65de36c0832d04e8c7bd614396c77a80047 PODFILE CHECKSUM: 0ccbb4f2406893c6e9f266dc1e7470dcd72885d2 diff --git a/package-lock.json b/package-lock.json index 8efb036099a9..81686286bf1c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "1.4.49-3", + "version": "1.4.49-4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.4.49-3", + "version": "1.4.49-4", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -23,6 +23,7 @@ "@invertase/react-native-apple-authentication": "^2.2.2", "@kie/act-js": "^2.6.0", "@kie/mock-github": "^1.0.0", + "@oguzhnatly/react-native-image-manipulator": "github:Expensify/react-native-image-manipulator#5cdae3d4455b03a04c57f50be3863e2fe6c92c52", "@onfido/react-native-sdk": "10.6.0", "@react-native-async-storage/async-storage": "1.21.0", "@react-native-camera-roll/camera-roll": "7.4.0", @@ -55,7 +56,6 @@ "expo": "^50.0.3", "expo-av": "~13.10.4", "expo-image": "1.10.1", - "expo-image-manipulator": "11.8.0", "fbjs": "^3.0.2", "htmlparser2": "^7.2.0", "idb-keyval": "^6.2.1", @@ -8048,6 +8048,12 @@ "@octokit/openapi-types": "^12.11.0" } }, + "node_modules/@oguzhnatly/react-native-image-manipulator": { + "version": "1.0.5", + "resolved": "git+ssh://git@github.com/Expensify/react-native-image-manipulator.git#5cdae3d4455b03a04c57f50be3863e2fe6c92c52", + "integrity": "sha512-C9Br1BQqm6io6lvYHptlLcOHbzlaqxp9tS35P8Qj3pdiiYRTzU3KPvZ61rQ+ZnZ4FOQ6MwPsKsmB8+6WHkAY6Q==", + "license": "MIT" + }, "node_modules/@onfido/active-video-capture": { "version": "0.28.6", "resolved": "https://registry.npmjs.org/@onfido/active-video-capture/-/active-video-capture-0.28.6.tgz", @@ -31144,25 +31150,6 @@ "expo": "*" } }, - "node_modules/expo-image-loader": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/expo-image-loader/-/expo-image-loader-4.6.0.tgz", - "integrity": "sha512-RHQTDak7/KyhWUxikn2yNzXL7i2cs16cMp6gEAgkHOjVhoCJQoOJ0Ljrt4cKQ3IowxgCuOrAgSUzGkqs7omj8Q==", - "peerDependencies": { - "expo": "*" - } - }, - "node_modules/expo-image-manipulator": { - "version": "11.8.0", - "resolved": "https://registry.npmjs.org/expo-image-manipulator/-/expo-image-manipulator-11.8.0.tgz", - "integrity": "sha512-ZWVrHnYmwJq6h7auk+ropsxcNi+LyZcPFKQc8oy+JA0SaJosfShvkCm7RADWAunHmfPCmjHrhwPGEu/rs7WG/A==", - "dependencies": { - "expo-image-loader": "~4.6.0" - }, - "peerDependencies": { - "expo": "*" - } - }, "node_modules/expo-keep-awake": { "version": "12.8.2", "resolved": "https://registry.npmjs.org/expo-keep-awake/-/expo-keep-awake-12.8.2.tgz", diff --git a/package.json b/package.json index 480806d7d111..7dab1aeaa386 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.4.49-3", + "version": "1.4.49-4", "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.", @@ -72,6 +72,7 @@ "@invertase/react-native-apple-authentication": "^2.2.2", "@kie/act-js": "^2.6.0", "@kie/mock-github": "^1.0.0", + "@oguzhnatly/react-native-image-manipulator": "github:Expensify/react-native-image-manipulator#5cdae3d4455b03a04c57f50be3863e2fe6c92c52", "@onfido/react-native-sdk": "10.6.0", "@react-native-async-storage/async-storage": "1.21.0", "@react-native-camera-roll/camera-roll": "7.4.0", @@ -104,7 +105,6 @@ "expo": "^50.0.3", "expo-av": "~13.10.4", "expo-image": "1.10.1", - "expo-image-manipulator": "11.8.0", "fbjs": "^3.0.2", "htmlparser2": "^7.2.0", "idb-keyval": "^6.2.1", diff --git a/patches/@oguzhnatly+react-native-image-manipulator+1.0.5.patch b/patches/@oguzhnatly+react-native-image-manipulator+1.0.5.patch new file mode 100644 index 000000000000..d5a390daf201 --- /dev/null +++ b/patches/@oguzhnatly+react-native-image-manipulator+1.0.5.patch @@ -0,0 +1,19 @@ +diff --git a/node_modules/@oguzhnatly/react-native-image-manipulator/android/build.gradle b/node_modules/@oguzhnatly/react-native-image-manipulator/android/build.gradle +index 3a1a548..fe030bb 100644 +--- a/node_modules/@oguzhnatly/react-native-image-manipulator/android/build.gradle ++++ b/node_modules/@oguzhnatly/react-native-image-manipulator/android/build.gradle +@@ -13,12 +13,12 @@ buildscript { + apply plugin: 'com.android.library' + + android { +- compileSdkVersion 28 ++ compileSdkVersion 34 + buildToolsVersion "28.0.3" + + defaultConfig { + minSdkVersion 16 +- targetSdkVersion 28 ++ targetSdkVersion 34 + versionCode 1 + versionName "1.0" + } diff --git a/src/CONST.ts b/src/CONST.ts index 645746c38a7d..ce2029c78713 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -1035,12 +1035,6 @@ const CONST = { VIDEO: 'video', }, - IMAGE_FILE_FORMAT: { - PNG: 'image/png', - WEBP: 'image/webp', - JPEG: 'image/jpeg', - }, - FILE_TYPE_REGEX: { // Image MimeTypes allowed by iOS photos app. IMAGE: /\.(jpg|jpeg|png|webp|gif|tiff|bmp|heic|heif)$/, diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 31e22491e2b9..bb1766f40e1f 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -241,9 +241,6 @@ const ONYXKEYS = { // This can be either "light", "dark" or "system" PREFERRED_THEME: 'preferredTheme', - // Experimental memory only Onyx mode flag - IS_USING_MEMORY_ONLY_KEYS: 'isUsingMemoryOnlyKeys', - // Information about the onyx updates IDs that were received from the server ONYX_UPDATES_FROM_SERVER: 'onyxUpdatesFromServer', @@ -566,7 +563,6 @@ type OnyxValuesMapping = { [ONYXKEYS.MY_DOMAIN_SECURITY_GROUPS]: Record; [ONYXKEYS.LAST_OPENED_PUBLIC_ROOM_ID]: string; [ONYXKEYS.PREFERRED_THEME]: ValueOf; - [ONYXKEYS.IS_USING_MEMORY_ONLY_KEYS]: boolean; [ONYXKEYS.MAPBOX_ACCESS_TOKEN]: OnyxTypes.MapboxAccessToken; [ONYXKEYS.ONYX_UPDATES_FROM_SERVER]: OnyxTypes.OnyxUpdatesFromServer; [ONYXKEYS.ONYX_UPDATES_LAST_UPDATE_ID_APPLIED_TO_CLIENT]: number; diff --git a/src/ROUTES.ts b/src/ROUTES.ts index db0068365760..856a6fb89a3e 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -385,9 +385,16 @@ const ROUTES = { getUrlWithBackToParam(`${action}/${iouType}/scan/${transactionID}/${reportID}`, backTo), }, MONEY_REQUEST_STEP_TAG: { - route: ':action/:iouType/tag/:tagIndex/:transactionID/:reportID', - getRoute: (action: ValueOf, iouType: ValueOf, tagIndex: number, transactionID: string, reportID: string, backTo = '') => - getUrlWithBackToParam(`${action}/${iouType}/tag/${tagIndex}/${transactionID}/${reportID}`, backTo), + route: ':action/:iouType/tag/:tagIndex/:transactionID/:reportID/:reportActionID?', + getRoute: ( + action: ValueOf, + iouType: ValueOf, + tagIndex: number, + transactionID: string, + reportID: string, + backTo = '', + reportActionID?: string, + ) => getUrlWithBackToParam(`${action}/${iouType}/tag/${tagIndex}/${transactionID}/${reportID}${reportActionID ? `/${reportActionID}` : ''}`, backTo), }, MONEY_REQUEST_STEP_WAYPOINT: { route: ':action/:iouType/waypoint/:transactionID/:reportID/:pageIndex', diff --git a/src/components/MoneyRequestConfirmationList.js b/src/components/MoneyRequestConfirmationList.js index 68114dcf4e4c..51a9148a3e6a 100755 --- a/src/components/MoneyRequestConfirmationList.js +++ b/src/components/MoneyRequestConfirmationList.js @@ -799,6 +799,7 @@ function MoneyRequestConfirmationList(props) { props.transaction.transactionID, props.reportID, Navigation.getActiveRouteWithoutParams(), + props.reportActionID, ), ); return; diff --git a/src/libs/Middleware/SaveResponseInOnyx.ts b/src/libs/Middleware/SaveResponseInOnyx.ts index 3b5cd60d84a4..ffe4f8a9bea9 100644 --- a/src/libs/Middleware/SaveResponseInOnyx.ts +++ b/src/libs/Middleware/SaveResponseInOnyx.ts @@ -1,8 +1,6 @@ import {SIDE_EFFECT_REQUEST_COMMANDS, WRITE_COMMANDS} from '@libs/API/types'; -import * as MemoryOnlyKeys from '@userActions/MemoryOnlyKeys/MemoryOnlyKeys'; import * as OnyxUpdates from '@userActions/OnyxUpdates'; import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; import type Middleware from './types'; // If we're executing any of these requests, we don't need to trigger our OnyxUpdates flow to update the current data even if our current value is out of @@ -19,16 +17,6 @@ const SaveResponseInOnyx: Middleware = (requestResponse, request) => return Promise.resolve(response); } - // If there is an OnyxUpdate for using memory only keys, enable them - onyxUpdates?.find(({key, value}) => { - if (key !== ONYXKEYS.IS_USING_MEMORY_ONLY_KEYS || !value) { - return false; - } - - MemoryOnlyKeys.enable(); - return true; - }); - const responseToApply = { type: CONST.ONYX_UPDATE_TYPES.HTTPS, lastUpdateID: Number(response?.lastUpdateID ?? 0), diff --git a/src/libs/Navigation/AppNavigator/AuthScreens.tsx b/src/libs/Navigation/AppNavigator/AuthScreens.tsx index 8c582b8ab259..a986e2995156 100644 --- a/src/libs/Navigation/AppNavigator/AuthScreens.tsx +++ b/src/libs/Navigation/AppNavigator/AuthScreens.tsx @@ -49,9 +49,6 @@ type AuthScreensProps = { /** The report ID of the last opened public room as anonymous user */ lastOpenedPublicRoomID: OnyxEntry; - /** Opt-in experimental mode that prevents certain Onyx keys from persisting to disk */ - isUsingMemoryOnlyKeys: OnyxEntry; - /** The last Onyx update ID was applied to the client */ initialLastUpdateIDAppliedToClient: OnyxEntry; }; @@ -150,7 +147,7 @@ const modalScreenListeners = { }, }; -function AuthScreens({session, lastOpenedPublicRoomID, isUsingMemoryOnlyKeys = false, initialLastUpdateIDAppliedToClient}: AuthScreensProps) { +function AuthScreens({session, lastOpenedPublicRoomID, initialLastUpdateIDAppliedToClient}: AuthScreensProps) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); const {isSmallScreenWidth} = useWindowDimensions(); @@ -168,7 +165,6 @@ function AuthScreens({session, lastOpenedPublicRoomID, isUsingMemoryOnlyKeys = f const chatShortcutConfig = CONST.KEYBOARD_SHORTCUTS.NEW_CHAT; const currentUrl = getCurrentUrl(); const isLoggingInAsNewUser = !!session?.email && SessionUtils.isLoggingInAsNewUser(currentUrl, session.email); - const shouldGetAllData = !!isUsingMemoryOnlyKeys || SessionUtils.didUserLogInDuringSession(); // Sign out the current user if we're transitioning with a different user const isTransitioning = currentUrl.includes(ROUTES.TRANSITION_BETWEEN_APPS); const isSupportalTransition = currentUrl.includes('authTokenType=support'); @@ -190,10 +186,7 @@ function AuthScreens({session, lastOpenedPublicRoomID, isUsingMemoryOnlyKeys = f // If we are on this screen then we are "logged in", but the user might not have "just logged in". They could be reopening the app // or returning from background. If so, we'll assume they have some app data already and we can call reconnectApp() instead of openApp(). - // Note: If a Guide has enabled the memory only key mode then we do want to run OpenApp as their app will not be rehydrated with - // the correct state on refresh. They are explicitly opting out of storing data they would need (i.e. reports_) to take advantage of - // the optimizations performed during ReconnectApp. - if (shouldGetAllData) { + if (SessionUtils.didUserLogInDuringSession()) { App.openApp(); } else { App.reconnectApp(initialLastUpdateIDAppliedToClient); @@ -392,9 +385,6 @@ export default withOnyx({ lastOpenedPublicRoomID: { key: ONYXKEYS.LAST_OPENED_PUBLIC_ROOM_ID, }, - isUsingMemoryOnlyKeys: { - key: ONYXKEYS.IS_USING_MEMORY_ONLY_KEYS, - }, initialLastUpdateIDAppliedToClient: { key: ONYXKEYS.ONYX_UPDATES_LAST_UPDATE_ID_APPLIED_TO_CLIENT, }, diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index 9f5b0ada4cb1..4689fd03ebd0 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -102,11 +102,6 @@ function isExpensifyTeam(email: string | undefined): boolean { return emailDomain === CONST.EXPENSIFY_PARTNER_NAME || emailDomain === CONST.EMAIL.GUIDES_DOMAIN; } -function isExpensifyGuideTeam(email: string): boolean { - const emailDomain = Str.extractEmailDomain(email ?? ''); - return emailDomain === CONST.EMAIL.GUIDES_DOMAIN; -} - /** * Checks if the current user is an admin of the policy. */ @@ -282,7 +277,6 @@ export { getPolicyBrickRoadIndicatorStatus, shouldShowPolicy, isExpensifyTeam, - isExpensifyGuideTeam, isInstantSubmitEnabled, isFreeGroupPolicy, isPolicyAdmin, diff --git a/src/libs/actions/MemoryOnlyKeys/MemoryOnlyKeys.ts b/src/libs/actions/MemoryOnlyKeys/MemoryOnlyKeys.ts deleted file mode 100644 index 15b9133f0aaf..000000000000 --- a/src/libs/actions/MemoryOnlyKeys/MemoryOnlyKeys.ts +++ /dev/null @@ -1,20 +0,0 @@ -import Onyx from 'react-native-onyx'; -import type {OnyxKey} from 'react-native-onyx'; -import Log from '@libs/Log'; -import ONYXKEYS from '@src/ONYXKEYS'; - -const memoryOnlyKeys: OnyxKey[] = [ONYXKEYS.COLLECTION.REPORT, ONYXKEYS.COLLECTION.POLICY, ONYXKEYS.PERSONAL_DETAILS_LIST]; - -const enable = () => { - Log.info('[MemoryOnlyKeys] enabled'); - Onyx.set(ONYXKEYS.IS_USING_MEMORY_ONLY_KEYS, true); - Onyx.setMemoryOnlyKeys(memoryOnlyKeys); -}; - -const disable = () => { - Log.info('[MemoryOnlyKeys] disabled'); - Onyx.set(ONYXKEYS.IS_USING_MEMORY_ONLY_KEYS, false); - Onyx.setMemoryOnlyKeys([]); -}; - -export {disable, enable}; diff --git a/src/libs/actions/MemoryOnlyKeys/exposeGlobalMemoryOnlyKeysMethods/index.native.ts b/src/libs/actions/MemoryOnlyKeys/exposeGlobalMemoryOnlyKeysMethods/index.native.ts deleted file mode 100644 index b89e03bdefdc..000000000000 --- a/src/libs/actions/MemoryOnlyKeys/exposeGlobalMemoryOnlyKeysMethods/index.native.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type ExposeGlobalMemoryOnlyKeysMethods from './types'; - -/** - * This is a no-op because the global methods will only work for web and desktop - */ -const exposeGlobalMemoryOnlyKeysMethods: ExposeGlobalMemoryOnlyKeysMethods = () => {}; - -export default exposeGlobalMemoryOnlyKeysMethods; diff --git a/src/libs/actions/MemoryOnlyKeys/exposeGlobalMemoryOnlyKeysMethods/index.ts b/src/libs/actions/MemoryOnlyKeys/exposeGlobalMemoryOnlyKeysMethods/index.ts deleted file mode 100644 index 4514edacb288..000000000000 --- a/src/libs/actions/MemoryOnlyKeys/exposeGlobalMemoryOnlyKeysMethods/index.ts +++ /dev/null @@ -1,13 +0,0 @@ -import * as MemoryOnlyKeys from '@userActions/MemoryOnlyKeys/MemoryOnlyKeys'; -import type ExposeGlobalMemoryOnlyKeysMethods from './types'; - -const exposeGlobalMemoryOnlyKeysMethods: ExposeGlobalMemoryOnlyKeysMethods = () => { - window.enableMemoryOnlyKeys = () => { - MemoryOnlyKeys.enable(); - }; - window.disableMemoryOnlyKeys = () => { - MemoryOnlyKeys.disable(); - }; -}; - -export default exposeGlobalMemoryOnlyKeysMethods; diff --git a/src/libs/actions/MemoryOnlyKeys/exposeGlobalMemoryOnlyKeysMethods/types.ts b/src/libs/actions/MemoryOnlyKeys/exposeGlobalMemoryOnlyKeysMethods/types.ts deleted file mode 100644 index 4cb50041b627..000000000000 --- a/src/libs/actions/MemoryOnlyKeys/exposeGlobalMemoryOnlyKeysMethods/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -type ExposeGlobalMemoryOnlyKeysMethods = () => void; - -export default ExposeGlobalMemoryOnlyKeysMethods; diff --git a/src/libs/cropOrRotateImage/getSaveFormat.ts b/src/libs/cropOrRotateImage/getSaveFormat.ts deleted file mode 100644 index 6d1ff280c5c5..000000000000 --- a/src/libs/cropOrRotateImage/getSaveFormat.ts +++ /dev/null @@ -1,17 +0,0 @@ -import {SaveFormat} from 'expo-image-manipulator'; -import CONST from '@src/CONST'; - -function getSaveFormat(type: string) { - switch (type) { - case CONST.IMAGE_FILE_FORMAT.PNG: - return SaveFormat.PNG; - case CONST.IMAGE_FILE_FORMAT.WEBP: - return SaveFormat.WEBP; - case CONST.IMAGE_FILE_FORMAT.JPEG: - return SaveFormat.JPEG; - default: - return SaveFormat.JPEG; - } -} - -export default getSaveFormat; diff --git a/src/libs/cropOrRotateImage/index.native.ts b/src/libs/cropOrRotateImage/index.native.ts index 03c233450435..c3fed59ba4fa 100644 --- a/src/libs/cropOrRotateImage/index.native.ts +++ b/src/libs/cropOrRotateImage/index.native.ts @@ -1,6 +1,5 @@ -import {manipulateAsync} from 'expo-image-manipulator'; +import RNImageManipulator from '@oguzhnatly/react-native-image-manipulator'; import RNFetchBlob from 'react-native-blob-util'; -import getSaveFormat from './getSaveFormat'; import type {CropOrRotateImage} from './types'; /** @@ -8,8 +7,7 @@ import type {CropOrRotateImage} from './types'; */ const cropOrRotateImage: CropOrRotateImage = (uri, actions, options) => new Promise((resolve) => { - const format = getSaveFormat(options.type); - manipulateAsync(uri, actions, {compress: options.compress, format}).then((result) => { + RNImageManipulator.manipulate(uri, actions, options).then((result) => { RNFetchBlob.fs.stat(result.uri.replace('file://', '')).then(({size}) => { resolve({ ...result, diff --git a/src/libs/cropOrRotateImage/index.ts b/src/libs/cropOrRotateImage/index.ts index 1b669999a1ee..ea3d51a465ec 100644 --- a/src/libs/cropOrRotateImage/index.ts +++ b/src/libs/cropOrRotateImage/index.ts @@ -1,19 +1,127 @@ -import {manipulateAsync} from 'expo-image-manipulator'; -import getSaveFormat from './getSaveFormat'; -import type {CropOrRotateImage} from './types'; +import type {CropOptions, CropOrRotateImage, CropOrRotateImageOptions} from './types'; -const cropOrRotateImage: CropOrRotateImage = (uri, actions, options) => - new Promise((resolve) => { - const format = getSaveFormat(options.type); - manipulateAsync(uri, actions, {compress: options.compress, format}).then((result) => { - fetch(result.uri) - .then((res) => res.blob()) - .then((blob) => { - const file = new File([blob], options.name || 'fileName.jpeg', {type: options.type || 'image/jpeg'}); - file.uri = URL.createObjectURL(file); - resolve(file); - }); +type SizeFromAngle = { + width: number; + height: number; +}; + +/** + * Calculates a size of canvas after rotation + */ +function sizeFromAngle(width: number, height: number, angle: number): SizeFromAngle { + const radians = (angle * Math.PI) / 180; + let sine = Math.cos(radians); + let cosine = Math.sin(radians); + if (cosine < 0) { + cosine = -cosine; + } + if (sine < 0) { + sine = -sine; + } + return {width: height * cosine + width * sine, height: height * sine + width * cosine}; +} + +/** + * Creates a new rotated canvas + */ +function rotateCanvas(canvas: HTMLCanvasElement, degrees: number): HTMLCanvasElement { + const {width, height} = sizeFromAngle(canvas.width, canvas.height, degrees); + + // We have to create a new canvas because it is not possible to change already drawn + // elements. Transformations such as rotation have to be applied before drawing + const result = document.createElement('canvas'); + result.width = width; + result.height = height; + + const context = result.getContext('2d'); + if (context) { + // In order to rotate image along its center we have to apply next transformation + context.translate(result.width / 2, result.height / 2); + + const radians = (degrees * Math.PI) / 180; + context.rotate(radians); + + context.drawImage(canvas, -canvas.width / 2, -canvas.height / 2, canvas.width, canvas.height); + } + return result; +} + +/** + * Creates new cropped canvas and returns it + */ +function cropCanvas(canvas: HTMLCanvasElement, options: CropOptions) { + let {originX = 0, originY = 0, width = 0, height = 0} = options; + const clamp = (value: number, max: number) => Math.max(0, Math.min(max, value)); + + width = clamp(width, canvas.width); + height = clamp(height, canvas.height); + originX = clamp(originX, canvas.width); + originY = clamp(originY, canvas.height); + + width = Math.min(originX + width, canvas.width) - originX; + height = Math.min(originY + height, canvas.height) - originY; + + const result = document.createElement('canvas'); + result.width = width; + result.height = height; + + const context = result.getContext('2d'); + context?.drawImage(canvas, originX, originY, width, height, 0, 0, width, height); + + return result; +} + +function convertCanvasToFile(canvas: HTMLCanvasElement, options: CropOrRotateImageOptions): Promise { + return new Promise((resolve) => { + canvas.toBlob((blob) => { + if (!blob) { + return; + } + const file = new File([blob], options.name || 'fileName.jpeg', {type: options.type || 'image/jpeg'}); + file.uri = URL.createObjectURL(file); + resolve(file); }); }); +} + +/** + * Loads image from specified url + */ +function loadImageAsync(uri: string): Promise { + return new Promise((resolve, reject) => { + const imageSource = new Image(); + imageSource.crossOrigin = 'anonymous'; + const canvas = document.createElement('canvas'); + imageSource.onload = () => { + canvas.width = imageSource.naturalWidth; + canvas.height = imageSource.naturalHeight; + + const context = canvas.getContext('2d'); + context?.drawImage(imageSource, 0, 0, imageSource.naturalWidth, imageSource.naturalHeight); + + resolve(canvas); + }; + imageSource.onerror = () => reject(canvas); + imageSource.src = uri; + }); +} + +/** + * Crops and rotates the image on web + */ + +const cropOrRotateImage: CropOrRotateImage = (uri, actions, options) => + loadImageAsync(uri).then((originalCanvas) => { + const resultCanvas = actions.reduce((canvas, action) => { + if (action.crop) { + return cropCanvas(canvas, action.crop); + } + if (action.rotate) { + return rotateCanvas(canvas, action.rotate); + } + return canvas; + }, originalCanvas); + return convertCanvasToFile(resultCanvas, options); + }); export default cropOrRotateImage; diff --git a/src/libs/cropOrRotateImage/types.ts b/src/libs/cropOrRotateImage/types.ts index 9d9b9ff98080..f882e4f9bea2 100644 --- a/src/libs/cropOrRotateImage/types.ts +++ b/src/libs/cropOrRotateImage/types.ts @@ -1,4 +1,4 @@ -import type {ImageResult} from 'expo-image-manipulator'; +import type {RNImageManipulatorResult} from '@oguzhnatly/react-native-image-manipulator'; type CropOrRotateImageOptions = { type: string; @@ -6,21 +6,20 @@ type CropOrRotateImageOptions = { compress: number; }; -type CropAction = { - crop: { - originX: number; - originY: number; - width: number; - height: number; - }; +type CropOptions = { + originX: number; + originY: number; + width: number; + height: number; }; -type RotateOption = {rotate: number}; - -type Action = CropAction | RotateOption; +type Action = { + crop?: CropOptions; + rotate?: number; +}; -type CustomRNImageManipulatorResult = ImageResult & {size: number; type: string; name: string}; +type CustomRNImageManipulatorResult = RNImageManipulatorResult & {size: number; type: string; name: string}; type CropOrRotateImage = (uri: string, actions: Action[], options: CropOrRotateImageOptions) => Promise; -export type {CustomRNImageManipulatorResult, CropOrRotateImage}; +export type {CropOrRotateImage, CropOptions, Action, CropOrRotateImageOptions, CustomRNImageManipulatorResult}; diff --git a/src/pages/iou/request/step/IOURequestStepTag.js b/src/pages/iou/request/step/IOURequestStepTag.js index 1b53dab12fa3..c1d1577a5d62 100644 --- a/src/pages/iou/request/step/IOURequestStepTag.js +++ b/src/pages/iou/request/step/IOURequestStepTag.js @@ -48,7 +48,16 @@ const propTypes = { policyTags: tagPropTypes, /** The actions from the parent report */ - parentReportActions: PropTypes.shape(reportActionPropTypes), + reportActions: PropTypes.shape(reportActionPropTypes), + + /** Session info for the currently logged in user. */ + session: PropTypes.shape({ + /** Currently logged in user accountID */ + accountID: PropTypes.number, + + /** Currently logged in user email */ + email: PropTypes.string, + }).isRequired, }; const defaultProps = { @@ -57,7 +66,7 @@ const defaultProps = { policyTags: null, policyCategories: null, transaction: {}, - parentReportActions: {}, + reportActions: {}, }; function IOURequestStepTag({ @@ -66,10 +75,11 @@ function IOURequestStepTag({ policyTags, report, route: { - params: {action, tagIndex: rawTagIndex, transactionID, backTo, iouType}, + params: {action, tagIndex: rawTagIndex, transactionID, backTo, iouType, reportActionID}, }, transaction, - parentReportActions, + reportActions, + session, }) { const styles = useThemeStyles(); const {translate} = useLocalize(); @@ -80,12 +90,14 @@ function IOURequestStepTag({ const tag = TransactionUtils.getTag(transaction, tagIndex); const isEditing = action === CONST.IOU.ACTION.EDIT; const isSplitBill = iouType === CONST.IOU.TYPE.SPLIT; + const reportAction = reportActions[report.parentReportActionID || reportActionID]; + const canEditSplitBill = isSplitBill && reportAction && session.accountID === reportAction.actorAccountID && TransactionUtils.areRequiredFieldsEmpty(transaction); const policyTagLists = useMemo(() => PolicyUtils.getTagLists(policyTags), [policyTags]); - const parentReportAction = parentReportActions[report.parentReportActionID]; + const shouldShowTag = ReportUtils.isGroupPolicy(report) && (transactionTag || OptionsListUtils.hasEnabledTags(policyTagLists)); // eslint-disable-next-line rulesdir/no-negated-variables - const shouldShowNotFoundPage = !shouldShowTag || (isEditing && !canEditMoneyRequest(parentReportAction)); + const shouldShowNotFoundPage = !shouldShowTag || (isEditing && (isSplitBill ? !canEditSplitBill : !canEditMoneyRequest(reportAction))); const navigateBack = () => { Navigation.goBack(backTo); @@ -154,9 +166,23 @@ export default compose( policyTags: { key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY_TAGS}${report ? report.policyID : '0'}`, }, - parentReportActions: { - key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report ? report.parentReportID : '0'}`, + reportActions: { + key: ({ + report, + route: { + params: {action, iouType}, + }, + }) => { + let reportID = '0'; + if (action === CONST.IOU.ACTION.EDIT) { + reportID = iouType === CONST.IOU.TYPE.SPLIT ? report.reportID : report.parentReportID; + } + return `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`; + }, canEvict: false, }, + session: { + key: ONYXKEYS.SESSION, + }, }), )(IOURequestStepTag); diff --git a/src/pages/signin/LoginForm/BaseLoginForm.tsx b/src/pages/signin/LoginForm/BaseLoginForm.tsx index bca0fbd2f8ef..4286a2603341 100644 --- a/src/pages/signin/LoginForm/BaseLoginForm.tsx +++ b/src/pages/signin/LoginForm/BaseLoginForm.tsx @@ -22,15 +22,12 @@ import useThemeStyles from '@hooks/useThemeStyles'; import canFocusInputOnScreenFocus from '@libs/canFocusInputOnScreenFocus'; import * as ErrorUtils from '@libs/ErrorUtils'; import isInputAutoFilled from '@libs/isInputAutoFilled'; -import Log from '@libs/Log'; import * as LoginUtils from '@libs/LoginUtils'; import {parsePhoneNumber} from '@libs/PhoneNumber'; -import * as PolicyUtils from '@libs/PolicyUtils'; import * as ValidationUtils from '@libs/ValidationUtils'; import Visibility from '@libs/Visibility'; import willBlurTextInputOnTapOutsideFunc from '@libs/willBlurTextInputOnTapOutside'; import * as CloseAccount from '@userActions/CloseAccount'; -import * as MemoryOnlyKeys from '@userActions/MemoryOnlyKeys/MemoryOnlyKeys'; import * as Session from '@userActions/Session'; import CONFIG from '@src/CONFIG'; import CONST from '@src/CONST'; @@ -151,12 +148,6 @@ function BaseLoginForm({account, credentials, closeAccount, blurOnSubmit = false const loginTrim = login.trim(); - // If the user has entered a guide email, then we are going to enable an experimental Onyx mode to help with performance - if (PolicyUtils.isExpensifyGuideTeam(loginTrim)) { - Log.info('Detected guide email in login field, setting memory only keys.'); - MemoryOnlyKeys.enable(); - } - const phoneLogin = LoginUtils.appendCountryCode(LoginUtils.getPhoneNumberWithoutSpecialChars(loginTrim)); const parsedPhoneNumber = parsePhoneNumber(phoneLogin); diff --git a/src/setup/index.ts b/src/setup/index.ts index a97862df9ae7..f79e6a461a3e 100644 --- a/src/setup/index.ts +++ b/src/setup/index.ts @@ -3,7 +3,6 @@ import Onyx from 'react-native-onyx'; import intlPolyfill from '@libs/IntlPolyfill'; import * as Metrics from '@libs/Metrics'; import * as Device from '@userActions/Device'; -import exposeGlobalMemoryOnlyKeysMethods from '@userActions/MemoryOnlyKeys/exposeGlobalMemoryOnlyKeysMethods'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import addUtilsToWindow from './addUtilsToWindow'; @@ -47,8 +46,6 @@ export default function () { }, }); - exposeGlobalMemoryOnlyKeysMethods(); - Device.setDeviceID(); // Force app layout to work left to right because our design does not currently support devices using this mode diff --git a/src/types/modules/react-native-onyx.d.ts b/src/types/modules/react-native-onyx.d.ts index ade71706a8e8..8498b03ec933 100644 --- a/src/types/modules/react-native-onyx.d.ts +++ b/src/types/modules/react-native-onyx.d.ts @@ -14,8 +14,6 @@ declare global { // Global methods for Onyx key management for debugging purposes // eslint-disable-next-line @typescript-eslint/consistent-type-definitions interface Window { - enableMemoryOnlyKeys: () => void; - disableMemoryOnlyKeys: () => void; Onyx: typeof Onyx; } }