Skip to content
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

[$500] PDF receipt preview loads each time visiting 1:1 DM, report and transaction thread #37852

Closed
3 of 6 tasks
kavimuru opened this issue Mar 6, 2024 · 74 comments
Closed
3 of 6 tasks
Assignees
Labels
Bug Something is broken. Auto assigns a BugZero manager. Daily KSv2 Engineering External Added to denote the issue can be worked on by a contributor Help Wanted Apply this label when an issue is open to proposals by contributors

Comments

@kavimuru
Copy link

kavimuru commented Mar 6, 2024

If you haven’t already, check out our contributing guidelines for onboarding and email contributors@expensify.com to request to join our Slack channel!


Version Number: 1.4.48-0
Reproducible in staging?: y
Reproducible in production?: n
If this was caught during regression testing, add the test name, ID and link from TestRail:
Email or phone of affected tester (no customers):
Logs: https://stackoverflow.com/c/expensify/questions/4856
Expensify/Expensify Issue URL:
Issue reported by: Applause internal team
Slack conversation:

Action Performed:

  1. Go to staging.new.expensify.com
  2. Go offline.
  3. Go to FAB > Request money > Scan.
  4. Select a PDF receipt.
  5. Select any participant and create the Scan request.
  6. in 1:1 DM, click on the scan preview.
  7. In IOU report, click on the scan preview.
  8. Click on the header subtitle to return to report and 1:1 DM.

Expected Result:

The PDF receipt preview will not load each time going back and forth between 1:1 DM, IOU report and transaction thread.

Actual Result:

The PDF receipt preview loads each time going back and forth between 1:1 DM, IOU report and transaction thread. Sometimes it shows blue "loading page" briefly when the receipt is rendering.

Workaround:

unknown

Platforms:

Which of our officially supported platforms is this issue occurring on?

  • Android: Native
  • Android: mWeb Chrome
  • iOS: Native
  • iOS: mWeb Safari
  • MacOS: Chrome / Safari
  • MacOS: Desktop

Screenshots/Videos

Add any screenshot/video evidence

Bug6404710_1709766997050.bandicam_2024-03-07_07-10-11-388.mp4

View all open jobs on GitHub

Upwork Automation - Do Not Edit
  • Upwork Job URL: https://www.upwork.com/jobs/~016df3671b30e63c80
  • Upwork Job ID: 1806060107499225786
  • Last Price Increase: 2024-07-24
Issue OwnerCurrent Issue Owner: @
@kavimuru kavimuru added DeployBlockerCash This issue or pull request should block deployment Daily KSv2 Bug Something is broken. Auto assigns a BugZero manager. labels Mar 6, 2024
@github-actions github-actions bot added Engineering Hourly KSv2 and removed Daily KSv2 labels Mar 6, 2024
Copy link
Contributor

github-actions bot commented Mar 6, 2024

👋 Friendly reminder that deploy blockers are time-sensitive ⏱ issues! Check out the open `StagingDeployCash` deploy checklist to see the list of PRs included in this release, then work quickly to do one of the following:

  1. Identify the pull request that introduced this issue and revert it.
  2. Find someone who can quickly fix the issue.
  3. Fix the issue yourself.

Copy link

melvin-bot bot commented Mar 6, 2024

Triggered auto assignment to @anmurali (Bug), see https://stackoverflow.com/c/expensify/questions/14418 for more details.

@melvin-bot melvin-bot bot added Daily KSv2 and removed Hourly KSv2 labels Mar 6, 2024
Copy link

melvin-bot bot commented Mar 6, 2024

Triggered auto assignment to @deetergp (Engineering), see https://stackoverflowteams.com/c/expensify/questions/9980/ for more details.

@kavimuru
Copy link
Author

kavimuru commented Mar 6, 2024

@anmurali FYI I haven't added the External label as I wasn't 100% sure about this issue. Please take a look and add the label if you agree it's a bug and can be handled by external contributors.

