Skip to content

(1/2) Implement store/caching for attachments#63723

Closed
NJ-2020 wants to merge 30 commits intoExpensify:mainfrom
NJ-2020:new-feat/9402-1
Closed

(1/2) Implement store/caching for attachments#63723
NJ-2020 wants to merge 30 commits intoExpensify:mainfrom
NJ-2020:new-feat/9402-1

Conversation

@NJ-2020
Copy link
Contributor

@NJ-2020 NJ-2020 commented Jun 7, 2025

Explanation of Change

Store/Cache attachment when uploading a file or markdown text link attachment i.e ![](https://images.unsplash.com/...)

Fixed Issues

$ #9402
PROPOSAL: #9402 (comment)

Tests

Same as QA

  • Verify that no errors appear in the JS console

Offline tests

Same as QA

QA Steps

  • Verify that no errors appear in the JS console

Prerequisites: DEV mode only

  1. Open Expensify App
  2. Go to any chat
  3. Open inspect element and go to > Applications tab

    File Upload
    1. Open attachment picker
    2. Select any file
    3. Go to CacheAPI/Onyx storage for web and observe the changes

    Markdown text link attachment
    1. Go to input chat
    2. Type any markdown text link attachment e.g ![](https://images.unsplash.com/...) and send it
    3. Go to CacheAPI (web-desktop only) or Onyx (attachment_{id}) storage and observe the changes

PR Author Checklist

  • I linked the correct issue in the ### Fixed Issues section above
  • I wrote clear testing steps that cover the changes made in this PR
    • I added steps for local testing in the Tests section
    • I added steps for the expected offline behavior in the Offline steps section
    • I added steps for Staging and/or Production testing in the QA steps section
    • I added steps to cover failure scenarios (i.e. verify an input displays the correct error message if the entered data is not correct)
    • I turned off my network connection and tested it while offline to ensure it matches the expected behavior (i.e. verify the default avatar icon is displayed if app is offline)
    • I tested this PR with a High Traffic account against the staging or production API to ensure there are no regressions (e.g. long loading states that impact usability).
  • I included screenshots or videos for tests on all platforms
  • I ran the tests on all platforms & verified they passed on:
    • Android: Native
    • Android: mWeb Chrome
    • iOS: Native
    • iOS: mWeb Safari
    • MacOS: Chrome / Safari
    • MacOS: Desktop
  • I verified there are no console errors (if there's a console error not related to the PR, report it or open an issue for it to be fixed)
  • I verified there are no new alerts related to the canBeMissing param for useOnyx
  • I followed proper code patterns (see Reviewing the code)
    • I verified that any callback methods that were added or modified are named for what the method does and never what callback they handle (i.e. toggleReport and not onIconClick)
    • I verified that comments were added to code that is not self explanatory
    • I verified that any new or modified comments were clear, correct English, and explained "why" the code was doing something instead of only explaining "what" the code was doing.
    • I verified any copy / text shown in the product is localized by adding it to src/languages/* files and using the translation method
      • If any non-english text was added/modified, I used JaimeGPT to get English > Spanish translation. I then posted it in #expensify-open-source and it was approved by an internal Expensify engineer. Link to Slack message:
    • I verified all numbers, amounts, dates and phone numbers shown in the product are using the localization methods
    • I verified any copy / text that was added to the app is grammatically correct in English. It adheres to proper capitalization guidelines (note: only the first word of header/labels should be capitalized), and is either coming verbatim from figma or has been approved by marketing (in order to get marketing approval, ask the Bug Zero team member to add the Waiting for copy label to the issue)
    • I verified proper file naming conventions were followed for any new files or renamed files. All non-platform specific files are named after what they export and are not named "index.js". All platform-specific files are named for the platform the code supports as outlined in the README.
    • I verified the JSDocs style guidelines (in STYLE.md) were followed
  • If a new code pattern is added I verified it was agreed to be used by multiple Expensify engineers
  • I followed the guidelines as stated in the Review Guidelines
  • I tested other components that can be impacted by my changes (i.e. if the PR modifies a shared library or component like Avatar, I verified the components using Avatar are working as expected)
  • I verified all code is DRY (the PR doesn't include any logic written more than once, with the exception of tests)
  • I verified any variables that can be defined as constants (ie. in CONST.ts or at the top of the file that uses the constant) are defined as such
  • I verified that if a function's arguments changed that all usages have also been updated correctly
  • If any new file was added I verified that:
    • The file has a description of what it does and/or why is needed at the top of the file if the code is not self explanatory
  • If a new CSS style is added I verified that:
    • A similar style doesn't already exist
    • The style can't be created with an existing StyleUtils function (i.e. StyleUtils.getBackgroundAndBorderStyle(theme.componentBG))
  • If the PR modifies code that runs when editing or sending messages, I tested and verified there is no unexpected behavior for all supported markdown - URLs, single line code, code blocks, quotes, headings, bold, strikethrough, and italic.
  • If the PR modifies a generic component, I tested and verified that those changes do not break usages of that component in the rest of the App (i.e. if a shared library or component like Avatar is modified, I verified that Avatar is working as expected in all cases)
  • If the PR modifies a component related to any of the existing Storybook stories, I tested and verified all stories for that component are still working as expected.
  • If the PR modifies a component or page that can be accessed by a direct deeplink, I verified that the code functions as expected when the deeplink is used - from a logged in and logged out account.
  • If the PR modifies the UI (e.g. new buttons, new UI components, changing the padding/spacing/sizing, moving components, etc) or modifies the form input styles:
    • I verified that all the inputs inside a form are aligned with each other.
    • I added Design label and/or tagged @Expensify/design so the design team can review the changes.
  • If a new page is added, I verified it's using the ScrollView component to make it scrollable when more elements are added to the page.
  • I added unit tests for any new feature or bug fix in this PR to help automatically prevent regressions in this user flow.
  • If the main branch was merged into this PR after a review, I tested again and verified the outcome was still expected according to the Test steps.

Screenshots/Videos

Android: Native
9402_android_native.mov
Android: mWeb Chrome
9402_android_mweb.mov
iOS: Native
9402_ios_native.mov
iOS: mWeb Safari

I have an issue while inspecting the web)

MacOS: Chrome / Safari
9402_web.mov
MacOS: Desktop
9402_desktop.1.mov

@NJ-2020 NJ-2020 marked this pull request as ready for review June 8, 2025 14:40
@NJ-2020 NJ-2020 requested a review from a team as a code owner June 8, 2025 14:40
@melvin-bot melvin-bot bot requested review from parasharrajat and removed request for a team June 8, 2025 14:40
@melvin-bot
Copy link

melvin-bot bot commented Jun 8, 2025

@parasharrajat Please copy/paste the Reviewer Checklist from here into a new comment on this PR and complete it. If you have the K2 extension, you can simply click: [this button]

(imagePickerFunc: (options: CameraOptions, callback: Callback) => Promise<ImagePickerResponse>): Promise<Asset[] | void> =>
new Promise((resolve, reject) => {
imagePickerFunc(getImagePickerOptions(type, fileLimit), (response: ImagePickerResponse) => {
imagePickerFunc(getImagePickerOptions(type, fileLimit), async (response: ImagePickerResponse) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't allow async await.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am still waiting for this comment: #63723 (comment)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reverting this changes due to this comment

Will fix it in the next PR

Comment on lines +156 to +180
if (!response.assets?.length) {
return resolve();
}

const localCopies = await keepLocalCopy({
files: response.assets.map((asset) => ({
uri: asset.uri,
fileName: asset.fileName ?? '',
})) as [FileToCopy, ...FileToCopy[]],
destination: 'documentDirectory',
});

const assets = localCopies.map((localCopy, index) => {
if (localCopy.status !== 'success') {
throw new Error('Failed to create local copy for file');
}

return {
...(response.assets?.at(index) ?? {}),
uri: localCopy.localUri,
};
});

const targetAsset = assets.at(0);
const targetAssetUri = targetAsset?.uri;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We not need to cache the attachment while the user is adding one. Let's only worry about the message attachments for now.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're not caching it, this for uploading attachment from Attachment picker specifically from Choose from Gallery where the files are copied to cache directories but we need to copy it into document directories

More info: #63723 (comment)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Replied on that linked thread. I think this is kind of out of scope of this issue.

Comment on lines +69 to +71
const imageSource = processedPreviewSource;
// const isAuthTokenRequired = isLocalFile(imageSource) ? false : isAttachmentOrReceipt;
const isAuthTokenRequired = isAttachmentOrReceipt;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Didn't understand this change?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ohh sorry, this is because when rendering attachment specifically using local uri, its failing to load the attachment, it's because we are passing the isAuthTokenRequired true for local uri

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

const isAuthTokenRequired = isLocalFile(imageSource) ? false

But this says false for local file.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, right we need to pass false for local uri

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But the previous logic was doing the same so why did you change it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nope

const thumbnailImageComponent = (
<ThumbnailImage
previewSourceURL={processedPreviewSource}
style={styles.webViewStyles.tagStyles.img}
isAuthTokenRequired={isAttachmentOrReceipt}

I think we can revert this changes because this related for retrieving attachment i.e the second phase of the PR (2/2), wdyt?

return;
}
if (uri.startsWith('file://')) {
Onyx.set(`${ONYXKEYS.COLLECTION.ATTACHMENT}${attachmentID}`, {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's convert this collection to store attacments into a a collection for report, not per attachment basis.

So `${ONYXKEYS.COLLECTION.ATTACHMENT}${reportId}. This way we can subscribe this key in the report screen and get the source for any attachment otherwise, we will have to subscribe to each attachment separately.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please let me know how it looks first. Does this increase complexity and impact performance?

Overall, I believe we should create a custom hook to handle attachments. I am particularly curious on how you will handle the remote source change from backend. Can you show me code on how you will handle it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's convert this collection to store attacments into a a collection for report, not per attachment basis.

I think we're using the data-attachment-id attribute value from the attachment tag for both storing/retrieving attachment

Basically if there's any attachment it will render through ImageRenderer|VideoRenderer|AnchorRenderer, inside that we can retrieve the data-attachment-id attribute value provided from BE and use the attachment id value to get the local source from Onyx

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe for alternative option instead of passing reportID we can use the reportActionID which is more easier to handle delete attachment, retrieving and others

But I think we can use the data-attachment-id from BE, which is specifically for attachments. For old attachments without attachment IDs, we can use reportActionID + {index}.

Please let me know how it looks first. Does this increase complexity and impact performance?

I am not sure if it's really impacting our app perfomance, because for every attachment we need to get the list of attachments of the current report(which can be more than 1), but we only need 1 attachment source from all the lists and we need to filter out again that matches with the data-attachment-id value

Wdyt?

Copy link
Contributor Author

@NJ-2020 NJ-2020 Jun 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall, I believe we should create a custom hook to handle attachments. I am particularly curious on how you will handle the remote source change from backend. Can you show me code on how you will handle it?

Currently, I handle it inside the getAttachmentSource function, where first I compared the current source from BE with attachment.remoteSource, if there's any differences, then we will recache the attachment and show the new one

function getAttachmentSource(attachmentID: string, currentSource: string) {
if (!attachmentID) {
return;
}
const attachment = attachments?.[`${ONYXKEYS.COLLECTION.ATTACHMENT}${attachmentID}`];
if (attachment?.remoteSource && attachment.remoteSource !== currentSource) {
storeAttachment(attachmentID, currentSource);
return currentSource;
}


Edit:
Ah, you're right I think we need to make new hook for retrieving attachment specifically for CacheAPI i.e like useOnyx, because it's returning Promise value

But I think I'll do it in the second phase of the PR, right?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, second PR sounds good,

return;
}

RNFetchBlob.config({fileCache: true, path: `${RNFetchBlob.fs.dirs.DocumentDir}/${attachmentID}`})
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a good idea to store in DocumentDir as it can not be cleared but system cleanup. We should instead create a subfolder in the CacheDir and save attachments there.

When we are clearly the cacheDir on signout. We exclude this folder from that cleanup.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's because when a user upload an attachment, if we save the file into cache dir, the attachment preview will gone if the user restart the app/device

So to fix this we need to store the attachment into documents directory i.e com.expensify.com/files/attachment_{id}

We will recache the attachment if there's new changes from BE, if not then we will use from the pre-cached attachment

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need to worry about the attachment preview when the user is uploading the attachment. Until it is sent, we don't have to save it for offline preview. If needed we would do that later in different PR.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need to worry about the attachment preview when the user is uploading the attachment. Until it is sent

Agree, so I think we can do it inside the storeAttachment function

we don't have to save it for offline preview

I think it will show weird behavior to the user if the user close and then reopen the app, because it will show empty content(offline icon) for all optimistic attachments

Wdyt?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When you close the app on add attachment modal, I think user will have to reopen the flow. Anyways, as I said, we will revisit this after we are done saving attachments in different PR, if needed.

Let's break it down to smaller PRs to faster completion.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, I will do this in the next PR

Comment on lines +748 to +751
if (file) {
parameters.attachmentID = attachmentID;
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to pass this to get the data-attachment-id attribute inside the attachment tag i.e <img src="..." data-attachment-id="1234..." />

For markdown text link attachments, we will use the reportActionID + index

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to pass this to get the data-attachment-id attribute inside the attachment tag i.e

Why do we need to do this? Does it handle the optimistic case?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's used for storing and retrieving the attachment from Onyx/CacheAPI by using the data-attachment-id attachment_{idHere}

Comment on lines +2 to +6
attachmentID: string;

source?: string;

remoteSource?: string;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing properties comment.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

return;
}

RNFetchBlob.config({fileCache: true, path: `${RNFetchBlob.fs.dirs.DocumentDir}/${attachmentID}`})
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need to worry about the attachment preview when the user is uploading the attachment. Until it is sent, we don't have to save it for offline preview. If needed we would do that later in different PR.

@parasharrajat
Copy link
Member

@NJ-2020 Thanks for your work on this, and remain committed while we were discussing it. Can you please speed up the implementation? I believe we will have to go through a few iterations before we complete this task. Thus, it is important to update daily.

@NJ-2020
Copy link
Contributor Author

NJ-2020 commented Jun 10, 2025

Okay, sure

@NJ-2020
Copy link
Contributor Author

NJ-2020 commented Jun 10, 2025

PR will be ready tomorrow

I am still working on the unit test file

@NJ-2020
Copy link
Contributor Author

NJ-2020 commented Jun 11, 2025

There's some issues while creating the unit test file
Will share an update when it's done

@NJ-2020
Copy link
Contributor Author

NJ-2020 commented Jun 12, 2025

PR is ready

cc: @parasharrajat

@NJ-2020
Copy link
Contributor Author

NJ-2020 commented Jun 15, 2025

Bump @parasharrajat for review

Comment on lines +10 to +12
if (!key) {
return;
}
Copy link
Member

@parasharrajat parasharrajat Jun 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

throw an error instead if key is not from passed CACHE_API_KEYS in init

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Comment on lines +22 to +24
if (!cacheName || !key || !value) {
return;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't have to check for these when all of them are required params. Please define the type in in App Response.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, add a logic that key is from CACHE_API_KEYS as this will allow overriding the settings passed in init.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, add a logic that key is from CACHE_API_KEYS as this will allow overriding the settings passed in init.

Done

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't have to check for these when all of them are required params. Please define the type in in App Response.

Regarding this, CacheAPI only allow response value type, in this case the return response value returned from the fetch API, so I think defining our own response type definition I think it may can lead to typeerror or unmatch type with certain urls, wdyt?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

its fine to use standard type.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok

Comment on lines +46 to +48
if (!keys || keys.length === 0) {
return;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should not pass any keys here instead they should be taken from the keys passed in init.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

return cache.match(key);
});
}
function remove(cacheName: string, key: string) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the cacheName?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cacheName is individual key in the lists of CACHE_API_KEYS

Screenshot 2025-06-16 at 17 08 32

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's keep this cacheName internal in the cacheAPI.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure

};
}) as [FileToCopy, ...FileToCopy[]],
destination: 'cachesDirectory',
destination: 'documentDirectory',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need this change now?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, because the caches directory will automatically cleared out when the app is closed which prevents the user to see the attachment when re-opening the app again

global.fetch = TestHelper.getGlobalFetchMock();
});

it('should store for file in Onyx', async () => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add tests for:

  1. Delete the attachment.
  2. Change of remote source should reflect the change in local source.
  3. After attachment is stored, you should confirm whether it was cached or not. Have tests for both Native and web.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, sure

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will create the unit test file after all the changes are done

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's add it now. It is to validate that your changes work so as you go along updating this implementation the tests should also pass.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We follow strict naming, so please move this to tests/actions/AttachmentTest.ts if you action file is called Attachment.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

attachmentID: string;

/** Source url of the attachment either can be local or remote url */
source?: string;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why would this source be remote? Should the remoteSource be remote? we should make sure this property always reflect local source and remoteSource remote.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry may bad, done, fixed

NetworkConnection.clearReconnectionCallbacks();
SessionUtils.resetDidUserLogInDuringSession();
resetHomeRouteParams();
CacheAPI.clear([CONST.CACHE_API_KEYS.ATTACHMENTS]);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this slow down the signout process when there are many resources to be removed?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've tried measuring the logout function performance using like this:

function cleanupSession() {
    const startTime = performance.now();

    Pusher.disconnect();
    Timers.clearAll();
    Welcome.resetAllChecks();
    MainQueue.clear();
    HttpUtils.cancelPendingRequests();
    PersistedRequests.clear();
    NetworkConnection.clearReconnectionCallbacks();
    SessionUtils.resetDidUserLogInDuringSession();
    resetHomeRouteParams();
    CacheAPI.clear();
    clearCache().then(() => {
        const endTime = performance.now();
        const duration = endTime - startTime;

        // console.log...
    });
    clearSoundAssetsCache();
    Timing.clearData();
}

And here's the result using 5 attachments video, for each video the size is: 14.3MB and the total is 71.5MB :

Without clearing CacheAPI With clearing CacheAPI
Duration: 174ms 245.8ms

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a lot. So if a user keeps on using the app for a long time and logs out, the app might take seconds to log out.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, so I think for alternative solution, I think we can clear the cache api when the user is on the login page or inside the PublicScreens, by using this way it will not affect the logout process, wdyt?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No a good idea. Maybe we leave it as it is. When a user uninstalls the app, whether the cache is cleaned or not?

We might need to add an option in the app to clean the storage manually.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When a user uninstalls the app, whether the cache is cleaned or not?

I am not sure about about uninstalling the app, because I don't know if the native os is deleting the entire app including the media folder which will also delete the attachment files

But for web if we don't clear the data/attachments on signout process, the data will keep saved until the user removes completely from their application storage

We might need to add an option in the app to clean the storage manually.

Yes, and I think maybe we can also implement like maybe storage management?

});

attachments.forEach((attachment) => {
storeAttachment(attachment.attachmentID, attachment.uri ?? '');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a problem with code, what if the message failed to send the image will still be cached. we should follow optimistic patterns here.

you should update Onyx store values in optimisticData, successData and failureData. Same for delete attachment. Now this can be little tricky let's see what options do we have.

We can do auto cleanup and save when a attachment key is updated in Onyx.
we have a subscription to attachment collection which will update whenever we make changes to it so when you add a new attachment with remoteSource and attachmentID, we can update the cache to save the remote source and same for delete, we can remove the cache. This way cache will always be in sync with Onyx. This is just an idea and I am not sure how fast would it be. Please suggest yours also.

That's why I was thinking we save attachments report basis so we can subscribe to only attachment of visible report.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to consider this case. It is important app concept.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think for Onyx we can solve it by passing into optimisticData, successData and failureData

But regarding CacheAPI it seems a little bit tricky, I am not sure regarding the subscribtion solution because we have to manage the Onyx and CacheAPI consistent in each other and make sure it runs smoothly

Please suggest yours also

IMO, I think we can introduce new method prop function inside libs/API onSuccess and onFailure
Here's an example

API.write(commandName, parameters, {
    optimisticData,
    successData,
    failureData,
    onSuccess: () => {},
    onFailure: () => { /** remove attachment from CacheAPI */ },
});

Wdyt?, we can improve this later

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO, I think we can introduce new method prop function inside libs/API onSuccess and onFailure
Here's an example

We can't do that. It is against our architecture. We need to find a way to do this. But we can focus on it later.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay


const CacheAPIKeys = Object.values(CONST.CACHE_API_KEYS);

CacheAPI.init(CacheAPIKeys);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's pass CONST.CACHE_API_KEYS it array to init function to pass CONST.CACHE_API_KEYS directly this function and then handle getting keys inside the function.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I left a very confusing commen. Sorry about that. What i meant to say was that we should pass CONST.CACHE_API_KEYS directly to the init function or send specific keys are array.

eg.

As object

CacheAPI.init(CONST.CACHE_API_KEYS)

As array

CacheAPI.init([Key1, Key2]);

But I am fine with the current logic where you are directly accessing them inside the function as we do not have any other keys currently for cache.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, okay, thank you

Comment on lines +3585 to +3588
ATTACHMENT: /<(img|video)[^>]*>/gi,
ATTACHMENT_ID: /data-attachment-id=(["'])(.*?)\1/,
ATTACHMENT_SOURCE_ID: /chat-attachments\/(\d+)/,
ATTACHMENT_SOURCE: /(src|data-expensify-source|data-optimistic-src)="([^"]+)"/i,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is somewhat confusing. Can you please add comments to explain use of each regex.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

if (!('caches' in window)) {
throw new Error('Cache API is not supported');
}
const keys = Object.values(CONST.CACHE_API_KEYS);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, maybe I wan't clear. We have to use keys passed in parm but this is fine for now.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay

@NJ-2020
Copy link
Contributor Author

NJ-2020 commented Jun 22, 2025

Bump @parasharrajat for above comments ^

@NJ-2020
Copy link
Contributor Author

NJ-2020 commented Jun 26, 2025

Bump @parasharrajat

@parasharrajat
Copy link
Member

I will start testing this tomorrow.

@NJ-2020
Copy link
Contributor Author

NJ-2020 commented Jul 1, 2025

Bump @parasharrajat

@NJ-2020 NJ-2020 closed this by deleting the head repository Jul 2, 2025
@NJ-2020
Copy link
Contributor Author

NJ-2020 commented Jul 2, 2025

Really sorry, due to some reasons, I've to close this PR

New PR here: #65321 - I am unable to reopen this PR

@NJ-2020
Copy link
Contributor Author

NJ-2020 commented Jul 2, 2025

Apologies for the confusion here

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants