-
Notifications
You must be signed in to change notification settings - Fork 4k
fix: Attachment - Edit comment option is shown for .doc file #90203
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
aa1b8bc
f3df7d2
0d4e9d2
21b9a79
d6c224c
d42f03f
b091a7e
fabe04b
791171f
4edca7a
6b0b059
3b2d71d
db4f659
36ddf8e
cc520ca
9dcb9bb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -737,6 +737,7 @@ | |
| "onloaderror", | ||
| "onopentag", | ||
| "onplayerror", | ||
| "ontext", | ||
| "onxy", | ||
| "openxmlformats", | ||
| "ordinality", | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,37 +1,88 @@ | ||
| import {Str} from 'expensify-common'; | ||
| import {Parser as HtmlParser} from 'htmlparser2'; | ||
| import CONST from '@src/CONST'; | ||
| import type {Message} from '@src/types/onyx/ReportAction'; | ||
|
|
||
| const attachmentRegex = new RegExp(` ${CONST.ATTACHMENT_SOURCE_ATTRIBUTE}="(.*)"`, 'i'); | ||
| const ATTACHMENT_TAGS = new Set(['a', 'img', 'video']); | ||
| // Leading space + `="` (not a bare substring) so a URL query param like `?data-expensify-source=` isn't a false positive. | ||
| const ATTACHMENT_SOURCE_TOKEN = ` ${CONST.ATTACHMENT_SOURCE_ATTRIBUTE}="`; | ||
|
|
||
| /** | ||
| * Check whether a report action is Attachment or not. | ||
| * Ignore messages containing [Attachment] as the main content. Attachments are actions with only text as [Attachment]. | ||
| * | ||
| * @param message report action's message as text, html and translationKey | ||
| */ | ||
| function isReportMessageAttachment(message: Message | undefined): boolean { | ||
| if (!message?.text || !message.html) { | ||
| // Keyed by Onyx message identity: re-renders pass the same immutable object, so parse at most once per message. | ||
| const resultCache = new WeakMap<Message, boolean>(); | ||
|
|
||
| function computeIsReportMessageAttachment(html: string, text: string, translationKey: string | undefined): boolean { | ||
| // Optimistic attachment-only messages carry this translationKey; the server drops it after sync. | ||
| if (translationKey === CONST.TRANSLATION_KEYS.ATTACHMENT) { | ||
| return true; | ||
| } | ||
|
|
||
| // Fast path: no source attribute → not an attachment (avoids the parser for most messages). | ||
| if (!html.includes(ATTACHMENT_SOURCE_TOKEN)) { | ||
| return false; | ||
| } | ||
|
|
||
| if (message.translationKey) { | ||
| return message.text === CONST.ATTACHMENT_MESSAGE_TEXT && message.translationKey === CONST.TRANSLATION_KEYS.ATTACHMENT; | ||
|
TaduJR marked this conversation as resolved.
|
||
| // image/video attachment-only keep text "[Attachment]"; only .doc/.pdf (text = "filename URL") need the parser. | ||
| if (text === CONST.ATTACHMENT_MESSAGE_TEXT) { | ||
| return true; | ||
| } | ||
|
|
||
| const hasAttachmentHtml = attachmentRegex.test(message.html); | ||
| let hasAttachment = false; | ||
| let hasOtherContent = false; | ||
| // Depth >0 = inside the attachment's subtree; its inner nodes (filename/markup) aren't user content. | ||
| let depth = 0; | ||
|
|
||
| if (!hasAttachmentHtml) { | ||
| const parser = new HtmlParser({ | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
@MelvinBot suggest changes for code optimization to reduce performance impact. |
||
| ontext: (nodeText) => { | ||
| if (depth > 0 || !nodeText.trim()) { | ||
| return; | ||
| } | ||
| hasOtherContent = true; | ||
| }, | ||
| onopentag: (name, attribs) => { | ||
| if (depth > 0) { | ||
| depth += 1; | ||
| return; | ||
| } | ||
| if (name === 'br') { | ||
| return; | ||
| } | ||
| if (ATTACHMENT_TAGS.has(name) && !!attribs[CONST.ATTACHMENT_SOURCE_ATTRIBUTE]) { | ||
| hasAttachment = true; | ||
| if (name === 'a' || name === 'video') { | ||
| depth = 1; | ||
| } | ||
| return; | ||
| } | ||
| hasOtherContent = true; | ||
| }, | ||
| onclosetag: () => { | ||
| if (depth <= 0) { | ||
| return; | ||
| } | ||
| depth -= 1; | ||
| }, | ||
| }); | ||
| parser.write(html); | ||
| parser.end(); | ||
|
|
||
| return hasAttachment && !hasOtherContent; | ||
| } | ||
|
|
||
| /** | ||
| * Returns true for attachment-only messages (no user-typed text), false for attachment+text and non-attachment messages. | ||
| */ | ||
| function isReportMessageAttachment(message: Message | undefined): boolean { | ||
| if (!message?.text || !message.html) { | ||
| return false; | ||
| } | ||
|
|
||
| const isAttachmentMessageText = message.text === CONST.ATTACHMENT_MESSAGE_TEXT; | ||
|
|
||
| if (isAttachmentMessageText) { | ||
| return true; | ||
| const cached = resultCache.get(message); | ||
| if (cached !== undefined) { | ||
| return cached; | ||
| } | ||
|
|
||
| return Str.isVideo(message.text); | ||
| const result = computeIsReportMessageAttachment(message.html, message.text, message.translationKey); | ||
| resultCache.set(message, result); | ||
| return result; | ||
| } | ||
|
|
||
| // eslint-disable-next-line import/prefer-default-export | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| import {measureFunction} from 'reassure'; | ||
| import type {Message} from '@src/types/onyx/ReportAction'; | ||
| import {isReportMessageAttachment} from '../../src/libs/isReportMessageAttachment'; | ||
|
|
||
| const SAMPLE_MESSAGES: Message[] = [ | ||
| // .doc attachment-only after OpenReport: text = "filename URL" (the parser-bound case) | ||
| { | ||
| text: 'sample.doc https://www.expensify.com/chat-attachments/123/w_abc.doc', | ||
| html: '<a href="https://www.expensify.com/chat-attachments/123/w_abc.doc" data-expensify-source="https://www.expensify.com/chat-attachments/123/w_abc.doc">sample.doc</a>', | ||
| type: '', | ||
| }, | ||
| // image attachment-only (translationKey path) | ||
| { | ||
| text: '[Attachment]', | ||
| html: '<img src="https://www.expensify.com/chat-attachments/123/img.jpg" data-expensify-source="https://www.expensify.com/chat-attachments/123/img.jpg" />', | ||
| type: '', | ||
| translationKey: 'common.attachment', | ||
| }, | ||
| { | ||
| text: 'Here is the file\nsample.pdf https://www.expensify.com/chat-attachments/123/s.pdf', | ||
| html: 'Here is the file<br /><br /><a href="https://www.expensify.com/chat-attachments/123/s.pdf" data-expensify-source="https://www.expensify.com/chat-attachments/123/s.pdf">sample.pdf</a>', | ||
| type: '', | ||
| }, | ||
| {text: 'Just a normal chat message with some words in it', html: 'Just a normal chat message with some words in it', type: ''}, | ||
| {text: 'https://example.com', html: '<a href="https://example.com" data-raw-href="https://example.com" target="_blank" rel="noreferrer noopener">https://example.com</a>', type: ''}, | ||
| ]; | ||
|
|
||
| // One classification pass over a long chat history (called per message on list renders / LHN). | ||
| const LONG_CHAT_HISTORY: Message[] = Array.from({length: 2000}, () => SAMPLE_MESSAGES).flat(); | ||
|
|
||
| test('[isReportMessageAttachment] classify a 10k-message chat history', async () => { | ||
| await measureFunction(() => { | ||
| for (const message of LONG_CHAT_HISTORY) { | ||
| isReportMessageAttachment(message); | ||
| } | ||
| }); | ||
| }); |
Uh oh!
There was an error while loading. Please reload this page.