@deetergp deetergp added Hourly KSv2 and removed Daily KSv2 labels Mar 7, 2024
@deetergp
Copy link
Contributor

deetergp commented Mar 7, 2024

Is this our culprit PR? #35255 (cc @eh2077 @s77rt @luacmartins)

@youssef-lr
Copy link
Contributor

We have just added support for PDF receipts, so I'm not sure this would qualify as a deploy blocker because we don't support PDF receipts in production right now.

@deetergp
Copy link
Contributor

deetergp commented Mar 7, 2024

Thanks @youssef-lr, that sounds reasonable. I'll remove the label and demote this back down to Daily.

@deetergp deetergp added Daily KSv2 and removed DeployBlockerCash This issue or pull request should block deployment Hourly KSv2 labels Mar 7, 2024
@s77rt
Copy link
Contributor

s77rt commented Mar 7, 2024

@eh2077 Can you please take a quick look on this?

@eh2077
Copy link
Contributor

eh2077 commented Mar 8, 2024

@s77rt Sure. I'm looking into it.

@eh2077
Copy link
Contributor

eh2077 commented Mar 8, 2024

It seems not easy to avoid reloading PDF when going back and forth between 1:1 DM, IOU report and transaction thread. This is because the browser by default can't cache the canvas of the PDFThumbnail but it's able to cache image by default.

While checking this issue, I also found another flaw that's easier to avoid - PDF thumbnail reloads when hovering on it

Screen.Recording.2024-03-08.at.3.51.11.PM.mov

@eh2077
Copy link
Contributor

eh2077 commented Mar 8, 2024

I think the reloading issue when hovering is a legit regression. I can make a quick PR to fix it.

But the reloading issue when re-visiting different pages seems a tricky one. Can this be a followup improvement instead of a regression?

@s77rt @luacmartins What're your opinions?

@s77rt
Copy link
Contributor

s77rt commented Mar 8, 2024

@eh2077 Can you please raise a PR fixing the hover issue? I thought we fixed that here #35255 (comment) Is this another case that we missed?

@deetergp deetergp added Weekly KSv2 and removed Daily KSv2 labels Jul 8, 2024
@melvin-bot melvin-bot bot removed the Overdue label Jul 8, 2024
Copy link

melvin-bot bot commented Jul 10, 2024

📣 It's been a week! Do we have any satisfactory proposals yet? Do we need to adjust the bounty for this issue? 💸

@melvin-bot melvin-bot bot added the Overdue label Jul 16, 2024
@anmurali anmurali changed the title [$250] PDF receipt preview loads each time visiting 1:1 DM, report and transaction thread [$500] PDF receipt preview loads each time visiting 1:1 DM, report and transaction thread Jul 17, 2024
Copy link

melvin-bot bot commented Jul 17, 2024

Upwork job price has been updated to $500

@anmurali
Copy link

Increased the bounty to $500 on the job.

@melvin-bot melvin-bot bot removed the Overdue label Jul 17, 2024
@tsa321
Copy link
Contributor

tsa321 commented Jul 20, 2024

Proposal

Please re-state the problem that we are trying to solve in this issue.

In Offline mode, PDF thumbnail of receipt is always re-rendered when revisiting the report

What is the root cause of that problem?

