diff --git a/src/app/utils/copyHandler.ts b/src/app/utils/copyHandler.ts index 788fa85..fc691c1 100644 --- a/src/app/utils/copyHandler.ts +++ b/src/app/utils/copyHandler.ts @@ -28,54 +28,69 @@ export const setupCopyHandler = ( const messages: Array<{ sender: string; time: string; content: string }> = []; // Get all message elements in the container - const messageElements = container.querySelectorAll('[data-message-id]'); + const messageElements = Array.from(container.querySelectorAll('[data-message-id]')); messageElements.forEach((msgEl) => { - // Check if this message element intersects with the selection - if (!selection.containsNode(msgEl, true)) return; - const eventId = msgEl.getAttribute('data-message-id'); if (!eventId) return; const data = getMessageData(eventId); if (!data) return; - // Get the selected text content within this message - const tempRange = document.createRange(); - tempRange.selectNodeContents(msgEl); + // Find content elements within the message (skip headers, timestamps, etc.) + const contentEl = msgEl.querySelector('[class*="MessageTextBody"], [class*="message-content"], [class*="MessageBody"]') as HTMLElement; - // Check if the selection overlaps with this message - const intersects = range.compareBoundaryPoints(Range.END_TO_START, tempRange) <= 0 && - range.compareBoundaryPoints(Range.START_TO_END, tempRange) >= 0; - - if (!intersects) return; + if (!contentEl) return; + + // Check if the content element intersects with the selection + // by checking if selection contains any part of it + let isSelected = false; + try { + // Try to detect if this node or its children are in the selection + const walker = document.createTreeWalker( + contentEl, + NodeFilter.SHOW_TEXT, + null + ); + + let textNode = walker.nextNode(); + while (textNode && !isSelected) { + if (range.intersectsNode(textNode)) { + isSelected = true; + } + textNode = walker.nextNode(); + } + } catch (e) { + // Fallback: check if selection contains the content element + isSelected = selection.containsNode(contentEl, true); + } + + if (!isSelected) return; // Format the time const timeStr = timeHourMinute(data.ts, hour24Clock); - // Get the text content that was selected from this message - // Use the original content if the message is fully selected, otherwise get selected portion + // Extract the selected text from within this message's content let selectedText = ''; - // Find the message content element (the actual text, not header) - const contentElement = msgEl.querySelector('[class*="MessageTextBody"], [class*="message-content"]'); - if (contentElement) { - const clonedRange = range.cloneRange(); - clonedRange.selectNodeContents(contentElement); - - // Intersect with selection - if (range.compareBoundaryPoints(Range.START_TO_START, clonedRange) > 0) { - clonedRange.setStart(range.startContainer, range.startOffset); - } - if (range.compareBoundaryPoints(Range.END_TO_END, clonedRange) < 0) { - clonedRange.setEnd(range.endContainer, range.endOffset); - } - - selectedText = clonedRange.toString().trim(); - } else { - // Fallback: use all selected text in this message - selectedText = range.toString().trim(); + // Create a range for this message's content + const contentRange = document.createRange(); + contentRange.selectNodeContents(contentEl); + + // Clone the selection range to avoid modifying it + const extractRange = range.cloneRange(); + + // Constrain the extract range to be within the content element + // If selection starts before content, move start to content start + if (contentRange.compareBoundaryPoints(Range.START_TO_START, extractRange) > 0) { + extractRange.setStart(contentRange.startContainer, contentRange.startOffset); } + // If selection ends after content, move end to content end + if (contentRange.compareBoundaryPoints(Range.END_TO_END, extractRange) < 0) { + extractRange.setEnd(contentRange.endContainer, contentRange.endOffset); + } + + selectedText = extractRange.toString().trim(); if (selectedText) { messages.push({