diff --git a/app/javascript/dashboard/components-next/message/bubbles/Email/Index.vue b/app/javascript/dashboard/components-next/message/bubbles/Email/Index.vue
index dbb6a0a71..78d01ece7 100644
--- a/app/javascript/dashboard/components-next/message/bubbles/Email/Index.vue
+++ b/app/javascript/dashboard/components-next/message/bubbles/Email/Index.vue
@@ -3,6 +3,7 @@ import { computed, useTemplateRef, ref, watch } from 'vue';
import { Letter } from 'vue-letter';
import Icon from 'next/icon/Icon.vue';
+import { EmailQuoteExtractor } from './removeReply.js';
import BaseBubble from 'next/message/bubbles/Base.vue';
import AttachmentChips from 'next/message/chips/AttachmentChips.vue';
@@ -35,6 +36,7 @@ const props = defineProps({
const isOverflowing = ref(false);
const isExpanded = ref(false);
+const showQuotedMessage = ref(false);
const contentContainer = useTemplateRef('contentContainer');
watch(
@@ -47,10 +49,18 @@ watch(
{ immediate: true }
);
-const contentToShow = computed(() => {
+const fullHTML = computed(() => {
return props.contentAttributes?.email?.htmlContent?.full ?? props.content;
});
+const unquotedHTML = computed(() => {
+ return EmailQuoteExtractor.extractQuotes(fullHTML.value);
+});
+
+const hasQuotedMessage = computed(() => {
+ return EmailQuoteExtractor.hasQuotes(fullHTML.value);
+});
+
const textToShow = computed(() => {
const text =
props.contentAttributes?.email?.textContent?.full ?? props.content;
@@ -81,10 +91,29 @@ const textToShow = computed(() => {
+
+
diff --git a/app/javascript/dashboard/components-next/message/bubbles/Email/removeReply.js b/app/javascript/dashboard/components-next/message/bubbles/Email/removeReply.js
new file mode 100644
index 000000000..50b0c9df9
--- /dev/null
+++ b/app/javascript/dashboard/components-next/message/bubbles/Email/removeReply.js
@@ -0,0 +1,126 @@
+// Quote detection strategies
+const QUOTE_INDICATORS = [
+ '.gmail_quote_container',
+ '.gmail_quote',
+ '.OutlookQuote',
+ '.email-quote',
+ '.quoted-text',
+ '.quote',
+ '[class*="quote"]',
+ '[class*="Quote"]',
+];
+
+// Regex patterns for quote identification
+const QUOTE_PATTERNS = [
+ /On .* wrote:/i,
+ /-----Original Message-----/i,
+ /Sent: /i,
+ /From: /i,
+];
+
+export class EmailQuoteExtractor {
+ /**
+ * Remove quotes from email HTML and return cleaned HTML
+ * @param {string} htmlContent - Full HTML content of the email
+ * @returns {string} HTML content with quotes removed
+ */
+ static extractQuotes(htmlContent) {
+ // Create a temporary DOM element to parse HTML
+ const tempDiv = document.createElement('div');
+ tempDiv.innerHTML = htmlContent;
+
+ // Remove elements matching class selectors
+ QUOTE_INDICATORS.forEach(selector => {
+ tempDiv.querySelectorAll(selector).forEach(el => {
+ el.remove();
+ });
+ });
+
+ // Remove text-based quotes
+ const textNodeQuotes = this.findTextNodeQuotes(tempDiv);
+ textNodeQuotes.forEach(el => {
+ el.remove();
+ });
+
+ return tempDiv.innerHTML;
+ }
+
+ /**
+ * Check if HTML content contains any quotes
+ * @param {string} htmlContent - Full HTML content of the email
+ * @returns {boolean} True if quotes are detected, false otherwise
+ */
+ static hasQuotes(htmlContent) {
+ const tempDiv = document.createElement('div');
+ tempDiv.innerHTML = htmlContent;
+
+ // Check for class-based quotes
+ // eslint-disable-next-line no-restricted-syntax
+ for (const selector of QUOTE_INDICATORS) {
+ if (tempDiv.querySelector(selector)) {
+ return true;
+ }
+ }
+
+ // Check for text-based quotes
+ const textNodeQuotes = this.findTextNodeQuotes(tempDiv);
+ return textNodeQuotes.length > 0;
+ }
+
+ /**
+ * Find text nodes that match quote patterns
+ * @param {Element} rootElement - Root element to search
+ * @returns {Element[]} Array of parent block elements containing quote-like text
+ */
+ static findTextNodeQuotes(rootElement) {
+ const quoteBlocks = [];
+ const treeWalker = document.createTreeWalker(
+ rootElement,
+ NodeFilter.SHOW_TEXT,
+ null,
+ false
+ );
+
+ for (
+ let currentNode = treeWalker.nextNode();
+ currentNode !== null;
+ currentNode = treeWalker.nextNode()
+ ) {
+ const isQuoteLike = QUOTE_PATTERNS.some(pattern =>
+ pattern.test(currentNode.textContent)
+ );
+
+ if (isQuoteLike) {
+ const parentBlock = this.findParentBlock(currentNode);
+ if (parentBlock && !quoteBlocks.includes(parentBlock)) {
+ quoteBlocks.push(parentBlock);
+ }
+ }
+ }
+
+ return quoteBlocks;
+ }
+
+ /**
+ * Find the closest block-level parent element by recursively traversing up the DOM tree.
+ * This method searches for common block-level elements like DIV, P, BLOCKQUOTE, and SECTION
+ * that contain the text node. It's used to identify and remove entire block-level elements
+ * that contain quote-like text, rather than just removing the text node itself. This ensures
+ * proper structural removal of quoted content while maintaining HTML integrity.
+ * @param {Node} node - Starting node to find parent
+ * @returns {Element|null} Block-level parent element
+ */
+ static findParentBlock(node) {
+ const blockElements = ['DIV', 'P', 'BLOCKQUOTE', 'SECTION'];
+ let current = node.parentElement;
+
+ while (current) {
+ if (blockElements.includes(current.tagName)) {
+ return current;
+ }
+ current = current.parentElement;
+ }
+
+ return null;
+ }
+}