From 800c2d6221bfdc55cd91528b5fa6ffe586dcb073 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Wed, 29 May 2024 13:18:10 +0800 Subject: [PATCH 001/146] shows a different message when approving all hold expenses --- src/components/ProcessMoneyReportHoldMenu.tsx | 16 ++++++++++++++-- src/languages/en.ts | 1 + 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/components/ProcessMoneyReportHoldMenu.tsx b/src/components/ProcessMoneyReportHoldMenu.tsx index 6e81c9d57bc8..e5240b6997c0 100644 --- a/src/components/ProcessMoneyReportHoldMenu.tsx +++ b/src/components/ProcessMoneyReportHoldMenu.tsx @@ -1,9 +1,10 @@ -import React from 'react'; +import React, {useMemo} from 'react'; import type {OnyxEntry} from 'react-native-onyx'; import useLocalize from '@hooks/useLocalize'; import Navigation from '@libs/Navigation/Navigation'; import {isLinkedTransactionHeld} from '@libs/ReportActionsUtils'; import * as IOU from '@userActions/IOU'; +import {TranslationPaths} from '@src/languages/types'; import ROUTES from '@src/ROUTES'; import type * as OnyxTypes from '@src/types/onyx'; import type {PaymentMethodType} from '@src/types/onyx/OriginalMessage'; @@ -64,12 +65,23 @@ function ProcessMoneyReportHoldMenu({ onClose(); }; + const promptText = useMemo(() => { + let promptTranslation: TranslationPaths; + if (nonHeldAmount) { + promptTranslation = isApprove ? 'iou.confirmApprovalAmount' : 'iou.confirmPayAmount'; + } else { + promptTranslation = isApprove ? 'iou.confirmApprovalAllHoldAmount' : 'iou.confirmPayAmount'; + } + + return translate(promptTranslation); + }, [nonHeldAmount]); + return ( onSubmit(false)} diff --git a/src/languages/en.ts b/src/languages/en.ts index 0f5822b9f411..9122d9292a96 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -753,6 +753,7 @@ export default { expenseOnHold: 'This expense was put on hold. Review the comments for next steps.', confirmApprove: 'Confirm approval amount', confirmApprovalAmount: "Approve what's not on hold, or approve the entire report.", + confirmApprovalAllHoldAmount: 'All expenses on this report are on hold. Do you want to unhold them and approve?', confirmPay: 'Confirm payment amount', confirmPayAmount: "Pay what's not on hold, or pay all out-of-pocket spend.", payOnly: 'Pay only', From 7bd1d725feb70cb5cfee306828df1f968523f644 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Tue, 11 Jun 2024 13:04:09 +0800 Subject: [PATCH 002/146] add new copy for paying a report with all hold expenses --- src/components/ProcessMoneyReportHoldMenu.tsx | 2 +- src/languages/en.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/ProcessMoneyReportHoldMenu.tsx b/src/components/ProcessMoneyReportHoldMenu.tsx index 12e2d818b715..7e2b60a868bc 100644 --- a/src/components/ProcessMoneyReportHoldMenu.tsx +++ b/src/components/ProcessMoneyReportHoldMenu.tsx @@ -74,7 +74,7 @@ function ProcessMoneyReportHoldMenu({ if (nonHeldAmount) { promptTranslation = isApprove ? 'iou.confirmApprovalAmount' : 'iou.confirmPayAmount'; } else { - promptTranslation = isApprove ? 'iou.confirmApprovalAllHoldAmount' : 'iou.confirmPayAmount'; + promptTranslation = isApprove ? 'iou.confirmApprovalAllHoldAmount' : 'iou.confirmPayAllHoldAmount'; } return translate(promptTranslation); diff --git a/src/languages/en.ts b/src/languages/en.ts index a90d0a5bb4b9..0569c3b1c551 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -769,6 +769,7 @@ export default { confirmApprovalAllHoldAmount: 'All expenses on this report are on hold. Do you want to unhold them and approve?', confirmPay: 'Confirm payment amount', confirmPayAmount: "Pay what's not on hold, or pay all out-of-pocket spend.", + confirmPayAllHoldAmount: 'All expenses on this report are on hold. Do you want to unhold them and pay?', payOnly: 'Pay only', approveOnly: 'Approve only', holdEducationalTitle: 'This expense is on', From bd8910c38b37105f005c7feb85db6531928f6e68 Mon Sep 17 00:00:00 2001 From: Yauheni Date: Thu, 13 Jun 2024 01:00:44 +0200 Subject: [PATCH 003/146] Add Handle image zoom for mobile browser apps --- .../AttachmentCarousel/CarouselItem.tsx | 6 +- .../Attachments/AttachmentCarousel/index.tsx | 5 +- .../AttachmentViewImage/index.tsx | 6 +- .../Attachments/AttachmentView/index.tsx | 5 + src/components/ImageView/index.tsx | 200 ++++++++++++++++-- src/components/ImageView/types.ts | 3 + 6 files changed, 204 insertions(+), 21 deletions(-) diff --git a/src/components/Attachments/AttachmentCarousel/CarouselItem.tsx b/src/components/Attachments/AttachmentCarousel/CarouselItem.tsx index 2ec1883fd7de..c0b3714ff5d8 100644 --- a/src/components/Attachments/AttachmentCarousel/CarouselItem.tsx +++ b/src/components/Attachments/AttachmentCarousel/CarouselItem.tsx @@ -19,6 +19,9 @@ type CarouselItemProps = { /** onPress callback */ onPress?: () => void; + /** onClose callback */ + onClose?: () => void; + /** Whether attachment carousel modal is hovered over */ isModalHovered?: boolean; @@ -26,7 +29,7 @@ type CarouselItemProps = { isFocused: boolean; }; -function CarouselItem({item, onPress, isFocused, isModalHovered}: CarouselItemProps) { +function CarouselItem({item, onPress, onClose, isFocused, isModalHovered}: CarouselItemProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); const {isAttachmentHidden} = useContext(ReportAttachmentsContext); @@ -77,6 +80,7 @@ function CarouselItem({item, onPress, isFocused, isModalHovered}: CarouselItemPr file={item.file} isAuthTokenRequired={item.isAuthTokenRequired} onPress={onPress} + onClose={onClose} transactionID={item.transactionID} reportActionID={item.reportActionID} isHovered={isModalHovered} diff --git a/src/components/Attachments/AttachmentCarousel/index.tsx b/src/components/Attachments/AttachmentCarousel/index.tsx index 947569538d32..f2ed12fc85eb 100644 --- a/src/components/Attachments/AttachmentCarousel/index.tsx +++ b/src/components/Attachments/AttachmentCarousel/index.tsx @@ -33,7 +33,7 @@ const viewabilityConfig = { const MIN_FLING_VELOCITY = 500; -function AttachmentCarousel({report, reportActions, parentReportActions, source, onNavigate, setDownloadButtonVisibility, type, accountID}: AttachmentCarouselProps) { +function AttachmentCarousel({report, reportActions, parentReportActions, source, onNavigate, onClose, setDownloadButtonVisibility, type, accountID}: AttachmentCarouselProps) { const theme = useTheme(); const {translate} = useLocalize(); const {isSmallScreenWidth, windowWidth} = useWindowDimensions(); @@ -174,6 +174,7 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source, ({item}: ListRenderItemInfo) => ( setShouldShowArrows((oldState) => !oldState) : undefined} @@ -181,7 +182,7 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source, /> ), - [activeSource, canUseTouchScreen, cellWidth, setShouldShowArrows, shouldShowArrows, styles.h100], + [activeSource, canUseTouchScreen, onClose, cellWidth, setShouldShowArrows, shouldShowArrows, styles.h100], ); /** Pan gesture handing swiping through attachments on touch screen devices */ const pan = useMemo( diff --git a/src/components/Attachments/AttachmentView/AttachmentViewImage/index.tsx b/src/components/Attachments/AttachmentView/AttachmentViewImage/index.tsx index c195c1e34554..6756742a978c 100644 --- a/src/components/Attachments/AttachmentView/AttachmentViewImage/index.tsx +++ b/src/components/Attachments/AttachmentView/AttachmentViewImage/index.tsx @@ -15,14 +15,18 @@ type AttachmentViewImageProps = Pick void; + + /** Function for handle on close */ + onClose?: () => void; }; -function AttachmentViewImage({url, file, isAuthTokenRequired, loadComplete, onPress, onError, isImage}: AttachmentViewImageProps) { +function AttachmentViewImage({url, file, isAuthTokenRequired, loadComplete, onPress, onError, onClose, isImage}: AttachmentViewImageProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); const children = ( void; + /** Function for handle on close */ + onClose?: () => void | undefined; + /** Whether this AttachmentView is shown as part of a AttachmentCarousel */ isUsedInCarousel?: boolean; @@ -84,6 +87,7 @@ function AttachmentView({ isFocused, isUsedInCarousel, isUsedInAttachmentModal, + onClose, isWorkspaceAvatar, maybeIcon, fallbackSource, @@ -220,6 +224,7 @@ function AttachmentView({ isAuthTokenRequired={isAuthTokenRequired} loadComplete={loadComplete} isImage={isImage} + onClose={onClose} onPress={onPress} onError={() => { setImageError(true); diff --git a/src/components/ImageView/index.tsx b/src/components/ImageView/index.tsx index f08941ef7d77..63ffa8fc93e0 100644 --- a/src/components/ImageView/index.tsx +++ b/src/components/ImageView/index.tsx @@ -1,16 +1,20 @@ import type {SyntheticEvent} from 'react'; -import React, {useCallback, useEffect, useRef, useState} from 'react'; -import type {GestureResponderEvent, LayoutChangeEvent} from 'react-native'; +import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; +import type {GestureResponderEvent, LayoutChangeEvent, ViewStyle} from 'react-native'; import {View} from 'react-native'; +import {Gesture, GestureDetector} from 'react-native-gesture-handler'; +import Animated, {useAnimatedStyle, useDerivedValue, useSharedValue, withSpring} from 'react-native-reanimated'; import AttachmentOfflineIndicator from '@components/AttachmentOfflineIndicator'; import FullscreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import Image from '@components/Image'; import RESIZE_MODES from '@components/Image/resizeModes'; import type {ImageOnLoadEvent} from '@components/Image/types'; +import {SPRING_CONFIG} from '@components/MultiGestureCanvas/constants'; import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; import useNetwork from '@hooks/useNetwork'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; +import addEncryptedAuthTokenToURL from '@libs/addEncryptedAuthTokenToURL'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import CONST from '@src/CONST'; import viewRef from '@src/types/utils/viewRef'; @@ -18,7 +22,7 @@ import type ImageViewProps from './types'; type ZoomDelta = {offsetX: number; offsetY: number}; -function ImageView({isAuthTokenRequired = false, url, fileName, onError}: ImageViewProps) { +function ImageView({isAuthTokenRequired = false, url, fileName, onError, onSwipeDown}: ImageViewProps) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); const [isLoading, setIsLoading] = useState(true); @@ -32,6 +36,8 @@ function ImageView({isAuthTokenRequired = false, url, fileName, onError}: ImageV const [initialX, setInitialX] = useState(0); const [initialY, setInitialY] = useState(0); const [imgWidth, setImgWidth] = useState(0); + const [imgContainerHeight, setImgContainerHeight] = useState(0); + const [imgContainerWidth, setImgContainerWidth] = useState(0); const [imgHeight, setImgHeight] = useState(0); const [zoomScale, setZoomScale] = useState(0); const [zoomDelta, setZoomDelta] = useState(); @@ -47,6 +53,131 @@ function ImageView({isAuthTokenRequired = false, url, fileName, onError}: ImageV const newZoomScale = Math.min(newContainerWidth / newImageWidth, newContainerHeight / newImageHeight); setZoomScale(newZoomScale); }; + const scale = useSharedValue(1); + const deltaScale = useSharedValue(1); + const minScale = 1.0; + const maxScale = 20; + const translationX = useSharedValue(1); + const translationY = useSharedValue(1); + const prevTranslationX = useSharedValue(0); + const prevTranslationY = useSharedValue(0); + const zoomedContentWidth = useDerivedValue(() => imgContainerWidth * scale.value, [imgContainerWidth, scale.value]); + const zoomedContentHeight = useDerivedValue(() => imgContainerHeight * scale.value, [imgContainerHeight, scale.value]); + const maxTranslateX = useMemo(() => imgContainerWidth / 2, [imgContainerWidth]); + const maxTranslateY = useMemo(() => containerHeight / 2, [containerHeight]); + const horizontalBoundaries = useMemo(() => { + let horizontalBoundary = 0; + if (containerWidth < zoomedContentWidth.value) { + horizontalBoundary = Math.abs(containerWidth - zoomedContentWidth.value) / 2; + } + return {min: -horizontalBoundary, max: horizontalBoundary}; + }, [containerWidth, zoomedContentWidth.value]); + const verticalBoundaries = useMemo(() => { + let verticalBoundary = 0; + if (containerHeight < zoomedContentHeight.value) { + verticalBoundary = Math.abs(zoomedContentHeight.value - containerHeight) / 2; + } + return {min: -verticalBoundary, max: verticalBoundary}; + }, [containerHeight, zoomedContentHeight.value]); + const pinchGesture = Gesture.Pinch() + .onStart(() => { + deltaScale.value = scale.value; + }) + .onUpdate((e) => { + if (scale.value < minScale / 2) { + return; + } + scale.value = deltaScale.value * e.scale; + }) + .onEnd(() => { + if (scale.value < minScale) { + scale.value = withSpring(minScale, SPRING_CONFIG); + translationX.value = 0; + translationY.value = 0; + } + if (scale.value > maxScale) { + scale.value = withSpring(maxScale, SPRING_CONFIG); + } + deltaScale.value = scale.value; + }) + .runOnJS(true); + const clamp = (val: number, min: number, max: number) => { + 'worklet'; + + return Math.min(Math.max(val, min), max); + }; + const panGesture = Gesture.Pan() + .onStart(() => { + 'worklet'; + + prevTranslationX.value = translationX.value; + prevTranslationY.value = translationY.value; + }) + .onUpdate((e) => { + 'worklet'; + + if (scale.value === minScale) { + if (e.translationX === 0 && e.translationY > 0) { + translationY.value = clamp(prevTranslationY.value + e.translationY, -maxTranslateY, maxTranslateY); + } else { + return; + } + } + translationX.value = clamp(prevTranslationX.value + e.translationX, -maxTranslateX, maxTranslateX); + if (zoomedContentHeight.value < containerHeight) { + return; + } + translationY.value = clamp(prevTranslationY.value + e.translationY, -maxTranslateY, maxTranslateY); + }) + .onEnd(() => { + 'worklet'; + + const swipeDownPadding = 150; + const dy = translationY.value + swipeDownPadding; + if (dy >= maxTranslateY && scale.value === minScale) { + if (onSwipeDown) { + onSwipeDown(); + } + } else if (scale.value === minScale) { + translationY.value = withSpring(0, SPRING_CONFIG); + translationX.value = withSpring(0, SPRING_CONFIG); + return; + } + const tsx = translationX.value * scale.value; + const tsy = translationY.value * scale.value; + const inHorizontalBoundaries = tsx >= horizontalBoundaries.min && tsx <= horizontalBoundaries.max; + const inVerticalBoundaries = tsy >= verticalBoundaries.min && tsy <= verticalBoundaries.max; + if (!inHorizontalBoundaries) { + const halfx = zoomedContentWidth.value / 2; + const diffx = halfx - translationX.value * scale.value; + const valx = maxTranslateX - diffx; + if (valx > 0) { + const p = (translationX.value * scale.value - valx) / scale.value; + translationX.value = withSpring(p, SPRING_CONFIG); + } + if (valx < 0) { + const p = (translationX.value * scale.value - valx) / scale.value; + translationX.value = withSpring(-p, SPRING_CONFIG); + } + } + if (!inVerticalBoundaries) { + if (zoomedContentHeight?.value < containerHeight) { + return; + } + const halfy = zoomedContentHeight.value / 2; + const diffy = halfy - translationY.value * scale.value; + const valy = maxTranslateY - diffy; + if (valy > 0) { + const p = (translationY.value * scale.value - valy) / scale.value; + translationY.value = withSpring(p, SPRING_CONFIG); + } + if (valy < 0) { + const p = (translationY.value * scale.value - valy) / scale.value; + translationY.value = withSpring(-p, SPRING_CONFIG); + } + } + }) + .runOnJS(true); const onContainerLayoutChanged = (e: LayoutChangeEvent) => { const {width, height} = e.nativeEvent.layout; @@ -195,25 +326,60 @@ function ImageView({isAuthTokenRequired = false, url, fileName, onError}: ImageV }; }, [canUseTouchScreen, trackMovement, trackPointerPosition]); + const animatedStyle = useAnimatedStyle(() => ({ + transform: [{scale: scale.value}, {translateX: translationX.value}, {translateY: translationY.value}], + })); + + const imgContainerStyle = useMemo(() => { + if (imgWidth >= imgHeight || imgHeight < containerHeight) { + const imgStyle: ViewStyle[] = [{width: imgWidth < containerWidth ? '100%' : '100%', aspectRatio: (imgHeight && imgWidth / imgHeight) || 1}]; + return imgStyle; + } + if (imgHeight > imgWidth) { + const imgStyle: ViewStyle[] = [{height: '100%', aspectRatio: (imgHeight && imgWidth / imgHeight) || 1}]; + return imgStyle; + } + }, [imgWidth, imgHeight, containerWidth, containerHeight]); + if (canUseTouchScreen) { return ( - 1 ? RESIZE_MODES.center : RESIZE_MODES.contain} - onLoadStart={imageLoadingStart} - onLoad={imageLoad} - onError={onError} - /> - {((isLoading && !isOffline) || (!isLoading && zoomScale === 0)) && } + + { + const {width, height} = e.nativeEvent.layout; + setImgContainerHeight(height); + setImgContainerWidth(width); + }} + > + { + const {width, height} = e.nativeEvent.source; + const params = { + nativeEvent: { + width, + height, + }, + }; + imageLoad(params); + }} + onError={onError} + /> + + + {isLoading && !isOffline && } {isLoading && } ); diff --git a/src/components/ImageView/types.ts b/src/components/ImageView/types.ts index b19e6b228cbd..aac6b994b5bc 100644 --- a/src/components/ImageView/types.ts +++ b/src/components/ImageView/types.ts @@ -14,6 +14,9 @@ type ImageViewProps = { /** Handles errors while displaying the image */ onError?: () => void; + /** Function to call when an user swipes down */ + onSwipeDown?: () => void; + /** Additional styles to add to the component */ style?: StyleProp; From 2bd1a2ae167ba58a6e1b8075b3f49e6c5bf75f45 Mon Sep 17 00:00:00 2001 From: Yauheni Date: Thu, 13 Jun 2024 01:11:54 +0200 Subject: [PATCH 004/146] Make a little refactoring --- src/components/ImageView/index.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/components/ImageView/index.tsx b/src/components/ImageView/index.tsx index 63ffa8fc93e0..59212a6fd317 100644 --- a/src/components/ImageView/index.tsx +++ b/src/components/ImageView/index.tsx @@ -331,15 +331,16 @@ function ImageView({isAuthTokenRequired = false, url, fileName, onError, onSwipe })); const imgContainerStyle = useMemo(() => { + const aspectRatio = (imgHeight && imgWidth / imgHeight) || 1; if (imgWidth >= imgHeight || imgHeight < containerHeight) { - const imgStyle: ViewStyle[] = [{width: imgWidth < containerWidth ? '100%' : '100%', aspectRatio: (imgHeight && imgWidth / imgHeight) || 1}]; + const imgStyle: ViewStyle[] = [{width: '100%', aspectRatio}]; return imgStyle; } if (imgHeight > imgWidth) { - const imgStyle: ViewStyle[] = [{height: '100%', aspectRatio: (imgHeight && imgWidth / imgHeight) || 1}]; + const imgStyle: ViewStyle[] = [{height: '100%', aspectRatio}]; return imgStyle; } - }, [imgWidth, imgHeight, containerWidth, containerHeight]); + }, [imgWidth, imgHeight, containerHeight]); if (canUseTouchScreen) { return ( From ca69b0fd30728774612e405cb780923348d38c0a Mon Sep 17 00:00:00 2001 From: Yauheni Date: Thu, 13 Jun 2024 01:42:11 +0200 Subject: [PATCH 005/146] Fix bug with close modal on send attachment screen --- src/components/AttachmentModal.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/AttachmentModal.tsx b/src/components/AttachmentModal.tsx index 54a073e30567..f0317dd904cb 100644 --- a/src/components/AttachmentModal.tsx +++ b/src/components/AttachmentModal.tsx @@ -547,6 +547,7 @@ function AttachmentModal({ source={sourceForAttachmentView} isAuthTokenRequired={isAuthTokenRequiredState} file={file} + onClose={closeModal} onToggleKeyboard={updateConfirmButtonVisibility} isWorkspaceAvatar={isWorkspaceAvatar} maybeIcon={maybeIcon} From 2e343765c5285ab19f0633ed3a61a95c1b547c00 Mon Sep 17 00:00:00 2001 From: Yauheni Date: Thu, 13 Jun 2024 01:46:25 +0200 Subject: [PATCH 006/146] Reset image position when onSwipeDown is undefined --- src/components/ImageView/index.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/components/ImageView/index.tsx b/src/components/ImageView/index.tsx index 59212a6fd317..deb44048d47e 100644 --- a/src/components/ImageView/index.tsx +++ b/src/components/ImageView/index.tsx @@ -137,6 +137,9 @@ function ImageView({isAuthTokenRequired = false, url, fileName, onError, onSwipe if (dy >= maxTranslateY && scale.value === minScale) { if (onSwipeDown) { onSwipeDown(); + } else { + translationY.value = withSpring(0, SPRING_CONFIG); + translationX.value = withSpring(0, SPRING_CONFIG); } } else if (scale.value === minScale) { translationY.value = withSpring(0, SPRING_CONFIG); From 58949066c8475b3faec960c5909ca7767cd742cb Mon Sep 17 00:00:00 2001 From: Yauheni Date: Thu, 13 Jun 2024 12:21:02 +0200 Subject: [PATCH 007/146] Refactor code --- src/components/AttachmentModal.tsx | 34 +-- .../AttachmentCarousel/CarouselItem.tsx | 6 +- .../Pager/AttachmentCarouselPagerContext.ts | 15 +- .../Attachments/AttachmentCarousel/index.tsx | 14 +- .../Attachments/AttachmentCarousel/types.ts | 8 + .../AttachmentViewImage/index.tsx | 6 +- .../Attachments/AttachmentView/index.tsx | 5 - src/components/ImageView/index.tsx | 201 +----------------- src/components/MultiGestureCanvas/index.tsx | 4 +- 9 files changed, 65 insertions(+), 228 deletions(-) diff --git a/src/components/AttachmentModal.tsx b/src/components/AttachmentModal.tsx index f0317dd904cb..2f5c85b10fb3 100644 --- a/src/components/AttachmentModal.tsx +++ b/src/components/AttachmentModal.tsx @@ -1,6 +1,7 @@ import {Str} from 'expensify-common'; -import React, {memo, useCallback, useEffect, useMemo, useState} from 'react'; +import React, {memo, useCallback, useEffect, useMemo, useRef, useState} from 'react'; import {Animated, Keyboard, View} from 'react-native'; +import type {GestureType} from 'react-native-gesture-handler'; import {GestureHandlerRootView} from 'react-native-gesture-handler'; import {withOnyx} from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx'; @@ -185,6 +186,8 @@ function AttachmentModal({ const nope = useSharedValue(false); const isOverlayModalVisible = (isReceiptAttachment && isDeleteReceiptConfirmModalVisible) || (!isReceiptAttachment && isAttachmentInvalid); const iouType = useMemo(() => (isTrackExpenseAction ? CONST.IOU.TYPE.TRACK : CONST.IOU.TYPE.SUBMIT), [isTrackExpenseAction]); + const pagerRef = useRef(null); + const [zoomScale, setZoomScale] = useState(1); const [file, setFile] = useState( originalFileName @@ -469,11 +472,13 @@ function AttachmentModal({ () => ({ pagerItems: [{source: sourceForAttachmentView, index: 0, isActive: true}], activePage: 0, - pagerRef: undefined, + pagerRef, isPagerScrolling: nope, isScrollEnabled: nope, onTap: () => {}, - onScaleChanged: () => {}, + onScaleChanged: (value: number) => { + setZoomScale(value); + }, onSwipeDown: closeModal, }), [closeModal, nope, sourceForAttachmentView], @@ -528,15 +533,19 @@ function AttachmentModal({ )} {!shouldShowNotFoundPage && (!isEmptyObject(report) && !isReceiptAttachment ? ( - + + + ) : ( !!sourceForAttachmentView && shouldLoadAttachment && @@ -547,7 +556,6 @@ function AttachmentModal({ source={sourceForAttachmentView} isAuthTokenRequired={isAuthTokenRequiredState} file={file} - onClose={closeModal} onToggleKeyboard={updateConfirmButtonVisibility} isWorkspaceAvatar={isWorkspaceAvatar} maybeIcon={maybeIcon} diff --git a/src/components/Attachments/AttachmentCarousel/CarouselItem.tsx b/src/components/Attachments/AttachmentCarousel/CarouselItem.tsx index c0b3714ff5d8..2ec1883fd7de 100644 --- a/src/components/Attachments/AttachmentCarousel/CarouselItem.tsx +++ b/src/components/Attachments/AttachmentCarousel/CarouselItem.tsx @@ -19,9 +19,6 @@ type CarouselItemProps = { /** onPress callback */ onPress?: () => void; - /** onClose callback */ - onClose?: () => void; - /** Whether attachment carousel modal is hovered over */ isModalHovered?: boolean; @@ -29,7 +26,7 @@ type CarouselItemProps = { isFocused: boolean; }; -function CarouselItem({item, onPress, onClose, isFocused, isModalHovered}: CarouselItemProps) { +function CarouselItem({item, onPress, isFocused, isModalHovered}: CarouselItemProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); const {isAttachmentHidden} = useContext(ReportAttachmentsContext); @@ -80,7 +77,6 @@ function CarouselItem({item, onPress, onClose, isFocused, isModalHovered}: Carou file={item.file} isAuthTokenRequired={item.isAuthTokenRequired} onPress={onPress} - onClose={onClose} transactionID={item.transactionID} reportActionID={item.reportActionID} isHovered={isModalHovered} diff --git a/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts b/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts index 87a9108d5f2e..c597b07487f0 100644 --- a/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts +++ b/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts @@ -1,5 +1,6 @@ import type {ForwardedRef} from 'react'; import {createContext} from 'react'; +import type {GestureType} from 'react-native-gesture-handler'; import type PagerView from 'react-native-pager-view'; import type {SharedValue} from 'react-native-reanimated'; import type {AttachmentSource} from '@components/Attachments/types'; @@ -22,11 +23,23 @@ type AttachmentCarouselPagerContextValue = { /** The index of the active page */ activePage: number; - pagerRef?: ForwardedRef; + + /** The ref of the active attachment */ + pagerRef?: ForwardedRef; + + /** The scroll state of the attachment */ isPagerScrolling: SharedValue; + + /** The scroll active of the attachment */ isScrollEnabled: SharedValue; + + /** The function to call after tap */ onTap: () => void; + + /** The function to call after scale */ onScaleChanged: (scale: number) => void; + + /** The function to call after swipe down */ onSwipeDown: () => void; }; diff --git a/src/components/Attachments/AttachmentCarousel/index.tsx b/src/components/Attachments/AttachmentCarousel/index.tsx index f2ed12fc85eb..611e79622075 100644 --- a/src/components/Attachments/AttachmentCarousel/index.tsx +++ b/src/components/Attachments/AttachmentCarousel/index.tsx @@ -1,7 +1,9 @@ import isEqual from 'lodash/isEqual'; +import type {MutableRefObject} from 'react'; import React, {useCallback, useEffect, useMemo, useState} from 'react'; import type {ListRenderItemInfo} from 'react-native'; import {Keyboard, PixelRatio, View} from 'react-native'; +import type {GestureType} from 'react-native-gesture-handler'; import {Gesture, GestureDetector} from 'react-native-gesture-handler'; import {withOnyx} from 'react-native-onyx'; import Animated, {scrollTo, useAnimatedRef} from 'react-native-reanimated'; @@ -33,7 +35,7 @@ const viewabilityConfig = { const MIN_FLING_VELOCITY = 500; -function AttachmentCarousel({report, reportActions, parentReportActions, source, onNavigate, onClose, setDownloadButtonVisibility, type, accountID}: AttachmentCarouselProps) { +function AttachmentCarousel({report, reportActions, parentReportActions, source, onNavigate, setDownloadButtonVisibility, type, accountID, pagerRef, zoomScale}: AttachmentCarouselProps) { const theme = useTheme(); const {translate} = useLocalize(); const {isSmallScreenWidth, windowWidth} = useWindowDimensions(); @@ -174,7 +176,6 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source, ({item}: ListRenderItemInfo) => ( setShouldShowArrows((oldState) => !oldState) : undefined} @@ -182,13 +183,13 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source, /> ), - [activeSource, canUseTouchScreen, onClose, cellWidth, setShouldShowArrows, shouldShowArrows, styles.h100], + [activeSource, canUseTouchScreen, cellWidth, setShouldShowArrows, shouldShowArrows, styles.h100], ); /** Pan gesture handing swiping through attachments on touch screen devices */ const pan = useMemo( () => Gesture.Pan() - .enabled(canUseTouchScreen) + .enabled(canUseTouchScreen && zoomScale === 1) .onUpdate(({translationX}) => scrollTo(scrollRef, page * cellWidth - translationX, 0, false)) .onEnd(({translationX, velocityX}) => { let newIndex; @@ -205,8 +206,9 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source, } scrollTo(scrollRef, newIndex * cellWidth, 0, true); - }), - [attachments.length, canUseTouchScreen, cellWidth, page, scrollRef], + }) + .withRef(pagerRef as MutableRefObject), + [attachments.length, canUseTouchScreen, cellWidth, page, pagerRef, scrollRef, zoomScale], ); return ( diff --git a/src/components/Attachments/AttachmentCarousel/types.ts b/src/components/Attachments/AttachmentCarousel/types.ts index d31ebbd328cd..984f914dfd33 100644 --- a/src/components/Attachments/AttachmentCarousel/types.ts +++ b/src/components/Attachments/AttachmentCarousel/types.ts @@ -1,4 +1,6 @@ +import type {ForwardedRef} from 'react'; import type {ViewToken} from 'react-native'; +import type {GestureType} from 'react-native-gesture-handler'; import type {OnyxEntry} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import type {Attachment, AttachmentSource} from '@components/Attachments/types'; @@ -38,6 +40,12 @@ type AttachmentCarouselProps = AttachmentCaraouselOnyxProps & { /** A callback that is called when swipe-down-to-close gesture happens */ onClose: () => void; + + /** The ref of the pager */ + pagerRef: ForwardedRef; + + /** The zoom scale of the attachment */ + zoomScale?: number; }; export type {AttachmentCarouselProps, UpdatePageProps, AttachmentCaraouselOnyxProps}; diff --git a/src/components/Attachments/AttachmentView/AttachmentViewImage/index.tsx b/src/components/Attachments/AttachmentView/AttachmentViewImage/index.tsx index 6756742a978c..c195c1e34554 100644 --- a/src/components/Attachments/AttachmentView/AttachmentViewImage/index.tsx +++ b/src/components/Attachments/AttachmentView/AttachmentViewImage/index.tsx @@ -15,18 +15,14 @@ type AttachmentViewImageProps = Pick void; - - /** Function for handle on close */ - onClose?: () => void; }; -function AttachmentViewImage({url, file, isAuthTokenRequired, loadComplete, onPress, onError, onClose, isImage}: AttachmentViewImageProps) { +function AttachmentViewImage({url, file, isAuthTokenRequired, loadComplete, onPress, onError, isImage}: AttachmentViewImageProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); const children = ( void; - /** Function for handle on close */ - onClose?: () => void | undefined; - /** Whether this AttachmentView is shown as part of a AttachmentCarousel */ isUsedInCarousel?: boolean; @@ -87,7 +84,6 @@ function AttachmentView({ isFocused, isUsedInCarousel, isUsedInAttachmentModal, - onClose, isWorkspaceAvatar, maybeIcon, fallbackSource, @@ -224,7 +220,6 @@ function AttachmentView({ isAuthTokenRequired={isAuthTokenRequired} loadComplete={loadComplete} isImage={isImage} - onClose={onClose} onPress={onPress} onError={() => { setImageError(true); diff --git a/src/components/ImageView/index.tsx b/src/components/ImageView/index.tsx index deb44048d47e..fa8f5fba993e 100644 --- a/src/components/ImageView/index.tsx +++ b/src/components/ImageView/index.tsx @@ -1,20 +1,17 @@ import type {SyntheticEvent} from 'react'; -import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; -import type {GestureResponderEvent, LayoutChangeEvent, ViewStyle} from 'react-native'; +import React, {useCallback, useEffect, useRef, useState} from 'react'; +import type {GestureResponderEvent, LayoutChangeEvent} from 'react-native'; import {View} from 'react-native'; -import {Gesture, GestureDetector} from 'react-native-gesture-handler'; -import Animated, {useAnimatedStyle, useDerivedValue, useSharedValue, withSpring} from 'react-native-reanimated'; import AttachmentOfflineIndicator from '@components/AttachmentOfflineIndicator'; import FullscreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import Image from '@components/Image'; import RESIZE_MODES from '@components/Image/resizeModes'; import type {ImageOnLoadEvent} from '@components/Image/types'; -import {SPRING_CONFIG} from '@components/MultiGestureCanvas/constants'; +import Lightbox from '@components/Lightbox'; import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; import useNetwork from '@hooks/useNetwork'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; -import addEncryptedAuthTokenToURL from '@libs/addEncryptedAuthTokenToURL'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import CONST from '@src/CONST'; import viewRef from '@src/types/utils/viewRef'; @@ -22,7 +19,7 @@ import type ImageViewProps from './types'; type ZoomDelta = {offsetX: number; offsetY: number}; -function ImageView({isAuthTokenRequired = false, url, fileName, onError, onSwipeDown}: ImageViewProps) { +function ImageView({isAuthTokenRequired = false, url, fileName, onError}: ImageViewProps) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); const [isLoading, setIsLoading] = useState(true); @@ -36,8 +33,6 @@ function ImageView({isAuthTokenRequired = false, url, fileName, onError, onSwipe const [initialX, setInitialX] = useState(0); const [initialY, setInitialY] = useState(0); const [imgWidth, setImgWidth] = useState(0); - const [imgContainerHeight, setImgContainerHeight] = useState(0); - const [imgContainerWidth, setImgContainerWidth] = useState(0); const [imgHeight, setImgHeight] = useState(0); const [zoomScale, setZoomScale] = useState(0); const [zoomDelta, setZoomDelta] = useState(); @@ -53,134 +48,6 @@ function ImageView({isAuthTokenRequired = false, url, fileName, onError, onSwipe const newZoomScale = Math.min(newContainerWidth / newImageWidth, newContainerHeight / newImageHeight); setZoomScale(newZoomScale); }; - const scale = useSharedValue(1); - const deltaScale = useSharedValue(1); - const minScale = 1.0; - const maxScale = 20; - const translationX = useSharedValue(1); - const translationY = useSharedValue(1); - const prevTranslationX = useSharedValue(0); - const prevTranslationY = useSharedValue(0); - const zoomedContentWidth = useDerivedValue(() => imgContainerWidth * scale.value, [imgContainerWidth, scale.value]); - const zoomedContentHeight = useDerivedValue(() => imgContainerHeight * scale.value, [imgContainerHeight, scale.value]); - const maxTranslateX = useMemo(() => imgContainerWidth / 2, [imgContainerWidth]); - const maxTranslateY = useMemo(() => containerHeight / 2, [containerHeight]); - const horizontalBoundaries = useMemo(() => { - let horizontalBoundary = 0; - if (containerWidth < zoomedContentWidth.value) { - horizontalBoundary = Math.abs(containerWidth - zoomedContentWidth.value) / 2; - } - return {min: -horizontalBoundary, max: horizontalBoundary}; - }, [containerWidth, zoomedContentWidth.value]); - const verticalBoundaries = useMemo(() => { - let verticalBoundary = 0; - if (containerHeight < zoomedContentHeight.value) { - verticalBoundary = Math.abs(zoomedContentHeight.value - containerHeight) / 2; - } - return {min: -verticalBoundary, max: verticalBoundary}; - }, [containerHeight, zoomedContentHeight.value]); - const pinchGesture = Gesture.Pinch() - .onStart(() => { - deltaScale.value = scale.value; - }) - .onUpdate((e) => { - if (scale.value < minScale / 2) { - return; - } - scale.value = deltaScale.value * e.scale; - }) - .onEnd(() => { - if (scale.value < minScale) { - scale.value = withSpring(minScale, SPRING_CONFIG); - translationX.value = 0; - translationY.value = 0; - } - if (scale.value > maxScale) { - scale.value = withSpring(maxScale, SPRING_CONFIG); - } - deltaScale.value = scale.value; - }) - .runOnJS(true); - const clamp = (val: number, min: number, max: number) => { - 'worklet'; - - return Math.min(Math.max(val, min), max); - }; - const panGesture = Gesture.Pan() - .onStart(() => { - 'worklet'; - - prevTranslationX.value = translationX.value; - prevTranslationY.value = translationY.value; - }) - .onUpdate((e) => { - 'worklet'; - - if (scale.value === minScale) { - if (e.translationX === 0 && e.translationY > 0) { - translationY.value = clamp(prevTranslationY.value + e.translationY, -maxTranslateY, maxTranslateY); - } else { - return; - } - } - translationX.value = clamp(prevTranslationX.value + e.translationX, -maxTranslateX, maxTranslateX); - if (zoomedContentHeight.value < containerHeight) { - return; - } - translationY.value = clamp(prevTranslationY.value + e.translationY, -maxTranslateY, maxTranslateY); - }) - .onEnd(() => { - 'worklet'; - - const swipeDownPadding = 150; - const dy = translationY.value + swipeDownPadding; - if (dy >= maxTranslateY && scale.value === minScale) { - if (onSwipeDown) { - onSwipeDown(); - } else { - translationY.value = withSpring(0, SPRING_CONFIG); - translationX.value = withSpring(0, SPRING_CONFIG); - } - } else if (scale.value === minScale) { - translationY.value = withSpring(0, SPRING_CONFIG); - translationX.value = withSpring(0, SPRING_CONFIG); - return; - } - const tsx = translationX.value * scale.value; - const tsy = translationY.value * scale.value; - const inHorizontalBoundaries = tsx >= horizontalBoundaries.min && tsx <= horizontalBoundaries.max; - const inVerticalBoundaries = tsy >= verticalBoundaries.min && tsy <= verticalBoundaries.max; - if (!inHorizontalBoundaries) { - const halfx = zoomedContentWidth.value / 2; - const diffx = halfx - translationX.value * scale.value; - const valx = maxTranslateX - diffx; - if (valx > 0) { - const p = (translationX.value * scale.value - valx) / scale.value; - translationX.value = withSpring(p, SPRING_CONFIG); - } - if (valx < 0) { - const p = (translationX.value * scale.value - valx) / scale.value; - translationX.value = withSpring(-p, SPRING_CONFIG); - } - } - if (!inVerticalBoundaries) { - if (zoomedContentHeight?.value < containerHeight) { - return; - } - const halfy = zoomedContentHeight.value / 2; - const diffy = halfy - translationY.value * scale.value; - const valy = maxTranslateY - diffy; - if (valy > 0) { - const p = (translationY.value * scale.value - valy) / scale.value; - translationY.value = withSpring(p, SPRING_CONFIG); - } - if (valy < 0) { - const p = (translationY.value * scale.value - valy) / scale.value; - translationY.value = withSpring(-p, SPRING_CONFIG); - } - } - }) - .runOnJS(true); const onContainerLayoutChanged = (e: LayoutChangeEvent) => { const {width, height} = e.nativeEvent.layout; @@ -329,63 +196,13 @@ function ImageView({isAuthTokenRequired = false, url, fileName, onError, onSwipe }; }, [canUseTouchScreen, trackMovement, trackPointerPosition]); - const animatedStyle = useAnimatedStyle(() => ({ - transform: [{scale: scale.value}, {translateX: translationX.value}, {translateY: translationY.value}], - })); - - const imgContainerStyle = useMemo(() => { - const aspectRatio = (imgHeight && imgWidth / imgHeight) || 1; - if (imgWidth >= imgHeight || imgHeight < containerHeight) { - const imgStyle: ViewStyle[] = [{width: '100%', aspectRatio}]; - return imgStyle; - } - if (imgHeight > imgWidth) { - const imgStyle: ViewStyle[] = [{height: '100%', aspectRatio}]; - return imgStyle; - } - }, [imgWidth, imgHeight, containerHeight]); - if (canUseTouchScreen) { return ( - - - { - const {width, height} = e.nativeEvent.layout; - setImgContainerHeight(height); - setImgContainerWidth(width); - }} - > - { - const {width, height} = e.nativeEvent.source; - const params = { - nativeEvent: { - width, - height, - }, - }; - imageLoad(params); - }} - onError={onError} - /> - - - {isLoading && !isOffline && } - {isLoading && } - + ); } return ( diff --git a/src/components/MultiGestureCanvas/index.tsx b/src/components/MultiGestureCanvas/index.tsx index 31a1f7a2c3d8..2e5cf8018c70 100644 --- a/src/components/MultiGestureCanvas/index.tsx +++ b/src/components/MultiGestureCanvas/index.tsx @@ -1,6 +1,7 @@ import type {ForwardedRef} from 'react'; import React, {useEffect, useMemo, useRef} from 'react'; import {View} from 'react-native'; +import type {GestureType} from 'react-native-gesture-handler'; import {Gesture, GestureDetector} from 'react-native-gesture-handler'; import type {GestureRef} from 'react-native-gesture-handler/lib/typescript/handlers/gestures/gesture'; import type PagerView from 'react-native-pager-view'; @@ -40,7 +41,7 @@ type MultiGestureCanvasProps = ChildrenProps & { shouldDisableTransformationGestures?: SharedValue; /** If there is a pager wrapping the canvas, we need to disable the pan gesture in case the pager is swiping */ - pagerRef?: ForwardedRef; // TODO: For TS migration: Exclude + pagerRef?: ForwardedRef; // TODO: For TS migration: Exclude /** Handles scale changed event */ onScaleChanged?: OnScaleChangedCallback; @@ -48,6 +49,7 @@ type MultiGestureCanvasProps = ChildrenProps & { /** Handles scale changed event */ onTap?: OnTapCallback; + /** Handles swipe down event */ onSwipeDown?: OnSwipeDownCallback; }; From e30a3bbc8e9c1040fb5b32740081f303ff21552d Mon Sep 17 00:00:00 2001 From: Yauheni Date: Thu, 13 Jun 2024 12:29:45 +0200 Subject: [PATCH 008/146] Make some minnor changes --- src/components/AttachmentModal.tsx | 11 ++++++++--- .../Pager/AttachmentCarouselPagerContext.ts | 16 ++++++++-------- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/src/components/AttachmentModal.tsx b/src/components/AttachmentModal.tsx index 2f5c85b10fb3..5838577bcc5d 100644 --- a/src/components/AttachmentModal.tsx +++ b/src/components/AttachmentModal.tsx @@ -397,6 +397,13 @@ function AttachmentModal({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [onModalClose]); + /** + * scale handler for attachment + */ + const scaleChanged = (value: number) => { + setZoomScale(value); + }; + /** * open the modal */ @@ -476,9 +483,7 @@ function AttachmentModal({ isPagerScrolling: nope, isScrollEnabled: nope, onTap: () => {}, - onScaleChanged: (value: number) => { - setZoomScale(value); - }, + onScaleChanged: scaleChanged, onSwipeDown: closeModal, }), [closeModal, nope, sourceForAttachmentView], diff --git a/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts b/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts index c597b07487f0..7af38f300532 100644 --- a/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts +++ b/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts @@ -18,28 +18,28 @@ type AttachmentCarouselPagerItems = { }; type AttachmentCarouselPagerContextValue = { - /** The list of items that are shown in the pager */ + /** List of items displayed in the attachment */ pagerItems: AttachmentCarouselPagerItems[]; - /** The index of the active page */ + /** Index of the currently active page */ activePage: number; - /** The ref of the active attachment */ + /** Ref to the active attachment */ pagerRef?: ForwardedRef; - /** The scroll state of the attachment */ + /** Indicates if the pager is currently scrolling */ isPagerScrolling: SharedValue; - /** The scroll active of the attachment */ + /** Indicates if scrolling is enabled for the attachment */ isScrollEnabled: SharedValue; - /** The function to call after tap */ + /** Function to call after a tap event */ onTap: () => void; - /** The function to call after scale */ + /** Function to call when the scale changes */ onScaleChanged: (scale: number) => void; - /** The function to call after swipe down */ + /** Function to call after a swipe down event */ onSwipeDown: () => void; }; From 2f40b541ad23de47838eca6cf9fb6d91cfc9c7ea Mon Sep 17 00:00:00 2001 From: Yauheni Date: Thu, 13 Jun 2024 12:58:04 +0200 Subject: [PATCH 009/146] Update animation for changing attachment item --- src/components/Attachments/AttachmentCarousel/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Attachments/AttachmentCarousel/index.tsx b/src/components/Attachments/AttachmentCarousel/index.tsx index 611e79622075..ebb43ef45eb9 100644 --- a/src/components/Attachments/AttachmentCarousel/index.tsx +++ b/src/components/Attachments/AttachmentCarousel/index.tsx @@ -190,7 +190,7 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source, () => Gesture.Pan() .enabled(canUseTouchScreen && zoomScale === 1) - .onUpdate(({translationX}) => scrollTo(scrollRef, page * cellWidth - translationX, 0, false)) + .onUpdate(({translationX}) => scrollTo(scrollRef, page * cellWidth - translationX * 2, 0, false)) .onEnd(({translationX, velocityX}) => { let newIndex; if (velocityX > MIN_FLING_VELOCITY) { From ebb0eb4d3c8989e0d5e1fb2665fb46897f9ac4db Mon Sep 17 00:00:00 2001 From: Yauheni Date: Thu, 13 Jun 2024 13:33:16 +0200 Subject: [PATCH 010/146] Remove unnecessary type --- src/components/ImageView/types.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/components/ImageView/types.ts b/src/components/ImageView/types.ts index aac6b994b5bc..b19e6b228cbd 100644 --- a/src/components/ImageView/types.ts +++ b/src/components/ImageView/types.ts @@ -14,9 +14,6 @@ type ImageViewProps = { /** Handles errors while displaying the image */ onError?: () => void; - /** Function to call when an user swipes down */ - onSwipeDown?: () => void; - /** Additional styles to add to the component */ style?: StyleProp; From fec109e91133849daedd3c52eb4b03010242802a Mon Sep 17 00:00:00 2001 From: Yauheni Date: Fri, 14 Jun 2024 21:05:55 +0200 Subject: [PATCH 011/146] Fix minnor issues related with arrows --- .../Attachments/AttachmentCarousel/index.tsx | 13 ++++++++--- .../AttachmentCarousel/useCarouselArrows.ts | 23 ++++++++++++++++++- 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/src/components/Attachments/AttachmentCarousel/index.tsx b/src/components/Attachments/AttachmentCarousel/index.tsx index ebb43ef45eb9..99332e703790 100644 --- a/src/components/Attachments/AttachmentCarousel/index.tsx +++ b/src/components/Attachments/AttachmentCarousel/index.tsx @@ -53,7 +53,14 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source, const [page, setPage] = useState(0); const [attachments, setAttachments] = useState([]); const [activeSource, setActiveSource] = useState(source); - const {shouldShowArrows, setShouldShowArrows, autoHideArrows, cancelAutoHideArrows} = useCarouselArrows(); + const {shouldShowArrows, setShouldShowArrows, autoHideArrows, cancelAutoHideArrows, onChangeArrowsState} = useCarouselArrows(); + + useEffect(() => { + if (!canUseTouchScreen || zoomScale !== 1) { + return; + } + setShouldShowArrows(true); + }, [canUseTouchScreen, page, setShouldShowArrows, zoomScale]); const compareImage = useCallback((attachment: Attachment) => attachment.source === source, [source]); @@ -178,12 +185,12 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source, setShouldShowArrows((oldState) => !oldState) : undefined} + onPress={canUseTouchScreen ? () => onChangeArrowsState(zoomScale === 1) : undefined} isModalHovered={shouldShowArrows} /> ), - [activeSource, canUseTouchScreen, cellWidth, setShouldShowArrows, shouldShowArrows, styles.h100], + [activeSource, canUseTouchScreen, cellWidth, zoomScale, onChangeArrowsState, shouldShowArrows, styles.h100], ); /** Pan gesture handing swiping through attachments on touch screen devices */ const pan = useMemo( diff --git a/src/components/Attachments/AttachmentCarousel/useCarouselArrows.ts b/src/components/Attachments/AttachmentCarousel/useCarouselArrows.ts index 12ca3db4e2ff..1b21a3af6000 100644 --- a/src/components/Attachments/AttachmentCarousel/useCarouselArrows.ts +++ b/src/components/Attachments/AttachmentCarousel/useCarouselArrows.ts @@ -7,6 +7,7 @@ function useCarouselArrows() { const canUseTouchScreen = DeviceCapabilities.canUseTouchScreen(); const [shouldShowArrows, setShouldShowArrowsInternal] = useState(canUseTouchScreen); const autoHideArrowTimeout = useRef(null); + const singleTapRef = useRef(null); /** * Cancels the automatic hiding of the arrows. @@ -45,7 +46,27 @@ function useCarouselArrows() { // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - return {shouldShowArrows, setShouldShowArrows, autoHideArrows, cancelAutoHideArrows}; + const onChangeArrowsState = useCallback( + (enabled: boolean) => { + if (!enabled) { + return; + } + + if (singleTapRef.current) { + clearTimeout(singleTapRef.current); + singleTapRef.current = null; + return; + } + + singleTapRef.current = setTimeout(() => { + setShouldShowArrows((oldState) => !oldState); + singleTapRef.current = null; + }, 200); + }, + [setShouldShowArrows], + ); + + return {shouldShowArrows, setShouldShowArrows, autoHideArrows, cancelAutoHideArrows, onChangeArrowsState}; } export default useCarouselArrows; From 6c6390b443aadf6c863b06f907dace581357af4f Mon Sep 17 00:00:00 2001 From: Yauheni Date: Fri, 14 Jun 2024 22:14:59 +0200 Subject: [PATCH 012/146] Fix bug with animation for carousel --- src/components/Attachments/AttachmentCarousel/index.tsx | 2 +- src/components/MultiGestureCanvas/usePanGesture.ts | 4 ---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/src/components/Attachments/AttachmentCarousel/index.tsx b/src/components/Attachments/AttachmentCarousel/index.tsx index 99332e703790..35accf837caa 100644 --- a/src/components/Attachments/AttachmentCarousel/index.tsx +++ b/src/components/Attachments/AttachmentCarousel/index.tsx @@ -197,7 +197,7 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source, () => Gesture.Pan() .enabled(canUseTouchScreen && zoomScale === 1) - .onUpdate(({translationX}) => scrollTo(scrollRef, page * cellWidth - translationX * 2, 0, false)) + .onUpdate(({translationX}) => scrollTo(scrollRef, page * cellWidth - translationX, 0, false)) .onEnd(({translationX, velocityX}) => { let newIndex; if (velocityX > MIN_FLING_VELOCITY) { diff --git a/src/components/MultiGestureCanvas/usePanGesture.ts b/src/components/MultiGestureCanvas/usePanGesture.ts index 903f384dd525..c236393027ef 100644 --- a/src/components/MultiGestureCanvas/usePanGesture.ts +++ b/src/components/MultiGestureCanvas/usePanGesture.ts @@ -206,10 +206,6 @@ const usePanGesture = ({ panVelocityX.value = evt.velocityX; panVelocityY.value = evt.velocityY; - if (!isSwipingDownToClose.value) { - panTranslateX.value += evt.changeX; - } - if (enableSwipeDownToClose.value || isSwipingDownToClose.value) { panTranslateY.value += evt.changeY; } From 879efb76df8de3316fed4489b73259945551ae98 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Sat, 15 Jun 2024 11:54:05 +0800 Subject: [PATCH 013/146] update translation --- src/components/MoneyReportHeader.tsx | 1 + src/components/ProcessMoneyReportHoldMenu.tsx | 17 ++++++++++------- .../ReportActionItem/ReportPreview.tsx | 1 + src/languages/en.ts | 5 +++-- src/languages/types.ts | 3 +++ 5 files changed, 18 insertions(+), 9 deletions(-) diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index 90952157f179..38ef7812c117 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -356,6 +356,7 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea paymentType={paymentType} chatReport={chatReport} moneyRequestReport={moneyRequestReport} + transactionCount={transactionIDs.length} /> )} { - let promptTranslation: TranslationPaths; if (nonHeldAmount) { - promptTranslation = isApprove ? 'iou.confirmApprovalAmount' : 'iou.confirmPayAmount'; + return translate(isApprove ? 'iou.confirmApprovalAmount' : 'iou.confirmPayAmount'); } else { - promptTranslation = isApprove ? 'iou.confirmApprovalAllHoldAmount' : 'iou.confirmPayAllHoldAmount'; + return translate( + isApprove ? 'iou.confirmApprovalAllHoldAmount' : 'iou.confirmPayAllHoldAmount', + {transactionCount} + ); } - - return translate(promptTranslation); - }, [nonHeldAmount]); + }, [nonHeldAmount, transactionCount, translate, isApprove]); return ( )} diff --git a/src/languages/en.ts b/src/languages/en.ts index 59c9da46a36d..3b2ac5c46d8d 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -13,6 +13,7 @@ import type { BeginningOfChatHistoryDomainRoomPartOneParams, CanceledRequestParams, CharacterLimitParams, + ConfirmHoldExpenseParams, ConfirmThatParams, DateShouldBeAfterParams, DateShouldBeBeforeParams, @@ -766,10 +767,10 @@ export default { reviewDuplicates: 'Review duplicates', confirmApprove: 'Confirm approval amount', confirmApprovalAmount: "Approve what's not on hold, or approve the entire report.", - confirmApprovalAllHoldAmount: 'All expenses on this report are on hold. Do you want to unhold them and approve?', + confirmApprovalAllHoldAmount: ({transactionCount}: ConfirmHoldExpenseParams) => `${Str.pluralize('This expense is', 'These expenses are', transactionCount)} on hold. Do you want to approve anyway?`, confirmPay: 'Confirm payment amount', confirmPayAmount: "Pay what's not on hold, or pay all out-of-pocket spend.", - confirmPayAllHoldAmount: 'All expenses on this report are on hold. Do you want to unhold them and pay?', + confirmPayAllHoldAmount: ({transactionCount}: ConfirmHoldExpenseParams) => `${Str.pluralize('This expense is', 'These expenses are', transactionCount)} on hold. Do you want to pay anyway?`, payOnly: 'Pay only', approveOnly: 'Approve only', holdEducationalTitle: 'This expense is on', diff --git a/src/languages/types.ts b/src/languages/types.ts index de9b1d2dadeb..b6553be196e1 100644 --- a/src/languages/types.ts +++ b/src/languages/types.ts @@ -298,6 +298,8 @@ type DistanceRateOperationsParams = {count: number}; type ReimbursementRateParams = {unit: Unit}; +type ConfirmHoldExpenseParams = {transactionCount: number}; + export type { AddressLineParams, AdminCanceledRequestParams, @@ -309,6 +311,7 @@ export type { BeginningOfChatHistoryDomainRoomPartOneParams, CanceledRequestParams, CharacterLimitParams, + ConfirmHoldExpenseParams, ConfirmThatParams, DateShouldBeAfterParams, DateShouldBeBeforeParams, From fc6515ed4fe59e189333d85b00f31fd249928c87 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Sat, 15 Jun 2024 11:56:02 +0800 Subject: [PATCH 014/146] prettier --- src/components/ProcessMoneyReportHoldMenu.tsx | 5 +---- src/languages/en.ts | 6 ++++-- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/components/ProcessMoneyReportHoldMenu.tsx b/src/components/ProcessMoneyReportHoldMenu.tsx index 8986083ee3c6..9f3839fe2a13 100644 --- a/src/components/ProcessMoneyReportHoldMenu.tsx +++ b/src/components/ProcessMoneyReportHoldMenu.tsx @@ -76,10 +76,7 @@ function ProcessMoneyReportHoldMenu({ if (nonHeldAmount) { return translate(isApprove ? 'iou.confirmApprovalAmount' : 'iou.confirmPayAmount'); } else { - return translate( - isApprove ? 'iou.confirmApprovalAllHoldAmount' : 'iou.confirmPayAllHoldAmount', - {transactionCount} - ); + return translate(isApprove ? 'iou.confirmApprovalAllHoldAmount' : 'iou.confirmPayAllHoldAmount', {transactionCount}); } }, [nonHeldAmount, transactionCount, translate, isApprove]); diff --git a/src/languages/en.ts b/src/languages/en.ts index 3b2ac5c46d8d..0a47ac58bcbd 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -767,10 +767,12 @@ export default { reviewDuplicates: 'Review duplicates', confirmApprove: 'Confirm approval amount', confirmApprovalAmount: "Approve what's not on hold, or approve the entire report.", - confirmApprovalAllHoldAmount: ({transactionCount}: ConfirmHoldExpenseParams) => `${Str.pluralize('This expense is', 'These expenses are', transactionCount)} on hold. Do you want to approve anyway?`, + confirmApprovalAllHoldAmount: ({transactionCount}: ConfirmHoldExpenseParams) => + `${Str.pluralize('This expense is', 'These expenses are', transactionCount)} on hold. Do you want to approve anyway?`, confirmPay: 'Confirm payment amount', confirmPayAmount: "Pay what's not on hold, or pay all out-of-pocket spend.", - confirmPayAllHoldAmount: ({transactionCount}: ConfirmHoldExpenseParams) => `${Str.pluralize('This expense is', 'These expenses are', transactionCount)} on hold. Do you want to pay anyway?`, + confirmPayAllHoldAmount: ({transactionCount}: ConfirmHoldExpenseParams) => + `${Str.pluralize('This expense is', 'These expenses are', transactionCount)} on hold. Do you want to pay anyway?`, payOnly: 'Pay only', approveOnly: 'Approve only', holdEducationalTitle: 'This expense is on', From 0cc82d7849b171ed04d5126aec2f2ee520a2c588 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Sat, 15 Jun 2024 12:25:33 +0800 Subject: [PATCH 015/146] lint --- src/components/ProcessMoneyReportHoldMenu.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/ProcessMoneyReportHoldMenu.tsx b/src/components/ProcessMoneyReportHoldMenu.tsx index 9f3839fe2a13..872464d8a5b0 100644 --- a/src/components/ProcessMoneyReportHoldMenu.tsx +++ b/src/components/ProcessMoneyReportHoldMenu.tsx @@ -75,9 +75,8 @@ function ProcessMoneyReportHoldMenu({ const promptText = useMemo(() => { if (nonHeldAmount) { return translate(isApprove ? 'iou.confirmApprovalAmount' : 'iou.confirmPayAmount'); - } else { - return translate(isApprove ? 'iou.confirmApprovalAllHoldAmount' : 'iou.confirmPayAllHoldAmount', {transactionCount}); } + return translate(isApprove ? 'iou.confirmApprovalAllHoldAmount' : 'iou.confirmPayAllHoldAmount', {transactionCount}); }, [nonHeldAmount, transactionCount, translate, isApprove]); return ( From 4fa559f121748d35ac88aae7646ac7f72166b57c Mon Sep 17 00:00:00 2001 From: Yauheni Date: Sat, 15 Jun 2024 08:50:31 +0200 Subject: [PATCH 016/146] Fix bug with animation for carousel x2 --- src/components/MultiGestureCanvas/usePanGesture.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/components/MultiGestureCanvas/usePanGesture.ts b/src/components/MultiGestureCanvas/usePanGesture.ts index c236393027ef..08d0d94d64d6 100644 --- a/src/components/MultiGestureCanvas/usePanGesture.ts +++ b/src/components/MultiGestureCanvas/usePanGesture.ts @@ -3,6 +3,7 @@ import {Dimensions} from 'react-native'; import type {PanGesture} from 'react-native-gesture-handler'; import {Gesture} from 'react-native-gesture-handler'; import {runOnJS, useDerivedValue, useSharedValue, useWorkletCallback, withDecay, withSpring} from 'react-native-reanimated'; +import * as Browser from '@libs/Browser'; import {SPRING_CONFIG} from './constants'; import type {MultiGestureCanvasVariables} from './types'; import * as MultiGestureCanvasUtils from './utils'; @@ -206,6 +207,10 @@ const usePanGesture = ({ panVelocityX.value = evt.velocityX; panVelocityY.value = evt.velocityY; + if (!isSwipingDownToClose.value && !Browser.isMobile()) { + panTranslateX.value += evt.changeX; + } + if (enableSwipeDownToClose.value || isSwipingDownToClose.value) { panTranslateY.value += evt.changeY; } From d133a3dee507affa61811186f9303a9350baa418 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Tue, 18 Jun 2024 12:25:12 +0800 Subject: [PATCH 017/146] update copy --- src/languages/en.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index b24c8b244b4d..293df4aa294c 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -770,7 +770,7 @@ export default { confirmApprovalAllHoldAmount: ({transactionCount}: ConfirmHoldExpenseParams) => `${Str.pluralize('This expense is', 'These expenses are', transactionCount)} on hold. Do you want to approve anyway?`, confirmPay: 'Confirm payment amount', - confirmPayAmount: "Pay what's not on hold, or pay all out-of-pocket spend.", + confirmPayAmount: "Pay what's not on hold, or pay the entire report.", confirmPayAllHoldAmount: ({transactionCount}: ConfirmHoldExpenseParams) => `${Str.pluralize('This expense is', 'These expenses are', transactionCount)} on hold. Do you want to pay anyway?`, payOnly: 'Pay only', From e30384394140cb717c20d039f66a6defac747996 Mon Sep 17 00:00:00 2001 From: Yauheni Date: Thu, 20 Jun 2024 12:02:03 +0200 Subject: [PATCH 018/146] Update maxDelay for doubleTapGesture for mobile browsers --- src/components/MultiGestureCanvas/useTapGestures.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/MultiGestureCanvas/useTapGestures.ts b/src/components/MultiGestureCanvas/useTapGestures.ts index f550e93d6be2..9036ae7ae39a 100644 --- a/src/components/MultiGestureCanvas/useTapGestures.ts +++ b/src/components/MultiGestureCanvas/useTapGestures.ts @@ -3,6 +3,7 @@ import {useMemo} from 'react'; import type {TapGesture} from 'react-native-gesture-handler'; import {Gesture} from 'react-native-gesture-handler'; import {runOnJS, useWorkletCallback, withSpring} from 'react-native-reanimated'; +import * as Browser from '@libs/Browser'; import {DOUBLE_TAP_SCALE, SPRING_CONFIG} from './constants'; import type {MultiGestureCanvasVariables} from './types'; import * as MultiGestureCanvasUtils from './utils'; @@ -129,7 +130,7 @@ const useTapGestures = ({ state.fail(); }) .numberOfTaps(2) - .maxDelay(150) + .maxDelay(Browser.isMobile() ? 300 : 150) .maxDistance(20) .onEnd((evt) => { const triggerScaleChangedEvent = () => { From ee0f65bce82043d41855d65b58e6385f2b68d15c Mon Sep 17 00:00:00 2001 From: cdOut <88325488+cdOut@users.noreply.github.com> Date: Thu, 20 Jun 2024 16:28:02 +0200 Subject: [PATCH 019/146] add trip receipt case --- src/components/EReceiptThumbnail.tsx | 12 ++++++++- .../ReportActionItemImage.tsx | 12 +++++++++ src/libs/TripReservationUtils.ts | 25 ++++++++++++++++++- 3 files changed, 47 insertions(+), 2 deletions(-) diff --git a/src/components/EReceiptThumbnail.tsx b/src/components/EReceiptThumbnail.tsx index f4216dcc9f8a..6762538b3c91 100644 --- a/src/components/EReceiptThumbnail.tsx +++ b/src/components/EReceiptThumbnail.tsx @@ -9,6 +9,7 @@ import colors from '@styles/theme/colors'; import variables from '@styles/variables'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import * as TripReservationUtils from '@libs/TripReservationUtils'; import type {Transaction} from '@src/types/onyx'; import Icon from './Icon'; import * as eReceiptBGs from './Icon/EReceiptBGs'; @@ -56,7 +57,8 @@ const backgroundImages = { function EReceiptThumbnail({transaction, borderRadius, fileExtension, isReceiptThumbnail = false, centerIconV = true, iconSize = 'large'}: EReceiptThumbnailProps) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); - const colorCode = isReceiptThumbnail ? StyleUtils.getFileExtensionColorCode(fileExtension) : StyleUtils.getEReceiptColorCode(transaction); + const {tripIcon, tripBGColor} = TripReservationUtils.getTripEReceiptData(transaction); + const colorCode = tripBGColor ?? (isReceiptThumbnail ? StyleUtils.getFileExtensionColorCode(fileExtension) : StyleUtils.getEReceiptColorCode(transaction)); const backgroundImage = useMemo(() => backgroundImages[colorCode], [colorCode]); @@ -141,6 +143,14 @@ function EReceiptThumbnail({transaction, borderRadius, fileExtension, isReceiptT fill={primaryColor} /> ) : null} + {tripIcon && isReceiptThumbnail ? ( + + ) : null} diff --git a/src/components/ReportActionItem/ReportActionItemImage.tsx b/src/components/ReportActionItem/ReportActionItemImage.tsx index 1251be83994b..b0105039bc18 100644 --- a/src/components/ReportActionItem/ReportActionItemImage.tsx +++ b/src/components/ReportActionItem/ReportActionItemImage.tsx @@ -95,6 +95,8 @@ function ReportActionItemImage({ const thumbnailSource = tryResolveUrlFromApiRoot(thumbnail ?? ''); const isEReceipt = transaction && TransactionUtils.hasEReceipt(transaction); + const shouldUseTripEReceiptThumbnail = transaction?.receipt?.reservationList?.length !== 0; + let propsObj: ReceiptImageProps; if (isEReceipt) { @@ -110,6 +112,16 @@ function ReportActionItemImage({ }; } else if (isLocalFile && filename && Str.isPDF(filename) && typeof attachmentModalSource === 'string') { propsObj = {isPDFThumbnail: true, source: attachmentModalSource}; + } else if (shouldUseTripEReceiptThumbnail) { + propsObj = { + isThumbnail, + transactionID: transaction?.transactionID, + ...(isThumbnail && {iconSize: (isSingleImage ? 'medium' : 'small') as IconSize, fileExtension}), + shouldUseThumbnailImage: true, + isAuthTokenRequired: false, + source: thumbnail ?? image ?? '', + shouldUseInitialObjectPosition: isDistanceRequest, + }; } else { propsObj = { isThumbnail, diff --git a/src/libs/TripReservationUtils.ts b/src/libs/TripReservationUtils.ts index ead786b8eafd..ced50c7ec0ac 100644 --- a/src/libs/TripReservationUtils.ts +++ b/src/libs/TripReservationUtils.ts @@ -3,6 +3,7 @@ import CONST from '@src/CONST'; import type {Reservation, ReservationType} from '@src/types/onyx/Transaction'; import type Transaction from '@src/types/onyx/Transaction'; import type IconAsset from '@src/types/utils/IconAsset'; +import { EReceiptColorName } from '@styles/utils/types'; function getTripReservationIcon(reservationType: ReservationType): IconAsset { switch (reservationType) { @@ -24,4 +25,26 @@ function getReservationsFromTripTransactions(transactions: Transaction[]): Reser .flat(); } -export {getTripReservationIcon, getReservationsFromTripTransactions}; +type TripEReceiptData = { + /** Icon asset associated with the type of trip reservation */ + tripIcon?: IconAsset, + + /** EReceipt background color associated with the type of trip reservation */ + tripBGColor?: EReceiptColorName, +} + +function getTripEReceiptData(transaction?: Transaction): TripEReceiptData { + const reservationType = transaction ? transaction.receipt?.reservationList?.[0]?.type : ''; + + switch (reservationType) { + case CONST.RESERVATION_TYPE.FLIGHT: + case CONST.RESERVATION_TYPE.CAR: + return {tripIcon: Expensicons.Plane, tripBGColor: CONST.ERECEIPT_COLORS.PINK}; + case CONST.RESERVATION_TYPE.HOTEL: + return {tripIcon: Expensicons.Plane, tripBGColor: CONST.ERECEIPT_COLORS.YELLOW}; + default: + return {}; + } +} + +export {getTripReservationIcon, getReservationsFromTripTransactions, getTripEReceiptData}; From 4e34eb15f41366c75698a3eb0567ae508d686bcd Mon Sep 17 00:00:00 2001 From: cdOut <88325488+cdOut@users.noreply.github.com> Date: Thu, 20 Jun 2024 16:29:11 +0200 Subject: [PATCH 020/146] fix prettier --- src/components/EReceiptThumbnail.tsx | 2 +- src/libs/TripReservationUtils.ts | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/EReceiptThumbnail.tsx b/src/components/EReceiptThumbnail.tsx index 6762538b3c91..aebfff1f5a00 100644 --- a/src/components/EReceiptThumbnail.tsx +++ b/src/components/EReceiptThumbnail.tsx @@ -5,11 +5,11 @@ import {withOnyx} from 'react-native-onyx'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; import * as ReportUtils from '@libs/ReportUtils'; +import * as TripReservationUtils from '@libs/TripReservationUtils'; import colors from '@styles/theme/colors'; import variables from '@styles/variables'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import * as TripReservationUtils from '@libs/TripReservationUtils'; import type {Transaction} from '@src/types/onyx'; import Icon from './Icon'; import * as eReceiptBGs from './Icon/EReceiptBGs'; diff --git a/src/libs/TripReservationUtils.ts b/src/libs/TripReservationUtils.ts index ced50c7ec0ac..23a1743d65f4 100644 --- a/src/libs/TripReservationUtils.ts +++ b/src/libs/TripReservationUtils.ts @@ -1,9 +1,9 @@ +import {EReceiptColorName} from '@styles/utils/types'; import * as Expensicons from '@src/components/Icon/Expensicons'; import CONST from '@src/CONST'; import type {Reservation, ReservationType} from '@src/types/onyx/Transaction'; import type Transaction from '@src/types/onyx/Transaction'; import type IconAsset from '@src/types/utils/IconAsset'; -import { EReceiptColorName } from '@styles/utils/types'; function getTripReservationIcon(reservationType: ReservationType): IconAsset { switch (reservationType) { @@ -27,11 +27,11 @@ function getReservationsFromTripTransactions(transactions: Transaction[]): Reser type TripEReceiptData = { /** Icon asset associated with the type of trip reservation */ - tripIcon?: IconAsset, + tripIcon?: IconAsset; /** EReceipt background color associated with the type of trip reservation */ - tripBGColor?: EReceiptColorName, -} + tripBGColor?: EReceiptColorName; +}; function getTripEReceiptData(transaction?: Transaction): TripEReceiptData { const reservationType = transaction ? transaction.receipt?.reservationList?.[0]?.type : ''; From 44a368f7cd09575073433837bee635a0dd2cf833 Mon Sep 17 00:00:00 2001 From: cdOut <88325488+cdOut@users.noreply.github.com> Date: Thu, 20 Jun 2024 17:04:47 +0200 Subject: [PATCH 021/146] correct type imports and lint --- src/libs/TripReservationUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/TripReservationUtils.ts b/src/libs/TripReservationUtils.ts index 23a1743d65f4..140347a99ed7 100644 --- a/src/libs/TripReservationUtils.ts +++ b/src/libs/TripReservationUtils.ts @@ -1,4 +1,4 @@ -import {EReceiptColorName} from '@styles/utils/types'; +import type {EReceiptColorName} from '@styles/utils/types'; import * as Expensicons from '@src/components/Icon/Expensicons'; import CONST from '@src/CONST'; import type {Reservation, ReservationType} from '@src/types/onyx/Transaction'; From 85f4941b0c71952a64e352ec9fbe60a1ad1419d2 Mon Sep 17 00:00:00 2001 From: cdOut <88325488+cdOut@users.noreply.github.com> Date: Thu, 20 Jun 2024 17:05:53 +0200 Subject: [PATCH 022/146] set correct icon for accommodation --- src/libs/TripReservationUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/TripReservationUtils.ts b/src/libs/TripReservationUtils.ts index 140347a99ed7..e937979ae7b9 100644 --- a/src/libs/TripReservationUtils.ts +++ b/src/libs/TripReservationUtils.ts @@ -41,7 +41,7 @@ function getTripEReceiptData(transaction?: Transaction): TripEReceiptData { case CONST.RESERVATION_TYPE.CAR: return {tripIcon: Expensicons.Plane, tripBGColor: CONST.ERECEIPT_COLORS.PINK}; case CONST.RESERVATION_TYPE.HOTEL: - return {tripIcon: Expensicons.Plane, tripBGColor: CONST.ERECEIPT_COLORS.YELLOW}; + return {tripIcon: Expensicons.Bed, tripBGColor: CONST.ERECEIPT_COLORS.YELLOW}; default: return {}; } From c42fcf555926da318d6a1903a07c6a3b63e3f2f5 Mon Sep 17 00:00:00 2001 From: Michal Muzyk Date: Fri, 21 Jun 2024 07:23:25 +0200 Subject: [PATCH 023/146] feat: integrate retry billing button --- src/ONYXKEYS.ts | 8 +++ src/languages/en.ts | 2 + src/languages/es.ts | 2 + src/libs/API/types.ts | 1 + .../AppNavigator/getPartialStateDiff.ts | 4 +- src/libs/actions/Subscription.ts | 53 ++++++++++++++++++- .../Subscription/CardSection/CardSection.tsx | 2 + 7 files changed, 69 insertions(+), 3 deletions(-) diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 8a20032b4f91..ca98e9ebbe0a 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -157,6 +157,12 @@ const ONYXKEYS = { /** Store the state of the subscription */ NVP_PRIVATE_SUBSCRIPTION: 'nvp_private_subscription', + /** Store retry billing successful status */ + SUBSCRIPTION_RETRY_BILLING_STATUS_SUCCESSFUL: 'subscriptionRetryBillingStatusSuccessful', + + /** Store retry billing failed status */ + SUBSCRIPTION_RETRY_BILLING_STATUS_FAILED: 'subscriptionRetryBillingStatusFailed', + /** Store preferred skintone for emoji */ PREFERRED_EMOJI_SKIN_TONE: 'nvp_expensify_preferredEmojiSkinTone', @@ -672,6 +678,8 @@ type OnyxValuesMapping = { [ONYXKEYS.NVP_DISMISSED_REFERRAL_BANNERS]: OnyxTypes.DismissedReferralBanners; [ONYXKEYS.NVP_HAS_SEEN_TRACK_TRAINING]: boolean; [ONYXKEYS.NVP_PRIVATE_SUBSCRIPTION]: OnyxTypes.PrivateSubscription; + [ONYXKEYS.SUBSCRIPTION_RETRY_BILLING_STATUS_SUCCESSFUL]: boolean; + [ONYXKEYS.SUBSCRIPTION_RETRY_BILLING_STATUS_FAILED]: boolean; [ONYXKEYS.USER_WALLET]: OnyxTypes.UserWallet; [ONYXKEYS.WALLET_ONFIDO]: OnyxTypes.WalletOnfido; [ONYXKEYS.WALLET_ADDITIONAL_DETAILS]: OnyxTypes.WalletAdditionalDetails; diff --git a/src/languages/en.ts b/src/languages/en.ts index 99ed3265f02b..af7516af6dcc 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -3234,6 +3234,8 @@ export default { changeCurrency: 'Change payment currency', cardNotFound: 'No payment card added', retryPaymentButton: 'Retry payment', + success: 'Success!', + yourCardHasBeenBilled: 'Your card has been billed successfully.', }, yourPlan: { title: 'Your plan', diff --git a/src/languages/es.ts b/src/languages/es.ts index 96346458af37..87a5e3b182dc 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -3738,6 +3738,8 @@ export default { changeCurrency: 'Cambiar moneda de pago', cardNotFound: 'No se ha añadido ninguna tarjeta de pago', retryPaymentButton: 'Reintentar el pago', + success: 'Éxito!', + yourCardHasBeenBilled: 'Tu tarjeta fue facturada correctamente.', }, yourPlan: { title: 'Tu plan', diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index 8f093ee827c3..280f1c58306a 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -227,6 +227,7 @@ const WRITE_COMMANDS = { UPDATE_SUBSCRIPTION_AUTO_RENEW: 'UpdateSubscriptionAutoRenew', UPDATE_SUBSCRIPTION_ADD_NEW_USERS_AUTOMATICALLY: 'UpdateSubscriptionAddNewUsersAutomatically', UPDATE_SUBSCRIPTION_SIZE: 'UpdateSubscriptionSize', + CLEAR_OUTSTANDING_BALANCE: 'ClearOutstandingBalance', } as const; type WriteCommand = ValueOf; diff --git a/src/libs/Navigation/AppNavigator/getPartialStateDiff.ts b/src/libs/Navigation/AppNavigator/getPartialStateDiff.ts index 5061c7500742..17a8ee158219 100644 --- a/src/libs/Navigation/AppNavigator/getPartialStateDiff.ts +++ b/src/libs/Navigation/AppNavigator/getPartialStateDiff.ts @@ -72,8 +72,8 @@ function getPartialStateDiff(state: State, templateState: St (!stateTopmostFullScreen && templateStateTopmostFullScreen) || (stateTopmostFullScreen && templateStateTopmostFullScreen && - stateTopmostFullScreen.name !== templateStateTopmostFullScreen.name && - !shallowCompare(stateTopmostFullScreen.params as Record | undefined, templateStateTopmostFullScreen.params as Record | undefined)) + (stateTopmostFullScreen.name !== templateStateTopmostFullScreen.name || + !shallowCompare(stateTopmostFullScreen.params as Record | undefined, templateStateTopmostFullScreen.params as Record | undefined))) ) { diff[NAVIGATORS.FULL_SCREEN_NAVIGATOR] = fullScreenDiff; } diff --git a/src/libs/actions/Subscription.ts b/src/libs/actions/Subscription.ts index 19a3bf0c547e..46d71e9f3b81 100644 --- a/src/libs/actions/Subscription.ts +++ b/src/libs/actions/Subscription.ts @@ -231,4 +231,55 @@ function clearUpdateSubscriptionSizeError() { }); } -export {openSubscriptionPage, updateSubscriptionAutoRenew, updateSubscriptionAddNewUsersAutomatically, updateSubscriptionSize, clearUpdateSubscriptionSizeError, updateSubscriptionType}; +function clearOutstandingBalance() { + const onyxData: OnyxData = { + optimisticData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.SUBSCRIPTION_RETRY_BILLING_STATUS_SUCCESSFUL, + value: true, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.SUBSCRIPTION_RETRY_BILLING_STATUS_FAILED, + value: false, + }, + ], + successData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.SUBSCRIPTION_RETRY_BILLING_STATUS_SUCCESSFUL, + value: true, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.SUBSCRIPTION_RETRY_BILLING_STATUS_FAILED, + value: false, + }, + ], + failureData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.SUBSCRIPTION_RETRY_BILLING_STATUS_SUCCESSFUL, + value: false, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.SUBSCRIPTION_RETRY_BILLING_STATUS_SUCCESSFUL, + value: true, + }, + ], + }; + + API.write(WRITE_COMMANDS.CLEAR_OUTSTANDING_BALANCE, null, onyxData); +} + +export { + openSubscriptionPage, + updateSubscriptionAutoRenew, + updateSubscriptionAddNewUsersAutomatically, + updateSubscriptionSize, + clearUpdateSubscriptionSizeError, + updateSubscriptionType, + clearOutstandingBalance, +}; diff --git a/src/pages/settings/Subscription/CardSection/CardSection.tsx b/src/pages/settings/Subscription/CardSection/CardSection.tsx index 7f80b189c517..3103b85363d5 100644 --- a/src/pages/settings/Subscription/CardSection/CardSection.tsx +++ b/src/pages/settings/Subscription/CardSection/CardSection.tsx @@ -20,6 +20,8 @@ function CardSection() { const styles = useThemeStyles(); const theme = useTheme(); const [fundList] = useOnyx(ONYXKEYS.FUND_LIST); + const [retryBillingStatusSuccessful] = useOnyx(ONYXKEYS.SUBSCRIPTION_RETRY_BILLING_STATUS_SUCCESSFUL, {initWithStoredValues: false}); + const [retryBillingStatusFailed] = useOnyx(ONYXKEYS.SUBSCRIPTION_RETRY_BILLING_STATUS_FAILED); const defaultCard = useMemo(() => Object.values(fundList ?? {}).find((card) => card.isDefault), [fundList]); From 68fcbcd5fdc4d31e45b6da66c829dfed50cb5b0a Mon Sep 17 00:00:00 2001 From: Michal Muzyk Date: Fri, 21 Jun 2024 13:42:08 +0200 Subject: [PATCH 024/146] before rebase --- src/ONYXKEYS.ts | 6 ----- src/languages/en.ts | 2 -- src/languages/es.ts | 2 -- src/libs/SubscriptionUtils.ts | 1 + src/libs/actions/Subscription.ts | 17 +++++--------- .../CardSection/BillingBanner.tsx | 20 +++++++++++++++++ .../Subscription/CardSection/CardSection.tsx | 22 +++++++++++++++---- .../Subscription/CardSection/utils.ts | 7 +++--- 8 files changed, 48 insertions(+), 29 deletions(-) diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 0eff19d471ef..da6f58cc705c 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -169,12 +169,6 @@ const ONYXKEYS = { /** Store the billing status */ NVP_PRIVATE_BILLING_STATUS: 'nvp_private_billingStatus', - /** Store retry billing successful status */ - SUBSCRIPTION_RETRY_BILLING_STATUS_SUCCESSFUL: 'subscriptionRetryBillingStatusSuccessful', - - /** Store retry billing failed status */ - SUBSCRIPTION_RETRY_BILLING_STATUS_FAILED: 'subscriptionRetryBillingStatusFailed', - /** Store preferred skintone for emoji */ PREFERRED_EMOJI_SKIN_TONE: 'nvp_expensify_preferredEmojiSkinTone', diff --git a/src/languages/en.ts b/src/languages/en.ts index edf83a1725f7..ad1509f1ac85 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -3248,8 +3248,6 @@ export default { changeCurrency: 'Change payment currency', cardNotFound: 'No payment card added', retryPaymentButton: 'Retry payment', - success: 'Success!', - yourCardHasBeenBilled: 'Your card has been billed successfully.', }, yourPlan: { title: 'Your plan', diff --git a/src/languages/es.ts b/src/languages/es.ts index ca00913d573f..eb828882e192 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -3753,8 +3753,6 @@ export default { changeCurrency: 'Cambiar moneda de pago', cardNotFound: 'No se ha añadido ninguna tarjeta de pago', retryPaymentButton: 'Reintentar el pago', - success: 'Éxito!', - yourCardHasBeenBilled: 'Tu tarjeta fue facturada correctamente.', }, yourPlan: { title: 'Tu plan', diff --git a/src/libs/SubscriptionUtils.ts b/src/libs/SubscriptionUtils.ts index 988c83354efb..c924332dca7b 100644 --- a/src/libs/SubscriptionUtils.ts +++ b/src/libs/SubscriptionUtils.ts @@ -108,6 +108,7 @@ Onyx.connect({ let billingStatusSuccessful: OnyxValues[typeof ONYXKEYS.SUBSCRIPTION_RETRY_BILLING_STATUS_SUCCESSFUL]; Onyx.connect({ key: ONYXKEYS.SUBSCRIPTION_RETRY_BILLING_STATUS_SUCCESSFUL, + initWithStoredValues: false, callback: (value) => { if (value === undefined) { return; diff --git a/src/libs/actions/Subscription.ts b/src/libs/actions/Subscription.ts index 46d71e9f3b81..558e4c0284a8 100644 --- a/src/libs/actions/Subscription.ts +++ b/src/libs/actions/Subscription.ts @@ -233,18 +233,6 @@ function clearUpdateSubscriptionSizeError() { function clearOutstandingBalance() { const onyxData: OnyxData = { - optimisticData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.SUBSCRIPTION_RETRY_BILLING_STATUS_SUCCESSFUL, - value: true, - }, - { - onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.SUBSCRIPTION_RETRY_BILLING_STATUS_FAILED, - value: false, - }, - ], successData: [ { onyxMethod: Onyx.METHOD.MERGE, @@ -274,6 +262,10 @@ function clearOutstandingBalance() { API.write(WRITE_COMMANDS.CLEAR_OUTSTANDING_BALANCE, null, onyxData); } +function resetRetryBillingStatus() { + Onyx.merge(ONYXKEYS.SUBSCRIPTION_RETRY_BILLING_STATUS_SUCCESSFUL, false); +} + export { openSubscriptionPage, updateSubscriptionAutoRenew, @@ -282,4 +274,5 @@ export { clearUpdateSubscriptionSizeError, updateSubscriptionType, clearOutstandingBalance, + resetRetryBillingStatus, }; diff --git a/src/pages/settings/Subscription/CardSection/BillingBanner.tsx b/src/pages/settings/Subscription/CardSection/BillingBanner.tsx index b3e4d8859249..8ea459b12aad 100644 --- a/src/pages/settings/Subscription/CardSection/BillingBanner.tsx +++ b/src/pages/settings/Subscription/CardSection/BillingBanner.tsx @@ -3,10 +3,14 @@ import {View} from 'react-native'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; import * as Illustrations from '@components/Icon/Illustrations'; +import {PressableWithoutFeedback} from '@components/Pressable'; import Text from '@components/Text'; +import useLocalize from '@hooks/useLocalize'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import variables from '@styles/variables'; +import * as Subscription from '@userActions/Subscription'; +import CONST from '@src/CONST'; type BillingBannerProps = { title?: string; @@ -20,6 +24,7 @@ type BillingBannerProps = { function BillingBanner({title, subtitle, isError, shouldShowRedDotIndicator, shouldShowGreenDotIndicator, isTrialActive}: BillingBannerProps) { const styles = useThemeStyles(); const theme = useTheme(); + const {translate} = useLocalize(); const backgroundStyle = isTrialActive ? styles.trialBannerBackgroundColor : styles.hoveredComponentBG; @@ -48,6 +53,21 @@ function BillingBanner({title, subtitle, isError, shouldShowRedDotIndicator, sho fill={theme.success} /> )} + {!isError && ( + { + Subscription.resetRetryBillingStatus(); + }} + style={[styles.touchableButtonImage]} + role={CONST.ROLE.BUTTON} + accessibilityLabel={translate('common.close')} + > + + + )} ); } diff --git a/src/pages/settings/Subscription/CardSection/CardSection.tsx b/src/pages/settings/Subscription/CardSection/CardSection.tsx index 5817e59ad60d..ad17ccddf369 100644 --- a/src/pages/settings/Subscription/CardSection/CardSection.tsx +++ b/src/pages/settings/Subscription/CardSection/CardSection.tsx @@ -1,13 +1,16 @@ import React, {useMemo} from 'react'; import {View} from 'react-native'; +import Button from '@components/Button'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; import Section from '@components/Section'; import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; +import useNetwork from '@hooks/useNetwork'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import DateUtils from '@libs/DateUtils'; +import * as Subscription from '@userActions/Subscription'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import BillingBanner from './BillingBanner'; import CardSectionActions from './CardSectionActions'; @@ -18,14 +21,13 @@ function CardSection() { const {translate, preferredLocale} = useLocalize(); const styles = useThemeStyles(); const theme = useTheme(); - const [retryBillingStatusSuccessful] = useOnyx(ONYXKEYS.SUBSCRIPTION_RETRY_BILLING_STATUS_SUCCESSFUL, {initWithStoredValues: false}); - const [retryBillingStatusFailed] = useOnyx(ONYXKEYS.SUBSCRIPTION_RETRY_BILLING_STATUS_FAILED); + const {isOffline} = useNetwork(); const defaultCard = CardSectionUtils.getCardForSubscriptionBilling(); const cardMonth = useMemo(() => DateUtils.getMonthNames(preferredLocale)[(defaultCard?.accountData?.cardMonth ?? 1) - 1], [defaultCard?.accountData?.cardMonth, preferredLocale]); - const {title, subtitle, isError, shouldShowRedDotIndicator, shouldShowGreenDotIndicator} = CardSectionUtils.getBillingStatus( + const {title, subtitle, isError, shouldShowRedDotIndicator, shouldShowGreenDotIndicator, isRetryAvailable} = CardSectionUtils.getBillingStatus( translate, preferredLocale, defaultCard?.accountData?.cardNumber ?? '', @@ -76,7 +78,19 @@ function CardSection() { )} - {isEmptyObject(defaultCard?.accountData) && } + {!isEmptyObject(defaultCard?.accountData) && } + {!isRetryAvailable && ( +