From 2fe2027c2e639b677ca530cf9f1edac900302e78 Mon Sep 17 00:00:00 2001 From: NJ-2020 Date: Fri, 6 Jun 2025 14:35:03 +0700 Subject: [PATCH 01/99] feat: implement store/caching attachment for native --- src/ONYXKEYS.ts | 2 + .../AttachmentPicker/index.native.tsx | 4 +- .../HTMLRenderers/ImageRenderer.tsx | 13 ++-- .../AddCommentOrAttachmentParams.ts | 1 + src/libs/CacheAPI/index.native.ts | 13 ++++ src/libs/CacheAPI/index.ts | 55 ++++++++++++++++ src/libs/ReportActionsUtils.ts | 9 ++- src/libs/ReportUtils.ts | 6 +- src/libs/actions/Attachment/index.native.ts | 66 +++++++++++++++++++ src/libs/actions/Attachment/index.ts | 19 ++++++ src/libs/actions/Report.ts | 18 ++++- src/setup/index.ts | 3 + src/types/onyx/Attachment.tsx | 9 +++ src/types/onyx/index.ts | 2 + 14 files changed, 207 insertions(+), 13 deletions(-) create mode 100644 src/libs/CacheAPI/index.native.ts create mode 100644 src/libs/CacheAPI/index.ts create mode 100644 src/libs/actions/Attachment/index.native.ts create mode 100644 src/libs/actions/Attachment/index.ts create mode 100644 src/types/onyx/Attachment.tsx diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index de249a8bd7e4a..775db017d490d 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -540,6 +540,7 @@ const ONYXKEYS = { /** Collection Keys */ COLLECTION: { + ATTACHMENT: 'attachment_', DOWNLOAD: 'download_', POLICY: 'policy_', POLICY_DRAFTS: 'policyDrafts_', @@ -941,6 +942,7 @@ type OnyxFormDraftValuesMapping = { }; type OnyxCollectionValuesMapping = { + [ONYXKEYS.COLLECTION.ATTACHMENT]: OnyxTypes.Attachment; [ONYXKEYS.COLLECTION.DOWNLOAD]: OnyxTypes.Download; [ONYXKEYS.COLLECTION.POLICY]: OnyxTypes.Policy; [ONYXKEYS.COLLECTION.POLICY_DRAFTS]: OnyxTypes.Policy; diff --git a/src/components/AttachmentPicker/index.native.tsx b/src/components/AttachmentPicker/index.native.tsx index 12b7df0521522..f42edafd74e67 100644 --- a/src/components/AttachmentPicker/index.native.tsx +++ b/src/components/AttachmentPicker/index.native.tsx @@ -213,7 +213,7 @@ function AttachmentPicker({ fileName: file.name ?? '', }; }) as [FileToCopy, ...FileToCopy[]], - destination: 'cachesDirectory', + destination: 'documentDirectory', }); return pickedFiles.map((file, index) => { @@ -221,7 +221,7 @@ function AttachmentPicker({ if (localCopy.status !== 'success') { throw new Error("Couldn't create local file copy"); } - + console.log('picked files', localCopy.localUri); return { name: file.name, uri: localCopy.localUri, diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/ImageRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/ImageRenderer.tsx index e650df1be6507..74e328aaa4703 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/ImageRenderer.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/ImageRenderer.tsx @@ -1,4 +1,4 @@ -import React, {memo} from 'react'; +import React, {memo, useEffect} from 'react'; import {useOnyx} from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx'; import type {CustomRendererProps, TBlock} from 'react-native-render-html'; @@ -12,7 +12,8 @@ import ThumbnailImage from '@components/ThumbnailImage'; import useLocalize from '@hooks/useLocalize'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; -import {getFileName, getFileType, splitExtensionFromFileName} from '@libs/fileDownload/FileUtils'; +import {getAttachmentSource} from '@libs/actions/Attachment'; +import {getFileName, getFileType, isLocalFile, splitExtensionFromFileName} from '@libs/fileDownload/FileUtils'; import Navigation from '@libs/Navigation/Navigation'; import {isArchivedNonExpenseReport} from '@libs/ReportUtils'; import tryResolveUrlFromApiRoot from '@libs/tryResolveUrlFromApiRoot'; @@ -66,7 +67,9 @@ function ImageRenderer({tnode}: ImageRendererProps) { // For other image formats, we retain the thumbnail as is to avoid unnecessary modifications. const processedPreviewSource = typeof previewSource === 'string' ? previewSource.replace(/\.png\.(1024|320)\.jpg$/, '.png') : previewSource; const source = tryResolveUrlFromApiRoot(isAttachmentOrReceipt ? attachmentSourceAttribute : htmlAttribs.src); - + const imageSource = getAttachmentSource(attachmentID) || processedPreviewSource; + console.log('imageSource', imageSource); + const isAuthTokenRequired = isLocalFile(imageSource) ? false : isAttachmentOrReceipt; const alt = htmlAttribs.alt; const imageWidth = (htmlAttribs['data-expensify-width'] && parseInt(htmlAttribs['data-expensify-width'], 10)) || undefined; const imageHeight = (htmlAttribs['data-expensify-height'] && parseInt(htmlAttribs['data-expensify-height'], 10)) || undefined; @@ -84,9 +87,9 @@ function ImageRenderer({tnode}: ImageRendererProps) { const thumbnailImageComponent = ( { + if (!key) { + return; + } + caches.has(key).then((isExist) => { + if (isExist) { + return; + } else { + caches.open(key); + } + }); + }); +} +function put(cacheName: string, key: string, value: Response) { + if (!cacheName || !key || !value) { + return; + } + caches.open(cacheName).then((cache) => { + cache.put(key, value); + }); +} +function get(cacheName: string, key: string) { + if (!cacheName || !key) { + return; + } + return caches.open(cacheName).then((cache) => { + return cache.match(key); + }); +} +function remove(cacheName: string, key: string) { + if (!cacheName || !key) { + return; + } + caches.open(cacheName).then((cache) => { + cache.delete(key); + }); +} +function clear(cacheName: string) { + if (!cacheName) { + return; + } + caches.delete(cacheName); +} +export default { + init, + put, + get, + remove, + clear, +}; diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index f2a85f3f02693..1174db9e17765 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -159,12 +159,15 @@ function isDeletedAction(reportAction: OnyxInputOrEntry m.concat(`${CONST.ATTACHMENT_ID_ATTRIBUTE}="${reportActionID}_${++attachmentID}" `)); + let index = 0; + return html.replace(/ m.concat(`${CONST.ATTACHMENT_ID_ATTRIBUTE}="${reportActionID}_${++index}" `)); } function getReportActionMessage(reportAction: PartialReportAction) { diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index fc3e842f5c33d..4978d8ccdbb63 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -5279,7 +5279,7 @@ function getParsedComment(text: string, parsingDetails?: ParsingDetails, mediaAt : lodashEscape(text); } -function getUploadingAttachmentHtml(file?: FileObject): string { +function getUploadingAttachmentHtml(file?: FileObject, attachmentID?: string): string { if (!file || typeof file.uri !== 'string') { return ''; } @@ -5288,6 +5288,7 @@ function getUploadingAttachmentHtml(file?: FileObject): string { `${CONST.ATTACHMENT_OPTIMISTIC_SOURCE_ATTRIBUTE}="${file.uri}"`, `${CONST.ATTACHMENT_SOURCE_ATTRIBUTE}="${file.uri}"`, `${CONST.ATTACHMENT_ORIGINAL_FILENAME_ATTRIBUTE}="${file.name}"`, + attachmentID && `${CONST.ATTACHMENT_ID_ATTRIBUTE}="${attachmentID}"`, 'width' in file && `${CONST.ATTACHMENT_THUMBNAIL_WIDTH_ATTRIBUTE}="${file.width}"`, 'height' in file && `${CONST.ATTACHMENT_THUMBNAIL_HEIGHT_ATTRIBUTE}="${file.height}"`, ] @@ -5335,9 +5336,10 @@ function buildOptimisticAddCommentReportAction( shouldEscapeText?: boolean, reportID?: string, reportActionID: string = rand64(), + attachmentID?: string, ): OptimisticReportAction { const commentText = getParsedComment(text ?? '', {shouldEscapeText, reportID}); - const attachmentHtml = getUploadingAttachmentHtml(file); + const attachmentHtml = getUploadingAttachmentHtml(file, attachmentID); const htmlForNewComment = `${commentText}${commentText && attachmentHtml ? '

' : ''}${attachmentHtml}`; const textForNewComment = Parser.htmlToText(htmlForNewComment); diff --git a/src/libs/actions/Attachment/index.native.ts b/src/libs/actions/Attachment/index.native.ts new file mode 100644 index 0000000000000..194fbef498b26 --- /dev/null +++ b/src/libs/actions/Attachment/index.native.ts @@ -0,0 +1,66 @@ +import RNFetchBlob from 'react-native-blob-util'; +import RNFS from 'react-native-fs'; +import Onyx, {OnyxCollection} from 'react-native-onyx'; +import {FileObject} from '@components/AttachmentModal'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {Attachment} from '@src/types/onyx'; + +let attachments: OnyxCollection | undefined; +let isAttachmentLoaded = false; +Onyx.connect({ + key: ONYXKEYS.COLLECTION.ATTACHMENT, + waitForCollectionCallback: true, + callback: (value) => { + if (!value) { + return; + } + attachments = value; + }, +}); + +function storeAttachment(attachmentID: string, uri: string) { + if (!attachmentID || !uri) { + console.log('leeh2'); + return; + } + + if (uri.startsWith('file://')) { + console.log('leeh44'); + Onyx.set(`${ONYXKEYS.COLLECTION.ATTACHMENT}${attachmentID}`, { + attachmentID, + source: uri, + }); + return; + } + + const attachment = attachments?.[attachmentID]; + + if (attachment?.source && attachment.remoteSource === uri) { + console.log('leeh'); + return; + } + + RNFetchBlob.config({fileCache: true}) + .fetch('GET', uri) + .then((response) => { + const filePath = response.path(); + console.log('filePath', filePath); + Onyx.set(`${ONYXKEYS.COLLECTION.ATTACHMENT}${attachmentID}`, { + attachmentID, + source: `file://${filePath}`, + }); + }) + .catch((error) => { + console.log('error', error); + }); +} + +function getAttachmentSource(attachmentID: string) { + if (!attachmentID) { + return; + } + const attachment = attachments?.[`${ONYXKEYS.COLLECTION.ATTACHMENT}${attachmentID}`]; + return attachment?.source; +} + +export {storeAttachment, getAttachmentSource}; diff --git a/src/libs/actions/Attachment/index.ts b/src/libs/actions/Attachment/index.ts new file mode 100644 index 0000000000000..3d5ec319923b4 --- /dev/null +++ b/src/libs/actions/Attachment/index.ts @@ -0,0 +1,19 @@ +import Onyx, {OnyxCollection} from 'react-native-onyx'; +import {FileObject} from '@components/AttachmentModal'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {Attachment} from '@src/types/onyx'; + +let attachments: OnyxCollection | undefined; +Onyx.connect({ + key: ONYXKEYS.COLLECTION.ATTACHMENT, + waitForCollectionCallback: true, + callback: (value) => (attachments = value), +}); + +function storeAttachment(attachmentID: string, uri: string) {} + +function getAttachmentSource(attachmentID: string) { + return ''; +} + +export {storeAttachment, getAttachmentSource}; diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 51f8012251557..cdf9e0930b05a 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -187,6 +187,7 @@ import type {ConnectionName} from '@src/types/onyx/Policy'; import type {NotificationPreference, Participants, Participant as ReportParticipant, RoomVisibility, WriteCapability} from '@src/types/onyx/Report'; import type {Message, ReportActions} from '@src/types/onyx/ReportAction'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; +import {storeAttachment} from './Attachment'; import {clearByKey} from './CachedPDFPaths'; import {setDownload} from './Download'; import {close} from './Modal'; @@ -649,6 +650,7 @@ function addActions(reportID: string, text = '', file?: FileObject) { let attachmentAction: OptimisticAddCommentReportAction | undefined; let commandName: typeof WRITE_COMMANDS.ADD_COMMENT | typeof WRITE_COMMANDS.ADD_ATTACHMENT | typeof WRITE_COMMANDS.ADD_TEXT_AND_ATTACHMENT = WRITE_COMMANDS.ADD_COMMENT; + const attachmentID = rand64(); if (text && !file) { const reportComment = buildOptimisticAddCommentReportAction(text, undefined, undefined, undefined, undefined, reportID); reportCommentAction = reportComment.reportAction; @@ -659,8 +661,9 @@ function addActions(reportID: string, text = '', file?: FileObject) { // When we are adding an attachment we will call AddAttachment. // It supports sending an attachment with an optional comment and AddComment supports adding a single text comment only. commandName = WRITE_COMMANDS.ADD_ATTACHMENT; - const attachment = buildOptimisticAddCommentReportAction(text, file, undefined, undefined, undefined, reportID); + const attachment = buildOptimisticAddCommentReportAction(text, file, undefined, undefined, undefined, reportID, attachmentID); attachmentAction = attachment.reportAction; + storeAttachment(attachmentID, file.uri as string); } if (text && file) { @@ -671,6 +674,15 @@ function addActions(reportID: string, text = '', file?: FileObject) { commandName = WRITE_COMMANDS.ADD_TEXT_AND_ATTACHMENT; } + const markdownMediaRegex = /<(?:img|video)[^>]*src=['"]([^'"]*)['"]/gi; + const mediaUrls = [...reportCommentText.matchAll(markdownMediaRegex)].map((m) => m[1]); + const reportActionID = file ? attachmentAction?.reportActionID : reportCommentAction?.reportActionID; + const attachments = mediaUrls.map((src, index) => ({uri: src, attachmentID: `${reportActionID}_${++index}`})); + console.log('markdownMediaRegex', attachments); + attachments.forEach((attachment) => { + storeAttachment(attachment.attachmentID, attachment.uri ?? ''); + }); + // Always prefer the file as the last action over text const lastAction = attachmentAction ?? reportCommentAction; const currentTime = DateUtils.getDBTimeWithSkew(); @@ -718,6 +730,10 @@ function addActions(reportID: string, text = '', file?: FileObject) { parameters.isOldDotConciergeChat = true; } + if (file) { + parameters.attachmentID = attachmentID; + } + const optimisticData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, diff --git a/src/setup/index.ts b/src/setup/index.ts index 457008424369f..a2ce5f0986bd5 100644 --- a/src/setup/index.ts +++ b/src/setup/index.ts @@ -1,5 +1,6 @@ import {I18nManager} from 'react-native'; import Onyx from 'react-native-onyx'; +import CacheAPI from '@libs/CacheAPI'; import intlPolyfill from '@libs/IntlPolyfill'; import {setDeviceID} from '@userActions/Device'; import initOnyxDerivedValues from '@userActions/OnyxDerived'; @@ -53,6 +54,8 @@ export default function () { skippableCollectionMemberIDs: CONST.SKIPPABLE_COLLECTION_MEMBER_IDS, }); + CacheAPI.init(['attachments']); + initOnyxDerivedValues(); setDeviceID(); diff --git a/src/types/onyx/Attachment.tsx b/src/types/onyx/Attachment.tsx new file mode 100644 index 0000000000000..c0ca9cde19261 --- /dev/null +++ b/src/types/onyx/Attachment.tsx @@ -0,0 +1,9 @@ +type Attachment = { + attachmentID: string; + + source?: string; + + remoteSource?: string; +}; + +export default Attachment; diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts index 7588584b8322f..4e3aa3f139dd3 100644 --- a/src/types/onyx/index.ts +++ b/src/types/onyx/index.ts @@ -3,6 +3,7 @@ import type Account from './Account'; import type AccountData from './AccountData'; import type {ApprovalWorkflowOnyx} from './ApprovalWorkflow'; import type {AssignCard} from './AssignCard'; +import type Attachment from './Attachment'; import type {BankAccountList} from './BankAccount'; import type BankAccount from './BankAccount'; import type Beta from './Beta'; @@ -122,6 +123,7 @@ import type WalletTransfer from './WalletTransfer'; export type { TryNewDot, + Attachment, Account, AccountData, AssignCard, From fc2382ac95dbdb9c04e843b645305d3f9c67ecc0 Mon Sep 17 00:00:00 2001 From: NJ-2020 Date: Sat, 7 Jun 2025 19:18:48 +0700 Subject: [PATCH 02/99] implement store/caching for web and fix some issues --- .../HTMLRenderers/ImageRenderer.tsx | 6 +-- src/libs/actions/Attachment/index.native.ts | 26 ++++----- src/libs/actions/Attachment/index.ts | 54 +++++++++++++++++-- 3 files changed, 62 insertions(+), 24 deletions(-) diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/ImageRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/ImageRenderer.tsx index 74e328aaa4703..e362ab53a8818 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/ImageRenderer.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/ImageRenderer.tsx @@ -67,9 +67,9 @@ function ImageRenderer({tnode}: ImageRendererProps) { // For other image formats, we retain the thumbnail as is to avoid unnecessary modifications. const processedPreviewSource = typeof previewSource === 'string' ? previewSource.replace(/\.png\.(1024|320)\.jpg$/, '.png') : previewSource; const source = tryResolveUrlFromApiRoot(isAttachmentOrReceipt ? attachmentSourceAttribute : htmlAttribs.src); - const imageSource = getAttachmentSource(attachmentID) || processedPreviewSource; - console.log('imageSource', imageSource); - const isAuthTokenRequired = isLocalFile(imageSource) ? false : isAttachmentOrReceipt; + const imageSource = processedPreviewSource; + // const isAuthTokenRequired = isLocalFile(imageSource) ? false : isAttachmentOrReceipt; + const isAuthTokenRequired = isAttachmentOrReceipt; const alt = htmlAttribs.alt; const imageWidth = (htmlAttribs['data-expensify-width'] && parseInt(htmlAttribs['data-expensify-width'], 10)) || undefined; const imageHeight = (htmlAttribs['data-expensify-height'] && parseInt(htmlAttribs['data-expensify-height'], 10)) || undefined; diff --git a/src/libs/actions/Attachment/index.native.ts b/src/libs/actions/Attachment/index.native.ts index 194fbef498b26..35e91166a2208 100644 --- a/src/libs/actions/Attachment/index.native.ts +++ b/src/libs/actions/Attachment/index.native.ts @@ -1,31 +1,21 @@ import RNFetchBlob from 'react-native-blob-util'; -import RNFS from 'react-native-fs'; import Onyx, {OnyxCollection} from 'react-native-onyx'; -import {FileObject} from '@components/AttachmentModal'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Attachment} from '@src/types/onyx'; let attachments: OnyxCollection | undefined; -let isAttachmentLoaded = false; Onyx.connect({ key: ONYXKEYS.COLLECTION.ATTACHMENT, waitForCollectionCallback: true, - callback: (value) => { - if (!value) { - return; - } - attachments = value; - }, + callback: (value) => (attachments = value ?? {}), }); function storeAttachment(attachmentID: string, uri: string) { if (!attachmentID || !uri) { - console.log('leeh2'); return; } if (uri.startsWith('file://')) { - console.log('leeh44'); Onyx.set(`${ONYXKEYS.COLLECTION.ATTACHMENT}${attachmentID}`, { attachmentID, source: uri, @@ -36,30 +26,34 @@ function storeAttachment(attachmentID: string, uri: string) { const attachment = attachments?.[attachmentID]; if (attachment?.source && attachment.remoteSource === uri) { - console.log('leeh'); return; } - RNFetchBlob.config({fileCache: true}) + RNFetchBlob.config({fileCache: true, path: `${RNFetchBlob.fs.dirs.DocumentDir}/${attachmentID}`}) .fetch('GET', uri) .then((response) => { const filePath = response.path(); - console.log('filePath', filePath); Onyx.set(`${ONYXKEYS.COLLECTION.ATTACHMENT}${attachmentID}`, { attachmentID, source: `file://${filePath}`, }); }) .catch((error) => { - console.log('error', error); + console.error(error); + throw new Error('Failed to store attachment'); }); } -function getAttachmentSource(attachmentID: string) { +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; + } return attachment?.source; } diff --git a/src/libs/actions/Attachment/index.ts b/src/libs/actions/Attachment/index.ts index 3d5ec319923b4..6a1fdd26bf890 100644 --- a/src/libs/actions/Attachment/index.ts +++ b/src/libs/actions/Attachment/index.ts @@ -1,5 +1,6 @@ import Onyx, {OnyxCollection} from 'react-native-onyx'; -import {FileObject} from '@components/AttachmentModal'; +import CacheAPI from '@libs/CacheAPI'; +import {isLocalFile} from '@libs/fileDownload/FileUtils'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Attachment} from '@src/types/onyx'; @@ -7,13 +8,56 @@ let attachments: OnyxCollection | undefined; Onyx.connect({ key: ONYXKEYS.COLLECTION.ATTACHMENT, waitForCollectionCallback: true, - callback: (value) => (attachments = value), + callback: (value) => (attachments = value ?? {}), }); -function storeAttachment(attachmentID: string, uri: string) {} +function storeAttachment(attachmentID: string, uri: string) { + if (!attachmentID || !uri) { + return; + } + fetch(uri) + .then((response) => { + if (!response.ok) { + throw new Error('Failed to store attachment'); + } + CacheAPI.put('attachments', attachmentID, response); + Onyx.set(`${ONYXKEYS.COLLECTION.ATTACHMENT}${attachmentID}`, { + attachmentID, + remoteSource: isLocalFile(uri) ? '' : uri, + }); + }) + .catch((error) => { + console.error(error); + throw new Error('Failed to store attachment'); + }); +} + +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; + } -function getAttachmentSource(attachmentID: string) { - return ''; + return CacheAPI.get('attachments', attachmentID)?.then((response) => { + if (!response) { + throw new Error('Failed to get attachment'); + } + return response + .blob() + .then((attachment) => { + const source = URL.createObjectURL(attachment); + return source; + }) + .catch((error) => { + console.error(error); + throw new Error('Failed to get attachment'); + }); + }); } export {storeAttachment, getAttachmentSource}; From 79f2ad2d4ff7238db452b9176373f801f860c90c Mon Sep 17 00:00:00 2001 From: NJ-2020 Date: Sat, 7 Jun 2025 19:27:13 +0700 Subject: [PATCH 03/99] remove unnecessary code --- src/components/AttachmentPicker/index.native.tsx | 2 +- src/libs/actions/Report.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/AttachmentPicker/index.native.tsx b/src/components/AttachmentPicker/index.native.tsx index f42edafd74e67..8bac7836f9d2c 100644 --- a/src/components/AttachmentPicker/index.native.tsx +++ b/src/components/AttachmentPicker/index.native.tsx @@ -221,7 +221,7 @@ function AttachmentPicker({ if (localCopy.status !== 'success') { throw new Error("Couldn't create local file copy"); } - console.log('picked files', localCopy.localUri); + return { name: file.name, uri: localCopy.localUri, diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 646c75f05580d..d740771a09538 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -678,7 +678,7 @@ function addActions(reportID: string, text = '', file?: FileObject) { const mediaUrls = [...reportCommentText.matchAll(markdownMediaRegex)].map((m) => m[1]); const reportActionID = file ? attachmentAction?.reportActionID : reportCommentAction?.reportActionID; const attachments = mediaUrls.map((src, index) => ({uri: src, attachmentID: `${reportActionID}_${++index}`})); - console.log('markdownMediaRegex', attachments); + attachments.forEach((attachment) => { storeAttachment(attachment.attachmentID, attachment.uri ?? ''); }); From 11b7669c7626e71b8eae94dfdaa5bb9edbf757e3 Mon Sep 17 00:00:00 2001 From: NJ-2020 Date: Sat, 7 Jun 2025 19:49:45 +0700 Subject: [PATCH 04/99] fix eslint errors --- .../HTMLEngineProvider/HTMLRenderers/ImageRenderer.tsx | 5 ++--- src/libs/CacheAPI/index.native.ts | 10 +++++----- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/ImageRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/ImageRenderer.tsx index e362ab53a8818..24cd22ffb32a6 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/ImageRenderer.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/ImageRenderer.tsx @@ -1,4 +1,4 @@ -import React, {memo, useEffect} from 'react'; +import React, {memo} from 'react'; import {useOnyx} from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx'; import type {CustomRendererProps, TBlock} from 'react-native-render-html'; @@ -12,8 +12,7 @@ import ThumbnailImage from '@components/ThumbnailImage'; import useLocalize from '@hooks/useLocalize'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; -import {getAttachmentSource} from '@libs/actions/Attachment'; -import {getFileName, getFileType, isLocalFile, splitExtensionFromFileName} from '@libs/fileDownload/FileUtils'; +import {getFileName, getFileType, splitExtensionFromFileName} from '@libs/fileDownload/FileUtils'; import Navigation from '@libs/Navigation/Navigation'; import {isArchivedNonExpenseReport} from '@libs/ReportUtils'; import tryResolveUrlFromApiRoot from '@libs/tryResolveUrlFromApiRoot'; diff --git a/src/libs/CacheAPI/index.native.ts b/src/libs/CacheAPI/index.native.ts index d77be11595e31..4b1ba2310b0f9 100644 --- a/src/libs/CacheAPI/index.native.ts +++ b/src/libs/CacheAPI/index.native.ts @@ -1,8 +1,8 @@ -function init(keys: string[]) {} -function put(cacheName: string, key: string, value: Response) {} -function get(cacheName: string, key: string) {} -function remove(cacheName: string, key: string) {} -function clear(cacheName: string) {} +function init() {} +function put() {} +function get() {} +function remove() {} +function clear() {} export default { init, From 78aaa7b0a8b2fbc9c7337eddbc94ef0ab7c15d1c Mon Sep 17 00:00:00 2001 From: NJ-2020 Date: Sun, 8 Jun 2025 10:43:05 +0700 Subject: [PATCH 05/99] fix: eslint errors & store file path for native --- .../AttachmentPicker/index.native.tsx | 60 +++++++++++++------ src/libs/CacheAPI/index.ts | 3 +- src/libs/ReportActionsUtils.ts | 2 +- src/libs/actions/Attachment/index.native.ts | 13 +++- 4 files changed, 56 insertions(+), 22 deletions(-) diff --git a/src/components/AttachmentPicker/index.native.tsx b/src/components/AttachmentPicker/index.native.tsx index 8bac7836f9d2c..a634f97e550c9 100644 --- a/src/components/AttachmentPicker/index.native.tsx +++ b/src/components/AttachmentPicker/index.native.tsx @@ -135,7 +135,7 @@ function AttachmentPicker({ const showImagePicker = useCallback( (imagePickerFunc: (options: CameraOptions, callback: Callback) => Promise): Promise => new Promise((resolve, reject) => { - imagePickerFunc(getImagePickerOptions(type, fileLimit), (response: ImagePickerResponse) => { + imagePickerFunc(getImagePickerOptions(type, fileLimit), async (response: ImagePickerResponse) => { if (response.didCancel) { // When the user cancelled resolve with no attachment return resolve(); @@ -153,9 +153,36 @@ function AttachmentPicker({ return reject(new Error(`Error during attachment selection: ${response.errorMessage}`)); } - const targetAsset = response.assets?.[0]; - const targetAssetUri = targetAsset?.uri; + 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![index], + uri: localCopy.localUri, + }; + }); + + const targetAsset = assets?.[0]; + const targetAssetUri = targetAsset?.uri; + console.log('targetAssetUri', { + targetAssetUri, + assets, + localCopies, + }); if (!targetAssetUri) { return resolve(); } @@ -185,12 +212,12 @@ function AttachmentPicker({ }) .catch((err) => reject(err)); } else { - return resolve(response.assets); + return resolve(assets); } }) .catch((err) => reject(err)); } else { - return resolve(response.assets); + return resolve(assets); } }); }), @@ -206,27 +233,26 @@ function AttachmentPicker({ allowMultiSelection: fileLimit !== 1, }); + const filesToCopy = pickedFiles.map((file) => ({ + uri: file.uri, + fileName: file.name ?? '', + })) as [FileToCopy, ...FileToCopy[]]; + const localCopies = await keepLocalCopy({ - files: pickedFiles.map((file) => { - return { - uri: file.uri, - fileName: file.name ?? '', - }; - }) as [FileToCopy, ...FileToCopy[]], + files: filesToCopy, destination: 'documentDirectory', }); - return pickedFiles.map((file, index) => { - const localCopy = localCopies[index]; + return localCopies.map((localCopy, index) => { if (localCopy.status !== 'success') { - throw new Error("Couldn't create local file copy"); + throw new Error(`Failed to create local copy for file ${index + 1}: ${localCopy.copyError ?? 'Unknown error'}`); } return { - name: file.name, + name: pickedFiles[index].name, uri: localCopy.localUri, - size: file.size, - type: file.type, + size: pickedFiles[index].size, + type: pickedFiles[index].type, }; }); }, [fileLimit, type]); diff --git a/src/libs/CacheAPI/index.ts b/src/libs/CacheAPI/index.ts index a9bf774c6b8b1..98f3c02a6d754 100644 --- a/src/libs/CacheAPI/index.ts +++ b/src/libs/CacheAPI/index.ts @@ -10,9 +10,8 @@ function init(keys: string[]) { caches.has(key).then((isExist) => { if (isExist) { return; - } else { - caches.open(key); } + caches.open(key); }); }); } diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 5dfcab30754bd..9c6bf6f73539e 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -161,7 +161,7 @@ function isDeletedAction(reportAction: OnyxInputOrEntry { const filePath = response.path(); + console.log(`markdown_link_attachment_${attachmentID}`, { + attachmentID, + source: filePath, + }); Onyx.set(`${ONYXKEYS.COLLECTION.ATTACHMENT}${attachmentID}`, { attachmentID, source: `file://${filePath}`, + remoteSource: uri, }); }) .catch((error) => { From 6ba658062d8d4096847e15f425ebacbd88fabc5f Mon Sep 17 00:00:00 2001 From: NJ-2020 Date: Sun, 8 Jun 2025 16:26:12 +0700 Subject: [PATCH 06/99] fix: remove attachment when deleting comment, improvements, eslint errors, and fixing some issues --- src/CONST.ts | 9 ++- .../BaseAnchorForAttachmentsOnly.tsx | 2 +- src/libs/CacheAPI/index.ts | 19 +++++- src/libs/ReportActionsUtils.ts | 6 +- src/libs/ReportUtils.ts | 2 +- src/libs/actions/Attachment/index.native.ts | 9 ++- src/libs/actions/Attachment/index.ts | 22 ++++--- src/libs/actions/Report.ts | 58 +++++++++++++++++-- src/libs/actions/Session/index.ts | 2 + src/setup/index.ts | 4 +- 10 files changed, 110 insertions(+), 23 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index d1bb5b9724909..9b26608fda88f 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -3589,7 +3589,10 @@ const CONST = { AFTER_FIRST_LINE_BREAK: /\n.*/g, LINE_BREAK: /\r\n|\r|\n|\u2028/g, CODE_2FA: /^\d{6}$/, - ATTACHMENT_ID: /chat-attachments\/(\d+)/, + 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, HAS_COLON_ONLY_AT_THE_BEGINNING: /^:[^:]+$/, HAS_AT_MOST_TWO_AT_SIGNS: /^@[^@]*@?[^@]*$/, EMPTY_COMMENT: /^(\s)*$/, @@ -6475,6 +6478,10 @@ const CONST = { BOOK_MEETING_LINK: 'https://calendly.com/d/cqsm-2gm-fxr/expensify-product-team', }, + CACHE_API_KEYS: { + ATTACHMENTS: 'attachments', + }, + SESSION_STORAGE_KEYS: { INITIAL_URL: 'INITIAL_URL', ACTIVE_WORKSPACE_ID: 'ACTIVE_WORKSPACE_ID', diff --git a/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.tsx b/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.tsx index 57213dc1d3d26..4069debe6da65 100644 --- a/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.tsx +++ b/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.tsx @@ -24,7 +24,7 @@ type BaseAnchorForAttachmentsOnlyProps = AnchorForAttachmentsOnlyProps & { function BaseAnchorForAttachmentsOnly({style, source = '', displayName = '', onPressIn, onPressOut, isDeleted}: BaseAnchorForAttachmentsOnlyProps) { const sourceURLWithAuth = addEncryptedAuthTokenToURL(source); - const sourceID = (source.match(CONST.REGEX.ATTACHMENT_ID) ?? [])[1]; + const sourceID = (source.match(CONST.REGEX.ATTACHMENT_SOURCE_ID) ?? [])[1]; const [download] = useOnyx(`${ONYXKEYS.COLLECTION.DOWNLOAD}${sourceID}`); diff --git a/src/libs/CacheAPI/index.ts b/src/libs/CacheAPI/index.ts index 98f3c02a6d754..1584df03a90ac 100644 --- a/src/libs/CacheAPI/index.ts +++ b/src/libs/CacheAPI/index.ts @@ -1,4 +1,7 @@ function init(keys: string[]) { + if (!keys || keys.length === 0) { + return; + } // Exit early if the Cache API is not supported in the current browser. if (!('caches' in window)) { throw new Error('Cache API is not supported'); @@ -39,11 +42,21 @@ function remove(cacheName: string, key: string) { cache.delete(key); }); } -function clear(cacheName: string) { - if (!cacheName) { +function clear(keys: string[]) { + if (!keys || keys.length === 0) { return; } - caches.delete(cacheName); + keys.forEach((key) => { + if (!key) { + return; + } + caches.has(key).then((isExist) => { + if (isExist) { + return; + } + caches.delete(key); + }); + }); } export default { init, diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 9c6bf6f73539e..93a6dc38cc554 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -21,6 +21,7 @@ import {isEmptyObject} from '@src/types/utils/EmptyObject'; import {convertToDisplayString} from './CurrencyUtils'; import DateUtils from './DateUtils'; import {getEnvironmentURL} from './Environment/Environment'; +import getAttachmentDetails from './fileDownload/getAttachmentDetails'; import getBase62ReportID from './getBase62ReportID'; import {isReportMessageAttachment} from './isReportMessageAttachment'; import {toLocaleOrdinal} from './LocaleDigitUtils'; @@ -159,9 +160,8 @@ function isDeletedAction(reportAction: OnyxInputOrEntry): str const message = Array.isArray(reportAction?.message) ? (reportAction?.message?.at(-1) ?? null) : (reportAction?.message ?? null); const html = message?.html ?? ''; const {sourceURL} = getAttachmentDetails(html); - const sourceID = (sourceURL?.match(CONST.REGEX.ATTACHMENT_ID) ?? [])[1]; + const sourceID = (sourceURL?.match(CONST.REGEX.ATTACHMENT_SOURCE_ID) ?? [])[1]; return sourceID; } diff --git a/src/libs/actions/Attachment/index.native.ts b/src/libs/actions/Attachment/index.native.ts index f2b9ff5db8435..c539d8b718fd4 100644 --- a/src/libs/actions/Attachment/index.native.ts +++ b/src/libs/actions/Attachment/index.native.ts @@ -66,4 +66,11 @@ function getAttachmentSource(attachmentID: string, currentSource: string) { return attachment?.source; } -export {storeAttachment, getAttachmentSource}; +function deleteAttachment(attachmentID: string) { + if (!attachmentID) { + return; + } + Onyx.set(`${ONYXKEYS.COLLECTION.ATTACHMENT}${attachmentID}`, null); +} + +export {storeAttachment, getAttachmentSource, deleteAttachment}; diff --git a/src/libs/actions/Attachment/index.ts b/src/libs/actions/Attachment/index.ts index 6a1fdd26bf890..04d0db5695801 100644 --- a/src/libs/actions/Attachment/index.ts +++ b/src/libs/actions/Attachment/index.ts @@ -1,6 +1,7 @@ import Onyx, {OnyxCollection} from 'react-native-onyx'; import CacheAPI from '@libs/CacheAPI'; import {isLocalFile} from '@libs/fileDownload/FileUtils'; +import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Attachment} from '@src/types/onyx'; @@ -20,14 +21,13 @@ function storeAttachment(attachmentID: string, uri: string) { if (!response.ok) { throw new Error('Failed to store attachment'); } - CacheAPI.put('attachments', attachmentID, response); + CacheAPI.put(CONST.CACHE_API_KEYS.ATTACHMENTS, attachmentID, response); Onyx.set(`${ONYXKEYS.COLLECTION.ATTACHMENT}${attachmentID}`, { attachmentID, remoteSource: isLocalFile(uri) ? '' : uri, }); }) - .catch((error) => { - console.error(error); + .catch(() => { throw new Error('Failed to store attachment'); }); } @@ -43,7 +43,7 @@ function getAttachmentSource(attachmentID: string, currentSource: string) { return currentSource; } - return CacheAPI.get('attachments', attachmentID)?.then((response) => { + return CacheAPI.get(CONST.CACHE_API_KEYS.ATTACHMENTS, attachmentID)?.then((response) => { if (!response) { throw new Error('Failed to get attachment'); } @@ -53,11 +53,19 @@ function getAttachmentSource(attachmentID: string, currentSource: string) { const source = URL.createObjectURL(attachment); return source; }) - .catch((error) => { - console.error(error); + .catch(() => { throw new Error('Failed to get attachment'); }); }); } -export {storeAttachment, getAttachmentSource}; +function deleteAttachment(attachmentID: string) { + if (!attachmentID) { + return; + } + + Onyx.set(`${ONYXKEYS.COLLECTION.ATTACHMENT}${attachmentID}`, null); + CacheAPI.remove(CONST.CACHE_API_KEYS.ATTACHMENTS, attachmentID); +} + +export {storeAttachment, getAttachmentSource, deleteAttachment}; diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index d740771a09538..5d083e404bd21 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -187,7 +187,7 @@ import type {ConnectionName} from '@src/types/onyx/Policy'; import type {NotificationPreference, Participants, Participant as ReportParticipant, RoomVisibility, WriteCapability} from '@src/types/onyx/Report'; import type {Message, ReportActions} from '@src/types/onyx/ReportAction'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; -import {storeAttachment} from './Attachment'; +import {deleteAttachment, storeAttachment} from './Attachment'; import {clearByKey} from './CachedPDFPaths'; import {setDownload} from './Download'; import {close} from './Modal'; @@ -674,10 +674,31 @@ function addActions(reportID: string, text = '', file?: FileObject) { commandName = WRITE_COMMANDS.ADD_TEXT_AND_ATTACHMENT; } - const markdownMediaRegex = /<(?:img|video)[^>]*src=['"]([^'"]*)['"]/gi; - const mediaUrls = [...reportCommentText.matchAll(markdownMediaRegex)].map((m) => m[1]); + // Store all markdown text attachments i.e `![](https://images.unsplash.com/...)` const reportActionID = file ? attachmentAction?.reportActionID : reportCommentAction?.reportActionID; - const attachments = mediaUrls.map((src, index) => ({uri: src, attachmentID: `${reportActionID}_${++index}`})); + const attachmentRegex = CONST.REGEX.ATTACHMENT; + const attachmentTags = [...reportCommentText.matchAll(attachmentRegex)]; + console.log('attachmentTags', reportCommentText, attachmentTags); + const attachments = attachmentTags.flatMap((htmlTag, index) => { + const tag = htmlTag[0]; + // [2] means the exact value, in this case source url and attachment id of the attachment tag + const source = tag.match(CONST.REGEX.ATTACHMENT_SOURCE)?.[2]; + const attachmentID = tag.match(CONST.REGEX.ATTACHMENT_ID)?.[2]; + console.log('details', { + tag, + source, + attachmentID, + }); + + if (!source) { + return []; + } + + return { + uri: source, + attachmentID: attachmentID || `${reportActionID}_${++index}`, + }; + }); attachments.forEach((attachment) => { storeAttachment(attachment.attachmentID, attachment.uri ?? ''); @@ -717,7 +738,7 @@ function addActions(reportID: string, text = '', file?: FileObject) { } const parameters: AddCommentOrAttachmentParams = { reportID, - reportActionID: file ? attachmentAction?.reportActionID : reportCommentAction?.reportActionID, + reportActionID, commentReportActionID: file && reportCommentAction ? reportCommentAction.reportActionID : null, reportComment: reportCommentText, file, @@ -1866,6 +1887,33 @@ function deleteReportComment(reportID: string | undefined, reportAction: ReportA return; } + if (Array.isArray(reportAction.message) && reportAction.message.length > 0) { + reportAction.message.forEach((message) => { + const reportCommentText = message?.html ?? ''; + + const attachmentTags = [...reportCommentText.matchAll(CONST.REGEX.ATTACHMENT)]; + const attachments = attachmentTags.flatMap((htmlTag, index) => { + const tag = htmlTag[0]; + // [2] means the exact value, in this case source url and attachment id of the attachment tag + const source = tag.match(CONST.REGEX.ATTACHMENT_SOURCE)?.[2]; + const attachmentID = tag.match(CONST.REGEX.ATTACHMENT_ID)?.[2]; + + if (!source) { + return []; + } + + return { + uri: source, + attachmentID: attachmentID || `${reportActionID}_${++index}`, + }; + }); + + attachments.forEach((attachment) => { + deleteAttachment(attachment.attachmentID); + }); + }); + } + const isDeletedParentAction = ReportActionsUtils.isThreadParentMessage(reportAction, reportID); const deletedMessage: Message[] = [ { diff --git a/src/libs/actions/Session/index.ts b/src/libs/actions/Session/index.ts index eccda3b31ebf4..612c4e31272fc 100644 --- a/src/libs/actions/Session/index.ts +++ b/src/libs/actions/Session/index.ts @@ -27,6 +27,7 @@ import type SignInUserParams from '@libs/API/parameters/SignInUserParams'; import {READ_COMMANDS, SIDE_EFFECT_REQUEST_COMMANDS, WRITE_COMMANDS} from '@libs/API/types'; import asyncOpenURL from '@libs/asyncOpenURL'; import * as Authentication from '@libs/Authentication'; +import CacheAPI from '@libs/CacheAPI'; import * as ErrorUtils from '@libs/ErrorUtils'; import Fullstory from '@libs/Fullstory'; import HttpUtils from '@libs/HttpUtils'; @@ -892,6 +893,7 @@ function cleanupSession() { NetworkConnection.clearReconnectionCallbacks(); SessionUtils.resetDidUserLogInDuringSession(); resetHomeRouteParams(); + CacheAPI.clear([CONST.CACHE_API_KEYS.ATTACHMENTS]); clearCache().then(() => { Log.info('Cleared all cache data', true, {}, true); }); diff --git a/src/setup/index.ts b/src/setup/index.ts index a2ce5f0986bd5..fae8b5912da79 100644 --- a/src/setup/index.ts +++ b/src/setup/index.ts @@ -54,7 +54,9 @@ export default function () { skippableCollectionMemberIDs: CONST.SKIPPABLE_COLLECTION_MEMBER_IDS, }); - CacheAPI.init(['attachments']); + const CacheAPIKeys = Object.values(CONST.CACHE_API_KEYS); + + CacheAPI.init(CacheAPIKeys); initOnyxDerivedValues(); From b6906133b7f7bc179ac930e1745334c22e64911c Mon Sep 17 00:00:00 2001 From: NJ-2020 Date: Sun, 8 Jun 2025 16:33:37 +0700 Subject: [PATCH 07/99] remove unnecessary changes --- .../AttachmentPicker/index.native.tsx | 28 ++++++++----------- src/libs/actions/Attachment/index.native.ts | 11 +------- src/libs/actions/Report.ts | 7 +---- 3 files changed, 14 insertions(+), 32 deletions(-) diff --git a/src/components/AttachmentPicker/index.native.tsx b/src/components/AttachmentPicker/index.native.tsx index a634f97e550c9..aaebf59c75fdc 100644 --- a/src/components/AttachmentPicker/index.native.tsx +++ b/src/components/AttachmentPicker/index.native.tsx @@ -178,11 +178,6 @@ function AttachmentPicker({ const targetAsset = assets?.[0]; const targetAssetUri = targetAsset?.uri; - console.log('targetAssetUri', { - targetAssetUri, - assets, - localCopies, - }); if (!targetAssetUri) { return resolve(); } @@ -233,26 +228,27 @@ function AttachmentPicker({ allowMultiSelection: fileLimit !== 1, }); - const filesToCopy = pickedFiles.map((file) => ({ - uri: file.uri, - fileName: file.name ?? '', - })) as [FileToCopy, ...FileToCopy[]]; - const localCopies = await keepLocalCopy({ - files: filesToCopy, + files: pickedFiles.map((file) => { + return { + uri: file.uri, + fileName: file.name ?? '', + }; + }) as [FileToCopy, ...FileToCopy[]], destination: 'documentDirectory', }); - return localCopies.map((localCopy, index) => { + return pickedFiles.map((file, index) => { + const localCopy = localCopies[index]; if (localCopy.status !== 'success') { - throw new Error(`Failed to create local copy for file ${index + 1}: ${localCopy.copyError ?? 'Unknown error'}`); + throw new Error("Couldn't create local file copy"); } return { - name: pickedFiles[index].name, + name: file.name, uri: localCopy.localUri, - size: pickedFiles[index].size, - type: pickedFiles[index].type, + size: file.size, + type: file.type, }; }); }, [fileLimit, type]); diff --git a/src/libs/actions/Attachment/index.native.ts b/src/libs/actions/Attachment/index.native.ts index c539d8b718fd4..d08204a3d7076 100644 --- a/src/libs/actions/Attachment/index.native.ts +++ b/src/libs/actions/Attachment/index.native.ts @@ -16,10 +16,6 @@ function storeAttachment(attachmentID: string, uri: string) { return; } if (uri.startsWith('file://')) { - console.log(`attachment_${attachmentID}`, { - attachmentID, - source: uri, - }); Onyx.set(`${ONYXKEYS.COLLECTION.ATTACHMENT}${attachmentID}`, { attachmentID, source: uri, @@ -37,18 +33,13 @@ function storeAttachment(attachmentID: string, uri: string) { .fetch('GET', uri) .then((response) => { const filePath = response.path(); - console.log(`markdown_link_attachment_${attachmentID}`, { - attachmentID, - source: filePath, - }); Onyx.set(`${ONYXKEYS.COLLECTION.ATTACHMENT}${attachmentID}`, { attachmentID, source: `file://${filePath}`, remoteSource: uri, }); }) - .catch((error) => { - console.error(error); + .catch(() => { throw new Error('Failed to store attachment'); }); } diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 5d083e404bd21..d7d0d44a526ed 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -678,17 +678,12 @@ function addActions(reportID: string, text = '', file?: FileObject) { const reportActionID = file ? attachmentAction?.reportActionID : reportCommentAction?.reportActionID; const attachmentRegex = CONST.REGEX.ATTACHMENT; const attachmentTags = [...reportCommentText.matchAll(attachmentRegex)]; - console.log('attachmentTags', reportCommentText, attachmentTags); + const attachments = attachmentTags.flatMap((htmlTag, index) => { const tag = htmlTag[0]; // [2] means the exact value, in this case source url and attachment id of the attachment tag const source = tag.match(CONST.REGEX.ATTACHMENT_SOURCE)?.[2]; const attachmentID = tag.match(CONST.REGEX.ATTACHMENT_ID)?.[2]; - console.log('details', { - tag, - source, - attachmentID, - }); if (!source) { return []; From ad8e005c6ffa69652a23c55bcdefeb17922617cb Mon Sep 17 00:00:00 2001 From: NJ-2020 Date: Sun, 8 Jun 2025 16:41:13 +0700 Subject: [PATCH 08/99] fix: eslint errors --- .../BaseAnchorForAttachmentsOnly.tsx | 16 +++++++++------- src/components/AttachmentPicker/index.native.tsx | 2 +- src/libs/actions/Attachment/index.ts | 7 ++++--- 3 files changed, 14 insertions(+), 11 deletions(-) diff --git a/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.tsx b/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.tsx index 4069debe6da65..ea4080cac8c90 100644 --- a/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.tsx +++ b/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.tsx @@ -6,10 +6,10 @@ import {ShowContextMenuContext, showContextMenuForReport} from '@components/Show import useNetwork from '@hooks/useNetwork'; import useThemeStyles from '@hooks/useThemeStyles'; import addEncryptedAuthTokenToURL from '@libs/addEncryptedAuthTokenToURL'; -import * as Browser from '@libs/Browser'; +import {isMobileSafari} from '@libs/Browser'; import fileDownload from '@libs/fileDownload'; -import * as ReportUtils from '@libs/ReportUtils'; -import * as Download from '@userActions/Download'; +import {isArchivedNonExpenseReport} from '@libs/ReportUtils'; +import {setDownload} from '@userActions/Download'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type AnchorForAttachmentsOnlyProps from './types'; @@ -26,7 +26,9 @@ function BaseAnchorForAttachmentsOnly({style, source = '', displayName = '', onP const sourceURLWithAuth = addEncryptedAuthTokenToURL(source); const sourceID = (source.match(CONST.REGEX.ATTACHMENT_SOURCE_ID) ?? [])[1]; - const [download] = useOnyx(`${ONYXKEYS.COLLECTION.DOWNLOAD}${sourceID}`); + const [download] = useOnyx(`${ONYXKEYS.COLLECTION.DOWNLOAD}${sourceID}`, { + canBeMissing: true, + }); const {isOffline} = useNetwork(); const styles = useThemeStyles(); @@ -42,8 +44,8 @@ function BaseAnchorForAttachmentsOnly({style, source = '', displayName = '', onP if (isDownloading || isOffline || !sourceID) { return; } - Download.setDownload(sourceID, true); - fileDownload(sourceURLWithAuth, displayName, '', Browser.isMobileSafari()).then(() => Download.setDownload(sourceID, false)); + setDownload(sourceID, true); + fileDownload(sourceURLWithAuth, displayName, '', isMobileSafari()).then(() => setDownload(sourceID, false)); }} onPressIn={onPressIn} onPressOut={onPressOut} @@ -51,7 +53,7 @@ function BaseAnchorForAttachmentsOnly({style, source = '', displayName = '', onP if (isDisabled || !shouldDisplayContextMenu) { return; } - showContextMenuForReport(event, anchor, report?.reportID, action, checkIfContextMenuActive, ReportUtils.isArchivedNonExpenseReport(report, reportNameValuePairs)); + showContextMenuForReport(event, anchor, report?.reportID, action, checkIfContextMenuActive, isArchivedNonExpenseReport(report, reportNameValuePairs)); }} shouldUseHapticsOnLongPress accessibilityLabel={displayName} diff --git a/src/components/AttachmentPicker/index.native.tsx b/src/components/AttachmentPicker/index.native.tsx index aaebf59c75fdc..4e1129d0291f7 100644 --- a/src/components/AttachmentPicker/index.native.tsx +++ b/src/components/AttachmentPicker/index.native.tsx @@ -176,7 +176,7 @@ function AttachmentPicker({ }; }); - const targetAsset = assets?.[0]; + const targetAsset = assets.at(0); const targetAssetUri = targetAsset?.uri; if (!targetAssetUri) { return resolve(); diff --git a/src/libs/actions/Attachment/index.ts b/src/libs/actions/Attachment/index.ts index 04d0db5695801..01d1e704b154e 100644 --- a/src/libs/actions/Attachment/index.ts +++ b/src/libs/actions/Attachment/index.ts @@ -1,4 +1,5 @@ -import Onyx, {OnyxCollection} from 'react-native-onyx'; +import Onyx from 'react-native-onyx'; +import type {OnyxCollection} from 'react-native-onyx'; import CacheAPI from '@libs/CacheAPI'; import {isLocalFile} from '@libs/fileDownload/FileUtils'; import CONST from '@src/CONST'; @@ -49,8 +50,8 @@ function getAttachmentSource(attachmentID: string, currentSource: string) { } return response .blob() - .then((attachment) => { - const source = URL.createObjectURL(attachment); + .then((attachmentFile) => { + const source = URL.createObjectURL(attachmentFile); return source; }) .catch(() => { From 041bcbc0f4be199087a8293a442d3185e3f7747a Mon Sep 17 00:00:00 2001 From: NJ-2020 Date: Sun, 8 Jun 2025 16:57:58 +0700 Subject: [PATCH 09/99] fix: eslint errors --- src/components/AttachmentPicker/index.native.tsx | 2 +- src/libs/actions/Report.ts | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/AttachmentPicker/index.native.tsx b/src/components/AttachmentPicker/index.native.tsx index 4e1129d0291f7..af35b27845ac2 100644 --- a/src/components/AttachmentPicker/index.native.tsx +++ b/src/components/AttachmentPicker/index.native.tsx @@ -171,7 +171,7 @@ function AttachmentPicker({ } return { - ...response.assets![index], + ...(response.assets?.at(index) ?? {}), uri: localCopy.localUri, }; }); diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index d7d0d44a526ed..f02dfb2192e8d 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -663,7 +663,7 @@ function addActions(reportID: string, text = '', file?: FileObject) { commandName = WRITE_COMMANDS.ADD_ATTACHMENT; const attachment = buildOptimisticAddCommentReportAction(text, file, undefined, undefined, undefined, reportID, attachmentID); attachmentAction = attachment.reportAction; - storeAttachment(attachmentID, file.uri as string); + storeAttachment(attachmentID, file.uri ?? ''); } if (text && file) { @@ -683,7 +683,7 @@ function addActions(reportID: string, text = '', file?: FileObject) { const tag = htmlTag[0]; // [2] means the exact value, in this case source url and attachment id of the attachment tag const source = tag.match(CONST.REGEX.ATTACHMENT_SOURCE)?.[2]; - const attachmentID = tag.match(CONST.REGEX.ATTACHMENT_ID)?.[2]; + const dataAttachmentID = tag.match(CONST.REGEX.ATTACHMENT_ID)?.[2]; if (!source) { return []; @@ -691,7 +691,7 @@ function addActions(reportID: string, text = '', file?: FileObject) { return { uri: source, - attachmentID: attachmentID || `${reportActionID}_${++index}`, + attachmentID: dataAttachmentID ?? `${reportActionID}_${index + 1}`, }; }); @@ -1899,7 +1899,7 @@ function deleteReportComment(reportID: string | undefined, reportAction: ReportA return { uri: source, - attachmentID: attachmentID || `${reportActionID}_${++index}`, + attachmentID: attachmentID ?? `${reportActionID}_${index + 1}`, }; }); From 393ad06a35073b50591b513a2caa30345087102a Mon Sep 17 00:00:00 2001 From: NJ-2020 Date: Sun, 8 Jun 2025 17:14:54 +0700 Subject: [PATCH 10/99] fix: attachmentID is returning undefined --- src/libs/actions/Report.ts | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index f02dfb2192e8d..e781eb357da32 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -661,7 +661,7 @@ function addActions(reportID: string, text = '', file?: FileObject) { // When we are adding an attachment we will call AddAttachment. // It supports sending an attachment with an optional comment and AddComment supports adding a single text comment only. commandName = WRITE_COMMANDS.ADD_ATTACHMENT; - const attachment = buildOptimisticAddCommentReportAction(text, file, undefined, undefined, undefined, reportID, attachmentID); + const attachment = buildOptimisticAddCommentReportAction(text, file, undefined, undefined, undefined, reportID, undefined, attachmentID); attachmentAction = attachment.reportAction; storeAttachment(attachmentID, file.uri ?? ''); } @@ -1881,24 +1881,25 @@ function deleteReportComment(reportID: string | undefined, reportAction: ReportA if (!reportActionID || !originalReportID || !reportID) { return; } - + console.log('delete', reportAction, reportAction.message); if (Array.isArray(reportAction.message) && reportAction.message.length > 0) { reportAction.message.forEach((message) => { const reportCommentText = message?.html ?? ''; const attachmentTags = [...reportCommentText.matchAll(CONST.REGEX.ATTACHMENT)]; + console.log('report action chat', reportCommentText, attachmentTags); + const attachments = attachmentTags.flatMap((htmlTag, index) => { const tag = htmlTag[0]; - // [2] means the exact value, in this case source url and attachment id of the attachment tag - const source = tag.match(CONST.REGEX.ATTACHMENT_SOURCE)?.[2]; - const attachmentID = tag.match(CONST.REGEX.ATTACHMENT_ID)?.[2]; - if (!source) { - return []; - } + const attachmentID = tag.match(CONST.REGEX.ATTACHMENT_ID)?.[2]; // [2] means the exact value of the attachment id of the attachment tag + console.log('detail', { + tag, + + attachmentID, + }); return { - uri: source, attachmentID: attachmentID ?? `${reportActionID}_${index + 1}`, }; }); From 6b23b6f28a88c15e8791d7eff62b55bcff75e034 Mon Sep 17 00:00:00 2001 From: NJ-2020 Date: Sun, 8 Jun 2025 19:02:52 +0700 Subject: [PATCH 11/99] fix: remove unnecessary code --- src/libs/actions/Report.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index e781eb357da32..0ad1834be5def 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -1881,23 +1881,16 @@ function deleteReportComment(reportID: string | undefined, reportAction: ReportA if (!reportActionID || !originalReportID || !reportID) { return; } - console.log('delete', reportAction, reportAction.message); if (Array.isArray(reportAction.message) && reportAction.message.length > 0) { reportAction.message.forEach((message) => { const reportCommentText = message?.html ?? ''; const attachmentTags = [...reportCommentText.matchAll(CONST.REGEX.ATTACHMENT)]; - console.log('report action chat', reportCommentText, attachmentTags); const attachments = attachmentTags.flatMap((htmlTag, index) => { const tag = htmlTag[0]; const attachmentID = tag.match(CONST.REGEX.ATTACHMENT_ID)?.[2]; // [2] means the exact value of the attachment id of the attachment tag - console.log('detail', { - tag, - - attachmentID, - }); return { attachmentID: attachmentID ?? `${reportActionID}_${index + 1}`, From 60ed01900d7355329241826ffee0404a9c79ee3e Mon Sep 17 00:00:00 2001 From: NJ-2020 Date: Tue, 10 Jun 2025 11:05:41 +0700 Subject: [PATCH 12/99] add missing comments --- src/types/onyx/Attachment.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/types/onyx/Attachment.tsx b/src/types/onyx/Attachment.tsx index c0ca9cde19261..76028a8b1b2bf 100644 --- a/src/types/onyx/Attachment.tsx +++ b/src/types/onyx/Attachment.tsx @@ -1,8 +1,11 @@ type Attachment = { + /** Attachment ID of the attachment */ attachmentID: string; + /** Source url of the attachment either can be local or remote url */ source?: string; + /** Remote source url of the attachment */ remoteSource?: string; }; From 1e56909fa8b065f8932352fa231151a076599703 Mon Sep 17 00:00:00 2001 From: NJ-2020 Date: Tue, 10 Jun 2025 16:19:22 +0700 Subject: [PATCH 13/99] revert showImagePicker changes --- .../AttachmentPicker/index.native.tsx | 32 +++---------------- 1 file changed, 5 insertions(+), 27 deletions(-) diff --git a/src/components/AttachmentPicker/index.native.tsx b/src/components/AttachmentPicker/index.native.tsx index af35b27845ac2..8bac7836f9d2c 100644 --- a/src/components/AttachmentPicker/index.native.tsx +++ b/src/components/AttachmentPicker/index.native.tsx @@ -135,7 +135,7 @@ function AttachmentPicker({ const showImagePicker = useCallback( (imagePickerFunc: (options: CameraOptions, callback: Callback) => Promise): Promise => new Promise((resolve, reject) => { - imagePickerFunc(getImagePickerOptions(type, fileLimit), async (response: ImagePickerResponse) => { + imagePickerFunc(getImagePickerOptions(type, fileLimit), (response: ImagePickerResponse) => { if (response.didCancel) { // When the user cancelled resolve with no attachment return resolve(); @@ -153,31 +153,9 @@ function AttachmentPicker({ return reject(new Error(`Error during attachment selection: ${response.errorMessage}`)); } - 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 targetAsset = response.assets?.[0]; const targetAssetUri = targetAsset?.uri; + if (!targetAssetUri) { return resolve(); } @@ -207,12 +185,12 @@ function AttachmentPicker({ }) .catch((err) => reject(err)); } else { - return resolve(assets); + return resolve(response.assets); } }) .catch((err) => reject(err)); } else { - return resolve(assets); + return resolve(response.assets); } }); }), From e395df4373284feaf863619a60d11d0f90062d04 Mon Sep 17 00:00:00 2001 From: NJ-2020 Date: Thu, 12 Jun 2025 10:04:33 +0700 Subject: [PATCH 14/99] feat: add unit test file --- tests/unit/AttachmentStorageTest.ts | 117 ++++++++++++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 tests/unit/AttachmentStorageTest.ts diff --git a/tests/unit/AttachmentStorageTest.ts b/tests/unit/AttachmentStorageTest.ts new file mode 100644 index 0000000000000..a2b4764e6e4e3 --- /dev/null +++ b/tests/unit/AttachmentStorageTest.ts @@ -0,0 +1,117 @@ +import Onyx from 'react-native-onyx'; +import type {OnyxCollection} from 'react-native-onyx'; +import {addAttachment, addComment} from '@libs/actions/Report'; +import {rand64} from '@libs/NumberUtils'; +import type {Attachment} from '@src/types/onyx'; +import ONYXKEYS from '../../src/ONYXKEYS'; +import * as TestHelper from '../utils/TestHelper'; +import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; + +jest.mock('react-native-blob-util', () => ({ + config: jest.fn(() => ({ + fetch: jest.fn(() => + Promise.resolve({ + path: jest.fn(() => '/mocked/path/to/file'), + }), + ), + })), + fs: { + dirs: { + DocumentDir: '/mocked/document/dir', + }, + }, + fetch: jest.fn(() => + Promise.resolve({ + path: jest.fn(() => '/mocked/path/to/file'), + }), + ), +})); + +describe('AttachmentStorage', () => { + beforeAll(() => { + Onyx.init({ + keys: ONYXKEYS, + }); + }); + beforeEach(() => { + global.fetch = TestHelper.getGlobalFetchMock(); + }); + + it('should store for file in Onyx', async () => { + // Mock file data + const reportID = rand64(); + const fileData = { + name: `test.jpg`, + source: 'https://images.unsplash.com/photo-1726066012751-2adfb5485977?w=500&auto=format&fit=crop&q=60&ixlib=rb-4.1.0&ixid=M3wxMjA3fDF8MHxmZWF0dXJlZC1waG90b3MtZmVlZHwxfHx8ZW58MHx8fHx8', + uri: 'https://images.unsplash.com/photo-1726066012751-2adfb5485977?w=500&auto=format&fit=crop&q=60&ixlib=rb-4.1.0&ixid=M3wxMjA3fDF8MHxmZWF0dXJlZC1waG90b3MtZmVlZHwxfHx8ZW58MHx8fHx8', + }; + + // Execute file upload + addAttachment(reportID, fileData); + + await waitForBatchedUpdates(); + + const attachments = await new Promise>((resolve) => { + const connection = Onyx.connect({ + key: ONYXKEYS.COLLECTION.ATTACHMENT, + waitForCollectionCallback: true, + callback: (value) => { + Onyx.disconnect(connection); + resolve(value); + }, + }); + }); + + // const cacheAPIAttachment = await CacheAPI.get(CONST.CACHE_API_KEYS.ATTACHMENTS, attachmentID); + + const attachmentLists = Object.values(attachments ?? {}); + const attachment = attachmentLists.at(0); + const attachmentID = attachment?.attachmentID; + + expect(attachmentID).toBeDefined(); + + // Verify Onyx storage + expect(attachment).toEqual({ + attachmentID, + source: 'file:///mocked/path/to/file', + remoteSource: fileData.uri, + }); + }); + it('should store markdown text link attachments in Onyx', async () => { + // Mock file data + const reportID = rand64(); + const sourceURL = + 'https://images.unsplash.com/photo-1726066012751-2adfb5485977?w=500&auto=format&fit=crop&q=60&ixlib=rb-4.1.0&ixid=M3wxMjA3fDF8MHxmZWF0dXJlZC1waG90b3MtZmVlZHwxfHx8ZW58MHx8fHx8'; + const markdownTextLinkAttachment = `![](${sourceURL})`; + + // Execute file upload + addComment(reportID, markdownTextLinkAttachment); + + await waitForBatchedUpdates(); + + const attachments = await new Promise>((resolve) => { + const connection = Onyx.connect({ + key: ONYXKEYS.COLLECTION.ATTACHMENT, + waitForCollectionCallback: true, + callback: (value) => { + Onyx.disconnect(connection); + resolve(value); + }, + }); + }); + + const attachmentLists = Object.values(attachments ?? {}); + const attachment = attachmentLists.at(0); + const attachmentID = attachment?.attachmentID; + + expect(attachmentID).toBeDefined(); + + // Verify Onyx storage + expect(attachment).toEqual({ + attachmentID, + source: 'file:///mocked/path/to/file', + remoteSource: + 'https://images.unsplash.com/photo-1726066012751-2adfb5485977?w=500&auto=format&fit=crop&q=60&ixlib=rb-4.1.0&ixid=M3wxMjA3fDF8MHxmZWF0dXJlZC1waG90b3MtZmVlZHwxfHx8ZW58MHx8fHx8', + }); + }); +}); From 63ad3f730354fd8ae4d8e165df8923e2ad9c2e60 Mon Sep 17 00:00:00 2001 From: NJ-2020 Date: Thu, 12 Jun 2025 10:18:14 +0700 Subject: [PATCH 15/99] fix some comments and improve the code --- tests/unit/AttachmentStorageTest.ts | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/tests/unit/AttachmentStorageTest.ts b/tests/unit/AttachmentStorageTest.ts index a2b4764e6e4e3..beb42ebb1c18c 100644 --- a/tests/unit/AttachmentStorageTest.ts +++ b/tests/unit/AttachmentStorageTest.ts @@ -28,6 +28,8 @@ jest.mock('react-native-blob-util', () => ({ })); describe('AttachmentStorage', () => { + const reportID = rand64(); + beforeAll(() => { Onyx.init({ keys: ONYXKEYS, @@ -38,15 +40,14 @@ describe('AttachmentStorage', () => { }); it('should store for file in Onyx', async () => { - // Mock file data - const reportID = rand64(); + // Given the attachment data consisting of name, type and uri const fileData = { - name: `test.jpg`, - source: 'https://images.unsplash.com/photo-1726066012751-2adfb5485977?w=500&auto=format&fit=crop&q=60&ixlib=rb-4.1.0&ixid=M3wxMjA3fDF8MHxmZWF0dXJlZC1waG90b3MtZmVlZHwxfHx8ZW58MHx8fHx8', + name: `TEST_ATTACHMENT_FILE`, + type: 'image/jpeg', uri: 'https://images.unsplash.com/photo-1726066012751-2adfb5485977?w=500&auto=format&fit=crop&q=60&ixlib=rb-4.1.0&ixid=M3wxMjA3fDF8MHxmZWF0dXJlZC1waG90b3MtZmVlZHwxfHx8ZW58MHx8fHx8', }; - // Execute file upload + // Then upload the attachment addAttachment(reportID, fileData); await waitForBatchedUpdates(); @@ -62,15 +63,12 @@ describe('AttachmentStorage', () => { }); }); - // const cacheAPIAttachment = await CacheAPI.get(CONST.CACHE_API_KEYS.ATTACHMENTS, attachmentID); - const attachmentLists = Object.values(attachments ?? {}); const attachment = attachmentLists.at(0); const attachmentID = attachment?.attachmentID; + // Then the attachmentID and attachment value should be defined expect(attachmentID).toBeDefined(); - - // Verify Onyx storage expect(attachment).toEqual({ attachmentID, source: 'file:///mocked/path/to/file', @@ -78,13 +76,12 @@ describe('AttachmentStorage', () => { }); }); it('should store markdown text link attachments in Onyx', async () => { - // Mock file data - const reportID = rand64(); + // Given the attachment data consisting of sourceURL and markdown comment text const sourceURL = 'https://images.unsplash.com/photo-1726066012751-2adfb5485977?w=500&auto=format&fit=crop&q=60&ixlib=rb-4.1.0&ixid=M3wxMjA3fDF8MHxmZWF0dXJlZC1waG90b3MtZmVlZHwxfHx8ZW58MHx8fHx8'; const markdownTextLinkAttachment = `![](${sourceURL})`; - // Execute file upload + // Then send the comment addComment(reportID, markdownTextLinkAttachment); await waitForBatchedUpdates(); @@ -104,9 +101,8 @@ describe('AttachmentStorage', () => { const attachment = attachmentLists.at(0); const attachmentID = attachment?.attachmentID; + // Then the attachmentID and attachment value should be defined expect(attachmentID).toBeDefined(); - - // Verify Onyx storage expect(attachment).toEqual({ attachmentID, source: 'file:///mocked/path/to/file', From 648c1965aa1ddabc0f4e8ea2eaebcbe0d599158b Mon Sep 17 00:00:00 2001 From: NJ-2020 Date: Mon, 16 Jun 2025 16:47:30 +0700 Subject: [PATCH 16/99] fix some comments & issues --- src/CONST/index.ts | 25 ++++++++++++------- .../BaseAnchorForAttachmentsOnly.tsx | 2 +- src/libs/CacheAPI/index.ts | 15 +++++------ src/libs/ReportActionsUtils.ts | 4 +-- src/libs/ReportUtils.ts | 2 +- src/libs/actions/Attachment/index.native.ts | 21 +++++----------- src/libs/actions/Attachment/index.ts | 16 +++++------- src/libs/actions/Report.ts | 19 +++++++------- .../report/ContextMenu/ContextMenuActions.tsx | 2 +- .../home/report/PureReportActionItem.tsx | 2 +- src/setup/index.ts | 4 +-- src/types/onyx/Attachment.tsx | 2 +- .../AttachmentTest.ts} | 0 13 files changed, 53 insertions(+), 61 deletions(-) rename tests/{unit/AttachmentStorageTest.ts => actions/AttachmentTest.ts} (100%) diff --git a/src/CONST/index.ts b/src/CONST/index.ts index 22bbde7c9be32..8ac4475ee540c 100755 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -1943,7 +1943,6 @@ const CONST = { YOUR_LOCATION_TEXT: 'Your Location', ATTACHMENT_MESSAGE_TEXT: '[Attachment]', - ATTACHMENT_REGEX: /