mirror of
https://github.com/abhinavxd/libredesk.git
synced 2026-09-22 18:43:33 +00:00
hide outlook/hotmail quoted text correctly and fix html email whitespace
This commit is contained in:
@@ -77,7 +77,7 @@
|
|||||||
:html="sanitizedContent"
|
:html="sanitizedContent"
|
||||||
:allowedSchemas="['cid', 'https', 'http', 'mailto']"
|
:allowedSchemas="['cid', 'https', 'http', 'mailto']"
|
||||||
:allowed-css-properties="extendedCssProperties"
|
:allowed-css-properties="extendedCssProperties"
|
||||||
class="mb-1 native-html whitespace-pre-wrap break-words"
|
class="mb-1 native-html break-words"
|
||||||
:class="{ 'mb-3': message.attachments.length > 0 }"
|
:class="{ 'mb-3': message.attachments.length > 0 }"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -205,6 +205,7 @@ import BubbleAttachmentPreview from '@main/features/conversation/message/attachm
|
|||||||
import MessageEnvelope from './MessageEnvelope.vue'
|
import MessageEnvelope from './MessageEnvelope.vue'
|
||||||
import CSATResponseDisplay from './CSATResponseDisplay.vue'
|
import CSATResponseDisplay from './CSATResponseDisplay.vue'
|
||||||
import api from '@main/api'
|
import api from '@main/api'
|
||||||
|
import { containsQuoteMarkers } from '@shared-ui/utils/quotedContent.js'
|
||||||
|
|
||||||
const extendedCssProperties = [...allowedCssProperties, 'transform', 'transform-origin']
|
const extendedCssProperties = [...allowedCssProperties, 'transform', 'transform-origin']
|
||||||
|
|
||||||
@@ -305,7 +306,7 @@ const retryMessage = (msg) => {
|
|||||||
|
|
||||||
const showQuotedText = ref(false)
|
const showQuotedText = ref(false)
|
||||||
const hasQuotedContent = computed(
|
const hasQuotedContent = computed(
|
||||||
() => !isOutgoing.value && sanitizedContent.value.includes('<blockquote')
|
() => !isOutgoing.value && containsQuoteMarkers(sanitizedContent.value)
|
||||||
)
|
)
|
||||||
const toggleQuote = () => {
|
const toggleQuote = () => {
|
||||||
showQuotedText.value = !showQuotedText.value
|
showQuotedText.value = !showQuotedText.value
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import { describe, test, expect } from 'vitest'
|
||||||
|
import { containsQuoteMarkers } from '@shared-ui/utils/quotedContent.js'
|
||||||
|
|
||||||
|
describe('containsQuoteMarkers', () => {
|
||||||
|
test('returns false for null', () => {
|
||||||
|
expect(containsQuoteMarkers(null)).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('returns false for undefined', () => {
|
||||||
|
expect(containsQuoteMarkers(undefined)).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('returns false for empty string', () => {
|
||||||
|
expect(containsQuoteMarkers('')).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('returns false for plain HTML with no quote', () => {
|
||||||
|
expect(containsQuoteMarkers('<p>Hello world</p><div>thanks</div>')).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('detects <blockquote> (Gmail/Apple/Thunderbird)', () => {
|
||||||
|
expect(containsQuoteMarkers('<blockquote>previous</blockquote>')).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('detects <blockquote> with attributes', () => {
|
||||||
|
expect(containsQuoteMarkers('<blockquote type="cite" class="gmail_quote">x</blockquote>'))
|
||||||
|
.toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('detects Outlook Web appendonsend marker', () => {
|
||||||
|
expect(containsQuoteMarkers('<div id="appendonsend"></div>')).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('detects Outlook divRplyFwdMsg marker', () => {
|
||||||
|
expect(containsQuoteMarkers('<div id="divRplyFwdMsg" dir="ltr">From:</div>')).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('detects Outlook for Mac OLK_SRC_BODY_SECTION marker', () => {
|
||||||
|
expect(containsQuoteMarkers('<div id="OLK_SRC_BODY_SECTION">body</div>')).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('detects legacy Outlook OutlookMessageHeader class', () => {
|
||||||
|
expect(containsQuoteMarkers('<div class="OutlookMessageHeader">x</div>')).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('detects real Hotmail reply payload', () => {
|
||||||
|
const html = `<div class="elementToProof">Quoted reply!</div>
|
||||||
|
<div id="appendonsend"></div>
|
||||||
|
<hr style="display:inline-block;width:98%" tabindex="-1">
|
||||||
|
<div id="divRplyFwdMsg" dir="ltr"><font face="Calibri, sans-serif">From: Libredesk
|
||||||
|
Sent: 17 May 2026 18:12</font></div>
|
||||||
|
<div>Original message body</div>`
|
||||||
|
expect(containsQuoteMarkers(html)).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('does not match marker word in plain prose', () => {
|
||||||
|
expect(containsQuoteMarkers('I love the appendonsend feature in Outlook')).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('does not match if id uses single quotes (edge case - Outlook always emits double)', () => {
|
||||||
|
expect(containsQuoteMarkers(`<div id='appendonsend'></div>`)).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -160,6 +160,7 @@ import MessageAttachment from './MessageAttachment.vue'
|
|||||||
import CSATMessageBubble from './CSATMessageBubble.vue'
|
import CSATMessageBubble from './CSATMessageBubble.vue'
|
||||||
import { TypingIndicator } from '@shared-ui/components/TypingIndicator'
|
import { TypingIndicator } from '@shared-ui/components/TypingIndicator'
|
||||||
import { Spinner } from '@shared-ui/components/ui/spinner'
|
import { Spinner } from '@shared-ui/components/ui/spinner'
|
||||||
|
import { containsQuoteMarkers } from '@shared-ui/utils/quotedContent.js'
|
||||||
|
|
||||||
const extendedCssProperties = [...allowedCssProperties, 'transform', 'transform-origin']
|
const extendedCssProperties = [...allowedCssProperties, 'transform', 'transform-origin']
|
||||||
|
|
||||||
@@ -187,9 +188,7 @@ const getMessageTime = (timestamp) => {
|
|||||||
return useRelativeTime(new Date(timestamp)).value
|
return useRelativeTime(new Date(timestamp)).value
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasQuotedContent = (content) => {
|
const hasQuotedContent = (content) => containsQuoteMarkers(content)
|
||||||
return content && content.includes('<blockquote')
|
|
||||||
}
|
|
||||||
|
|
||||||
const isQuotedTextVisible = (messageUuid) => {
|
const isQuotedTextVisible = (messageUuid) => {
|
||||||
return quotedTextState.value[messageUuid] || false
|
return quotedTextState.value[messageUuid] || false
|
||||||
|
|||||||
@@ -296,7 +296,21 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.hide-quoted-text {
|
.hide-quoted-text {
|
||||||
blockquote {
|
// Selectors below use attribute-suffix matching because vue-letter
|
||||||
|
// prefixes every id/class with a random "msg_XYZ_" at render time, so
|
||||||
|
// raw "#appendonsend" would never match.
|
||||||
|
// Outlook/Hotmail emit a marker with the quoted history as trailing
|
||||||
|
// siblings (no wrapping element), so we hide the marker AND everything
|
||||||
|
// after it. Outlook always top-posts, so trailing siblings are quote.
|
||||||
|
blockquote,
|
||||||
|
[id$="_divRplyFwdMsg"],
|
||||||
|
[id$="_divRplyFwdMsg"] ~ *,
|
||||||
|
[id$="_appendonsend"],
|
||||||
|
[id$="_appendonsend"] ~ *,
|
||||||
|
[id$="_OLK_SRC_BODY_SECTION"],
|
||||||
|
[id$="_OLK_SRC_BODY_SECTION"] ~ *,
|
||||||
|
[class*="_OutlookMessageHeader"],
|
||||||
|
[class*="_OutlookMessageHeader"] ~ * {
|
||||||
@apply hidden;
|
@apply hidden;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
// Runs against the RAW HTML stored on the message - before vue-letter
|
||||||
|
// renders. vue-letter prefixes every id/class with a random "msg_XYZ_" at
|
||||||
|
// render time, so any CSS that needs to match the rendered DOM must use
|
||||||
|
// attribute-suffix selectors (see .hide-quoted-text in main.scss).
|
||||||
|
//
|
||||||
|
// Scope: <blockquote> covers Gmail/Apple/Thunderbird. The id/class markers
|
||||||
|
// below cover the Microsoft variants (Outlook desktop/web/mac, Hotmail).
|
||||||
|
export const QUOTE_MARKERS = [
|
||||||
|
'<blockquote',
|
||||||
|
'id="divRplyFwdMsg"',
|
||||||
|
'id="appendonsend"',
|
||||||
|
'id="OLK_SRC_BODY_SECTION"',
|
||||||
|
'class="OutlookMessageHeader"'
|
||||||
|
]
|
||||||
|
|
||||||
|
export const containsQuoteMarkers = (html) => {
|
||||||
|
if (!html) return false
|
||||||
|
return QUOTE_MARKERS.some((marker) => html.includes(marker))
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user