When showing PDF thumbnail of the receipt in ReportActionItemImage:

} else if (isLocalFile && filename && Str.isPDF(filename) && typeof attachmentModalSource === 'string') {
propsObj = {isPDFThumbnail: true, source: attachmentModalSource};

the PDFThumbnail:

function PDFThumbnail({previewSourceURL, style, isAuthTokenRequired = false, enabled = true, onPassword, onLoadError}: PDFThumbnailProps) {

doesn't cache the rendered image. So the pdf file will be rendered each time the pdfthumbnail mounted.

What changes do you think we should make in order to solve the problem?

We could implement caching logic in PDFThumbnail to store the cached image in an object variable using previewSourceURL as the key. To generate the cached image, we could add the canvasRef property to the Thumbnail of PDFThumbnail and convert the canvas element into a blob object.
The changes in PDFThumbnail code could be:

    import pdfWorkerSource from 'pdfjs-dist/legacy/build/pdf.worker';
+ import React, {useCallback, useMemo, useRef, useState} from 'react';
   import {View} from 'react-native';
   import {Document, Image, pdfjs, Thumbnail} from 'react-pdf';
   import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator';
+ import * as Expensicons from '@components/Icon/Expensicons';
+ import ThumbnailImage from '@components/ThumbnailImage';
   import useThemeStyles from '@hooks/useThemeStyles';
   import addEncryptedAuthTokenToURL from '@libs/addEncryptedAuthTokenToURL';
+ import CONST from '@src/CONST';
   import PDFThumbnailError from './PDFThumbnailError';
   import type PDFThumbnailProps from './types';

if (!pdfjs.GlobalWorkerOptions.workerSrc) {
    pdfjs.GlobalWorkerOptions.workerSrc = URL.createObjectURL(new Blob([pdfWorkerSource], {type: 'text/javascript'}));
}

+ let pdfThumbnailImageCaches = {};

function PDFThumbnail({
    previewSourceURL,
    cachedPDFThumbnail = null,
    style,
    isAuthTokenRequired = false,
    enabled = true,
    onPassword,
    onLoadError,
    onRenderSuccess = () => {},
}: PDFThumbnailProps) {
      const styles = useThemeStyles();
+    const canvasRef = useRef();
      const [failedToLoad, setFailedToLoad] = useState(false);

+    const [thumbnailCache, setThumbnailCache] = useState(pdfThumbnailImageCaches[previewSourceURL]);

+    const cachePDFThumbnailImage = useCallback(() => {
+        if (!!canvasRef.current) {
+            canvasRef.current.toBlob((blob) => {
+                const href = URL.createObjectURL(blob);
+                pdfThumbnailImageCaches[previewSourceURL] = href;
+                setThumbnailCache(href);
+            }, 'image/png');
+        }
+    }, [previewSourceURL, pdfThumbnailImageCaches]);
   
      const thumbnail = useMemo(() => {
+        if (!!thumbnailCache) {
+            return (
+                <ThumbnailImage
+                    previewSourceURL={thumbnailCache}
+                    style={[styles.w100, styles.h100]}
+                    isAuthTokenRequired={false}
+                    shouldDynamicallyResize={false}
+                    fallbackIcon={Expensicons.Document}
+                    objectPosition={CONST.IMAGE_OBJECT_POSITION.TOP}
+                />
+            );
+        }

          return (
              <Document
                  loading={<FullScreenLoadingIndicator />}
                  file={isAuthTokenRequired ? addEncryptedAuthTokenToURL(previewSourceURL) : previewSourceURL}
                  options={{
                      cMapUrl: 'cmaps/',
                      cMapPacked: true,
                  }}
                  externalLinkTarget="_blank"
                  onPassword={onPassword}
                  onLoad={() => {
                      setFailedToLoad(false);
                  }}
                  onLoadError={() => {
                      if (onLoadError) {
                          onLoadError();
                      }
                      setFailedToLoad(true);
                  }}
                  error={() => null}
              >
                  <View pointerEvents="none">
                      <Thumbnail
                          pageIndex={0}
+                        canvasRef={canvasRef}
+                        onRenderSuccess={cachePDFThumbnailImage}
                      />
                  </View>
              </Document>
        );
      }, [isAuthTokenRequired, previewSourceURL, onPassword, onLoadError, thumbnailCache]);

      return (
          <View style={[style, styles.overflowHidden, failedToLoad && styles.h100]}>
              <View style={[styles.w100, styles.h100, !failedToLoad && {...styles.alignItemsCenter, ...styles.justifyContentCenter}]}>
                  {enabled && !failedToLoad && thumbnail}
                  {failedToLoad && <PDFThumbnailError />}
              </View>
          </View>
      );
  }

  PDFThumbnail.displayName = 'PDFThumbnail';
  export default React.memo(PDFThumbnail);

If we want to store the cached image in onyx as base64 we could do that too.

Alternative Solution

If we want to move the caching logic in ReportActionImageItem, the details:

alternative solution

We could use canvasRef property of Thumbail of the PDFThumnail:

<Thumbnail pageIndex={0} />

and use the canvasElement to generate URL blob image of the canvas. (I don't know if storing base64 of the image is allowed in onyx, if it is allowed we could use the base64 instead of the url blob)

The rough code could be, in PDFThumbnail we add canvas ref and use canvasRef and onRenderSuccess property of the Thumbnail:

+ const canvas = useRef();
...
<Thumbnail 
      pageIndex={0} 
+      canvasRef={canvas} 
+      onRenderSuccess={() => onRenderSuccess(canvas.current)}
}} />

Then in ReportActionItemImage we add:

const cachedPDFReceiptThumbnail = useRef(ReceiptUtils.getPDFThumbnailCache(transaction.transactionID));
...
...
const cachePDFThumbnail = useCallback((canvasElement) => {
    if (!!canvasElement) {
        canvasElement.toBlob((blob) => {
            const href = URL.createObjectURL(blob);
            cachedPDFReceiptThumbnail.current = href;
            ReceiptUtils.cachePDFReceiptThumbnail(transaction.transactionID, href);
        }, 'image/png');
    }
}, [transaction]);


....
else if (isLocalFile && filename && Str.isPDF(filename) && typeof attachmentModalSource === 'string') {
    propsObj = {
        isPDFThumbnail: true,
        source: attachmentModalSource,
        onPDFRenderSuccess: cachePDFThumbnail,
        cachedPDFThumbnail: cachedPDFReceiptThumbnail.current
    };
}

The ReceiptUtils.cachePDFReceiptThumbnail could be simple :

function cachePDFReceiptThumbnail(transactionID: string, pdfThumbnailCache: string) {
    pdfThumbnailImageCaches[transactionID] = pdfThumbnailCache;  
}

function getPDFThumbnailCache(transactionID: string) {
    return pdfThumbnailImageCaches[transactionID];
}

Maybe we could combine transactionID with lastModified as key of the pdfThumbnailImageCaches.

Then in ReceiptImage:

if (isPDFThumbnail) {
        if (!!cachedPDFThumbnail) {
            return (
            <ThumbnailImage
                previewSourceURL={cachedPDFThumbnail ?? ''}
                style={[styles.w100, styles.h100]}
                isAuthTokenRequired={false}
                shouldDynamicallyResize={false}
                fallbackIcon={fallbackIcon}
                fallbackIconSize={fallbackIconSize}
                fallbackIconColor={fallbackIconColor}
                fallbackIconBackground={fallbackIconBackground}
                objectPosition={shouldUseInitialObjectPosition ? CONST.IMAGE_OBJECT_POSITION.INITIAL : CONST.IMAGE_OBJECT_POSITION.TOP}
            />);
        }
        return (
            <PDFThumbnail
                previewSourceURL={source ?? ''}
                style={[styles.w100, styles.h100]}
                onRenderSuccess={onPDFRenderSuccess}
                cachedPDFThumbnail={cachedPDFThumbnail}
            />
        );
    }

@melvin-bot melvin-bot bot added Daily KSv2 and removed Weekly KSv2 labels Jul 20, 2024
@tsa321
Copy link
Contributor

tsa321 commented Jul 21, 2024

Proposal

updated

@s77rt
Copy link
Contributor

s77rt commented Jul 21, 2024

The bug is no longer reproducible. The PDF preview loads only when going forth (opening new reports), when going back the pdf preview is kept rendered as expected.

Screen.Recording.2024-07-21.at.1.56.14.PM.mov

@deetergp We can close this one. Looks we have a room for improvement

@tsa321
Copy link
Contributor

tsa321 commented Jul 21, 2024

@s77rt I can still reproduce it in the latest main:

macos-web-d.mp4

@tsa321
Copy link
Contributor

tsa321 commented Jul 21, 2024

@s77rt for navigating back to main expense chat, you can switch to another chat and then return to the main expense chat; you will see the loading spinner.

@s77rt
Copy link
Contributor

s77rt commented Jul 21, 2024

@tsa321 Going back to the IOU report via the "From" link does not cause the preview to reload, but doing so from the LHN reloads the preview. Can you explain why is that happening?

Screen.Recording.2024-07-21.at.5.58.13.PM.mov

@tsa321
Copy link
Contributor

tsa321 commented Jul 23, 2024

@s77rt, please wait. I will try to take a look.

@melvin-bot melvin-bot bot added the Overdue label Jul 23, 2024
@s77rt
Copy link
Contributor

s77rt commented Jul 23, 2024

Waiting on proposals...

@melvin-bot melvin-bot bot removed the Overdue label Jul 23, 2024
@tsa321
Copy link
Contributor

tsa321 commented Jul 24, 2024

@s77rt The PDF also loads immediately when the user presses the browser's back button. When the user presses the link to the parent report in the report header, it functions similarly to going back (much like pressing the browser's back button). Perhaps the stack navigation/browser preserves the content of the previous screen/route, allowing the PDF to load immediately.

