-
Notifications
You must be signed in to change notification settings - Fork 3.9k
Hover-to-view for receipt thumbnails v2 #68276
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
8e48daa
2420102
e13adb6
a1ab3ea
51db4cb
1d57a70
273125f
e0ac7da
60ae9bc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,8 +1,11 @@ | ||
| import {Str} from 'expensify-common'; | ||
| import React from 'react'; | ||
| import {View} from 'react-native'; | ||
| import type {ViewStyle} from 'react-native'; | ||
| import {Receipt} from '@components/Icon/Expensicons'; | ||
| import ReceiptImage from '@components/ReceiptImage'; | ||
| import ReceiptPreview from '@components/TransactionItemRow/ReceiptPreview'; | ||
| import useHover from '@hooks/useHover'; | ||
| import useStyleUtils from '@hooks/useStyleUtils'; | ||
| import useTheme from '@hooks/useTheme'; | ||
| import useThemeStyles from '@hooks/useThemeStyles'; | ||
|
|
@@ -18,13 +21,17 @@ function ReceiptCell({transactionItem, isSelected, style}: {transactionItem: Tra | |
| const styles = useThemeStyles(); | ||
| const StyleUtils = useStyleUtils(); | ||
| const backgroundStyles = isSelected ? StyleUtils.getBackgroundColorStyle(theme.buttonHoveredBG) : StyleUtils.getBackgroundColorStyle(theme.border); | ||
|
|
||
| const {hovered, bind} = useHover(); | ||
| const isEReceipt = transactionItem.hasEReceipt && !hasReceiptSource(transactionItem); | ||
| let source = transactionItem?.receipt?.source ?? ''; | ||
| let previewSource = transactionItem?.receipt?.source ?? ''; | ||
|
|
||
| if (source && typeof source === 'string') { | ||
| if (source) { | ||
| const filename = getFileName(source); | ||
| const receiptURIs = getThumbnailAndImageURIs(transactionItem, null, filename); | ||
| source = tryResolveUrlFromApiRoot(receiptURIs.thumbnail ?? receiptURIs.image ?? ''); | ||
| const previewImageURI = Str.isImage(filename) ? receiptURIs.image : receiptURIs.thumbnail; | ||
| previewSource = tryResolveUrlFromApiRoot(previewImageURI ?? ''); | ||
| } | ||
|
|
||
| return ( | ||
|
|
@@ -36,12 +43,14 @@ function ReceiptCell({transactionItem, isSelected, style}: {transactionItem: Tra | |
| backgroundStyles, | ||
| style, | ||
| ]} | ||
| onMouseEnter={bind.onMouseEnter} | ||
| onMouseLeave={bind.onMouseLeave} | ||
| > | ||
| <ReceiptImage | ||
| source={source} | ||
| isEReceipt={transactionItem.hasEReceipt && !hasReceiptSource(transactionItem)} | ||
| isEReceipt={isEReceipt} | ||
| transactionID={transactionItem.transactionID} | ||
| shouldUseThumbnailImage={!transactionItem?.receipt?.source} | ||
| shouldUseThumbnailImage | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Coming from #70651, we should handle the loading style for expenses that have receipt here. More details: #70651 (comment) |
||
| isAuthTokenRequired | ||
| fallbackIcon={Receipt} | ||
| fallbackIconSize={20} | ||
|
|
@@ -51,6 +60,13 @@ function ReceiptCell({transactionItem, isSelected, style}: {transactionItem: Tra | |
| loadingIconSize="small" | ||
| loadingIndicatorStyles={styles.bgTransparent} | ||
| transactionItem={transactionItem} | ||
| shouldUseInitialObjectPosition | ||
| /> | ||
| <ReceiptPreview | ||
| source={previewSource} | ||
| hovered={hovered} | ||
| isEReceipt={!!isEReceipt} | ||
| transactionItem={transactionItem} | ||
| /> | ||
| </View> | ||
| ); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| function ReceiptPreview() { | ||
| return null; | ||
| } | ||
|
|
||
| export default ReceiptPreview; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,168 @@ | ||
| import React, {useCallback, useEffect, useRef, useState} from 'react'; | ||
| import ReactDOM from 'react-dom'; | ||
| import type {LayoutChangeEvent} from 'react-native'; | ||
| import {ActivityIndicator, StyleSheet, View} from 'react-native'; | ||
| import Animated, {FadeIn, FadeOut} from 'react-native-reanimated'; | ||
| import DistanceEReceipt from '@components/DistanceEReceipt'; | ||
| import EReceiptWithSizeCalculation from '@components/EReceiptWithSizeCalculation'; | ||
| import type {ImageOnLoadEvent} from '@components/Image/types'; | ||
| import useDebouncedState from '@hooks/useDebouncedState'; | ||
| import useResponsiveLayout from '@hooks/useResponsiveLayout'; | ||
| import useTheme from '@hooks/useTheme'; | ||
| import useThemeStyles from '@hooks/useThemeStyles'; | ||
| import useWindowDimensions from '@hooks/useWindowDimensions'; | ||
| import {isDistanceRequest} from '@libs/TransactionUtils'; | ||
| import variables from '@styles/variables'; | ||
| import Image from '@src/components/Image'; | ||
| import CONST from '@src/CONST'; | ||
| import type {Transaction} from '@src/types/onyx'; | ||
|
|
||
| type ReceiptPreviewProps = { | ||
| /** Path to the image to be opened in the preview */ | ||
| source: string; | ||
|
|
||
| /** Whether the preview should be shown (e.g. if we are hovered over certain ReceiptCell) */ | ||
| hovered: boolean; | ||
|
|
||
| /** Is preview for an e-receipt */ | ||
| isEReceipt: boolean; | ||
|
|
||
| /** Transaction object related to the preview */ | ||
| transactionItem: Transaction; | ||
| }; | ||
|
|
||
| function ReceiptPreview({source, hovered, isEReceipt = false, transactionItem}: ReceiptPreviewProps) { | ||
| const isDistanceEReceipt = isDistanceRequest(transactionItem); | ||
| const styles = useThemeStyles(); | ||
| const theme = useTheme(); | ||
| const [eReceiptScaleFactor, setEReceiptScaleFactor] = useState(0); | ||
| const [imageAspectRatio, setImageAspectRatio] = useState<string | number | undefined>(undefined); | ||
| const [distanceEReceiptAspectRatio, setDistanceEReceiptAspectRatio] = useState<string | number | undefined>(undefined); | ||
| const [shouldShow, debounceShouldShow, setShouldShow] = useDebouncedState(false, CONST.TIMING.SHOW_HOVER_PREVIEW_DELAY); | ||
| const {shouldUseNarrowLayout} = useResponsiveLayout(); | ||
| const hasMeasured = useRef(false); | ||
| const {windowHeight} = useWindowDimensions(); | ||
| const [isLoading, setIsLoading] = useState(true); | ||
|
|
||
| const handleDistanceEReceiptLayout = (e: LayoutChangeEvent) => { | ||
| if (hasMeasured.current) { | ||
| return; | ||
| } | ||
| hasMeasured.current = true; | ||
|
|
||
| const {height, width} = e.nativeEvent.layout; | ||
| if (height === 0) { | ||
| // on the initial layout, measured height is 0, so we want to set everything on the second one | ||
| hasMeasured.current = false; | ||
| return; | ||
| } | ||
| if (height * eReceiptScaleFactor > windowHeight - CONST.RECEIPT_PREVIEW_TOP_BOTTOM_MARGIN) { | ||
| setDistanceEReceiptAspectRatio(variables.eReceiptBGHWidth / (windowHeight - CONST.RECEIPT_PREVIEW_TOP_BOTTOM_MARGIN)); | ||
| return; | ||
| } | ||
| setDistanceEReceiptAspectRatio(variables.eReceiptBGHWidth / height); | ||
| setEReceiptScaleFactor(width / variables.eReceiptBGHWidth); | ||
| }; | ||
|
|
||
| const updateImageAspectRatio = useCallback( | ||
| (width: number, height: number) => { | ||
| if (!source) { | ||
| return; | ||
| } | ||
|
|
||
| setImageAspectRatio(height ? width / height : 'auto'); | ||
| }, | ||
| [source], | ||
| ); | ||
|
|
||
| const handleLoad = useCallback( | ||
| (e: ImageOnLoadEvent) => { | ||
| const {width, height} = e.nativeEvent; | ||
| updateImageAspectRatio(width, height); | ||
| setIsLoading(false); | ||
| }, | ||
| [updateImageAspectRatio], | ||
| ); | ||
|
|
||
| const handleError = () => { | ||
| setIsLoading(false); | ||
| }; | ||
|
|
||
| useEffect(() => { | ||
| setShouldShow(hovered); | ||
| }, [hovered, setShouldShow]); | ||
|
|
||
| if (shouldUseNarrowLayout || !debounceShouldShow || !shouldShow || (!source && !isEReceipt && !isDistanceEReceipt)) { | ||
| return null; | ||
| } | ||
|
|
||
| const shouldShowImage = source && !(isEReceipt || isDistanceEReceipt); | ||
| const shouldShowDistanceEReceipt = isDistanceEReceipt && !isEReceipt; | ||
|
|
||
| return ReactDOM.createPortal( | ||
| <Animated.View | ||
| entering={FadeIn.duration(CONST.TIMING.SHOW_HOVER_PREVIEW_ANIMATION_DURATION)} | ||
| exiting={FadeOut.duration(CONST.TIMING.SHOW_HOVER_PREVIEW_ANIMATION_DURATION)} | ||
| style={[styles.receiptPreview, styles.flexColumn, styles.alignItemsCenter, styles.justifyContentStart]} | ||
| > | ||
| {shouldShowImage ? ( | ||
| <View style={[styles.w100]}> | ||
| {isLoading && ( | ||
| <View style={[StyleSheet.absoluteFillObject, styles.justifyContentCenter, styles.alignItemsCenter]}> | ||
| <ActivityIndicator | ||
| color={theme.spinner} | ||
| size="large" | ||
| /> | ||
| </View> | ||
| )} | ||
|
|
||
| <Image | ||
| source={{uri: source}} | ||
| style={[ | ||
| styles.w100, | ||
| {aspectRatio: imageAspectRatio ?? 1}, | ||
| isLoading && {opacity: 0}, // hide until loaded | ||
| ]} | ||
| onLoadStart={() => { | ||
| if (isLoading) { | ||
| return; | ||
| } | ||
| setIsLoading(true); | ||
| }} | ||
| onError={handleError} | ||
| onLoad={handleLoad} | ||
| isAuthTokenRequired | ||
| /> | ||
| </View> | ||
| ) : ( | ||
| <View style={styles.receiptPreviewEReceiptsContainer}> | ||
| {shouldShowDistanceEReceipt ? ( | ||
| <View | ||
| onLayout={handleDistanceEReceiptLayout} | ||
| style={[ | ||
| { | ||
| transformOrigin: 'center', | ||
| scale: eReceiptScaleFactor, | ||
| aspectRatio: distanceEReceiptAspectRatio, | ||
| }, | ||
| ]} | ||
| > | ||
| <DistanceEReceipt | ||
| transaction={transactionItem} | ||
| hoverPreview | ||
| /> | ||
| </View> | ||
| ) : ( | ||
| <EReceiptWithSizeCalculation | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Coming from this issue #72414 , we are handling image receipts and distance eReceipts here, so per diem was falling back to EReceiptWithSizeCalculation with the card-style view instead of using its dedicated per diem PerDiemEReceipt view. That caused the inconsistency, and we’ve fixed it here: #73911 |
||
| transactionID={transactionItem.transactionID} | ||
| transactionItem={transactionItem} | ||
| /> | ||
| )} | ||
| </View> | ||
| )} | ||
| </Animated.View>, | ||
| document.body, | ||
| ); | ||
| } | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Display name missing
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Shall I create a pr with it?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please add it to some follow up in future, no need to do it now. Also in case we need to revert it might cause more problems |
||
|
|
||
| export default ReceiptPreview; | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Coming from #69278, removing the
typeofcheck resulted in a crash when creating a distance expense. This is because we on native the source is set to a number for optimistically created expenses.