-
Notifications
You must be signed in to change notification settings - Fork 3.7k
feat: Add thumbnail support for receipts and improve image handling #84919
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
Merged
Julesssss
merged 24 commits into
Expensify:main
from
callstack-internal:pregenerate-thumbnails-for-the-confirm-screen
Apr 7, 2026
Merged
Changes from all commits
Commits
Show all changes
24 commits
Select commit
Hold shift + click to select a range
d6fa27a
feat: Add thumbnail support for receipts and improve image handling
kubabutkiewicz 35be2b4
Merge branch 'main' of github.com:callstack-internal/Expensify-App in…
kubabutkiewicz 23bb98f
Refactor IOURequestStepScan to generate thumbnails off the critical p…
kubabutkiewicz 6c74437
resolve lint issue
kubabutkiewicz 90397b1
Merge branch 'main' of github.com:callstack-internal/Expensify-App in…
kubabutkiewicz af737d4
Merge branch 'main' of github.com:callstack-internal/Expensify-App in…
kubabutkiewicz ddff150
feat: Add local receipt thumbnail generation for improved image handl…
kubabutkiewicz 8b0918c
Merge branch 'main' of github.com:callstack-internal/Expensify-App in…
kubabutkiewicz f4cf69d
refactor: Enhance local receipt thumbnail handling in MoneyRequestCon…
kubabutkiewicz ecc9d6c
resolve comments
kubabutkiewicz 336ecb9
Merge branch 'main' of github.com:callstack-internal/Expensify-App in…
kubabutkiewicz 05b4b40
fix lint
kubabutkiewicz fd0e6fe
Enhance thumbnail generation logic and update logging import. Added s…
kubabutkiewicz 946ddbb
Merge branch 'main' of github.com:callstack-internal/Expensify-App in…
kubabutkiewicz f8cac08
Fix: Update style property to use absoluteFill for camera overlay in …
kubabutkiewicz a3e143e
Merge branch 'main' of github.com:callstack-internal/Expensify-App in…
kubabutkiewicz cbfb4f1
Merge branch 'main' of github.com:callstack-internal/Expensify-App in…
kubabutkiewicz 8e8bcf0
Implement thumbnail pre-generation for improved performance in confir…
kubabutkiewicz f24b70f
Merge branch 'main' of github.com:callstack-internal/Expensify-App in…
kubabutkiewicz 5beb1fa
Enhance thumbnail generation for confirmation screen by reducing max …
kubabutkiewicz 6acd919
Merge branch 'main' of github.com:callstack-internal/Expensify-App in…
kubabutkiewicz 3b1ef08
Merge branch 'main' of github.com:callstack-internal/Expensify-App in…
kubabutkiewicz 49d3c60
Merge branch 'main' of github.com:callstack-internal/Expensify-App in…
kubabutkiewicz 512c481
fix lint
kubabutkiewicz File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -873,6 +873,7 @@ | |
| "zxldvw", | ||
| "مثال", | ||
| "Airwallex", | ||
| "deprioritizes", | ||
| "AMRO", | ||
| "Bancorporation", | ||
| "Banque", | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,113 @@ | ||
| import {useEffect, useRef, useState, useTransition} from 'react'; | ||
| import {Image} from 'react-native'; | ||
| import {generateThumbnail} from '@pages/iou/request/step/IOURequestStepScan/cropImageToAspectRatio'; | ||
|
|
||
| const thumbnailCache = new Map<string, string>(); | ||
| /** Track how many mounted hook instances reference each sourceUri */ | ||
| const thumbnailRefCount = new Map<string, number>(); | ||
|
|
||
| function retainUri(uri: string) { | ||
| thumbnailRefCount.set(uri, (thumbnailRefCount.get(uri) ?? 0) + 1); | ||
| } | ||
|
|
||
| function releaseUri(uri: string) { | ||
| const count = (thumbnailRefCount.get(uri) ?? 1) - 1; | ||
| if (count <= 0) { | ||
| thumbnailRefCount.delete(uri); | ||
| thumbnailCache.delete(uri); | ||
| } else { | ||
| thumbnailRefCount.set(uri, count); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Pre-populate the thumbnail cache so the confirm screen can use it | ||
| * synchronously on first render, avoiding any source swap / flash. | ||
| */ | ||
| function pregenerateThumbnail(sourceUri: string): Promise<string | undefined> { | ||
| if (thumbnailCache.has(sourceUri)) { | ||
| return Promise.resolve(thumbnailCache.get(sourceUri)); | ||
| } | ||
| return generateThumbnail(sourceUri).then((uri) => { | ||
| if (uri) { | ||
| thumbnailCache.set(sourceUri, uri); | ||
| // Pre-decode the thumbnail in the native image pipeline so the | ||
| // confirmation screen can display it instantly without decode latency. | ||
| Image.prefetch(uri); | ||
| } | ||
| return uri; | ||
| }); | ||
| } | ||
|
|
||
| /** | ||
| * Returns a cached low-resolution thumbnail for a local receipt image. | ||
| * The thumbnail should be pre-generated via `pregenerateThumbnail` before | ||
| * navigating to the confirm screen. If it wasn't, this hook generates it | ||
| * as a fallback, but in that case a source swap (flash) may occur. | ||
| */ | ||
| function useLocalReceiptThumbnail(sourceUri: string | undefined, isLocalFile: boolean): {thumbnailUri: string | undefined; isGenerating: boolean} { | ||
| const [asyncResult, setAsyncResult] = useState<{source: string; uri?: string; done: boolean} | undefined>(); | ||
| const [, startTransition] = useTransition(); | ||
| const retainedUriRef = useRef<string | undefined>(undefined); | ||
|
|
||
| // Resolve cached thumbnails synchronously during render (fast path) | ||
| const cachedUri = sourceUri ? thumbnailCache.get(sourceUri) : undefined; | ||
| const resultForCurrentSource = asyncResult?.source === sourceUri ? asyncResult : undefined; | ||
| const thumbnailUri = cachedUri ?? resultForCurrentSource?.uri; | ||
|
|
||
| const shouldGenerate = !!sourceUri && isLocalFile && !cachedUri; | ||
| const isGenerating = shouldGenerate && !resultForCurrentSource?.done; | ||
|
|
||
| // Retain / release the cache entry so it lives as long as at least one | ||
| // mounted hook instance references it, and is cleaned up after the last | ||
| // consumer unmounts. | ||
| useEffect(() => { | ||
| if (!sourceUri || !isLocalFile) { | ||
| return; | ||
| } | ||
|
|
||
| retainUri(sourceUri); | ||
| retainedUriRef.current = sourceUri; | ||
|
|
||
| return () => { | ||
| releaseUri(sourceUri); | ||
| retainedUriRef.current = undefined; | ||
| }; | ||
| }, [sourceUri, isLocalFile]); | ||
|
|
||
| // Fallback: generate if not already in cache (e.g. gallery pick path) | ||
| useEffect(() => { | ||
| if (!sourceUri || !isLocalFile || thumbnailCache.has(sourceUri)) { | ||
| return; | ||
| } | ||
|
|
||
| let cancelled = false; | ||
| generateThumbnail(sourceUri) | ||
| .then((uri) => { | ||
| if (cancelled) { | ||
| return; | ||
| } | ||
| if (uri) { | ||
| thumbnailCache.set(sourceUri, uri); | ||
| } | ||
| startTransition(() => { | ||
| setAsyncResult({source: sourceUri, uri: uri ?? undefined, done: true}); | ||
| }); | ||
| }) | ||
| .catch(() => { | ||
| if (cancelled) { | ||
| return; | ||
| } | ||
| setAsyncResult({source: sourceUri, done: true}); | ||
| }); | ||
|
|
||
| return () => { | ||
| cancelled = true; | ||
| }; | ||
| }, [sourceUri, isLocalFile, startTransition]); | ||
|
|
||
| return {thumbnailUri, isGenerating}; | ||
| } | ||
|
|
||
| export {pregenerateThumbnail}; | ||
| export default useLocalReceiptThumbnail; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.
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.
We definitely should check this on Android and iOS both. I think I saw on my crappy Galaxy S8 that the image preview on confirmation page takes about 1 second to load without any resizing.
But, also I think we can control the overall file size by tweaking the vision camera settings e.g. if we use
takeSnapshot()instead oftakePhoto()then maybe this thumbnail generation is not needed? Or the tradeoff is less? Hard to say! Hacking over here.