Copy link

melvin-bot bot commented Jul 24, 2024

📣 It's been a week! Do we have any satisfactory proposals yet? Do we need to adjust the bounty for this issue? 💸

@s77rt
Copy link
Contributor

s77rt commented Jul 24, 2024

@tsa321 I don't think we really have a bug then. Opening a new report renders a new PDF Previewer and the loader is expected since the pdf is loaded at library level (unlike images where they are loaded at the browser level where caching is implemented). This can be a feature request but if we implement a caching mechanism it should be at react-pdf level and we should cache the pdf content and not its thumbnail.

(Same note applies for mobile if the bug is reproducible there)

@tsa321
Copy link
Contributor

tsa321 commented Jul 24, 2024

@s77rt Actually, we can cache all pages of the PDF as images. Is this the expected result? Caching the PDF means converting it into images, correct? Also, the server returns the PDF thumbnail as images too. Why should we do this at the react-pdf level? We can do it in the app.

@s77rt
Copy link
Contributor

s77rt commented Jul 24, 2024

@tsa321 Caching is to save the pdf data (bytes) for a given uri. If we use a known uri we'd use the cached data instead of fetching that uri contents again.

Why should we do this at the react-pdf level?

It's a feature for react-pdf and not related to the app.

For this case the feature is a low priority since the uri that the PDF Previewer will load always points to a local file

@tsa321
Copy link
Contributor

tsa321 commented Jul 24, 2024

@s77rt, the PDF file is already stored as a blob URI, meaning it's stored in memory as bytes.

I believe we may have different perspectives on PDF caching:
My understanding is that PDF rendering involves drawing various elements such as text, lines, shapes, rectangles, paths, vectors, and images specified in the PDF file onto a canvas. This process is managed by react-pdf or whichever PDF engine library is utilized (indicated by the loading spinner during rendering).

On the other hand, caching the PDF means storing the rendered result as an image, so that it doesn't need to be redrawn.

However, I agree that this issue is of very low priority. I'm okay with closing this issue. @deetergp, what are your thoughts on this issue? Thank you.

@s77rt
Copy link
Contributor

s77rt commented Jul 25, 2024

@deetergp Let's close this based on #37852 (comment)

@deetergp
Copy link
Contributor

Sounds like a plan @s77rt 👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Bug Something is broken. Auto assigns a BugZero manager. Daily KSv2 Engineering External Added to denote the issue can be worked on by a contributor Help Wanted Apply this label when an issue is open to proposals by contributors
Projects
None yet
Development

No branches or pull requests

9 participants