- {messageGroups.map(group => (
-
- {/* Date separator */}
-
-
-
- {group.dateLabel}
-
-
-
+ {messageGroups.map(group => {
+ // Group consecutive messages within this date group
+ const senderGroups = groupConsecutiveMessages(group.messages);
- {/* Messages for this date */}
-
- {group.messages.map(message => (
-
- ))}
+ return (
+
+ {/* Date separator */}
+
+
+
+ {group.dateLabel}
+
+
+
+
+ {/* Messages for this date, grouped by sender */}
+
+ {senderGroups.map((senderGroup, groupIndex) => (
+
+ ))}
+
-
- ))}
+ );
+ })}
{/* No replies yet message */}
{!hasReplies && (
-
- No replies yet. Our team will respond shortly.
-
+
+
+
+ Your message has been received! Our support team typically responds within 24 hours.
+
+
)}
+
+ {/* Auto-scroll anchor */}
+
);
}
+/**
+ * Grouped messages from the same sender - shows avatar only on first message
+ */
+function MessageGroupBubbles({
+ senderGroup,
+ pendingMessageId,
+ lastViewedTime,
+ showSentConfirmation,
+}: {
+ senderGroup: SenderMessageGroup;
+ pendingMessageId: string | null;
+ lastViewedTime: string | null;
+ showSentConfirmation: boolean;
+}) {
+ const { isCustomer, messages } = senderGroup;
+
+ return (
+
+ {/* Avatar - only show for support (on left side) */}
+ {!isCustomer && (
+
+
+
+ )}
+
+ {/* Message bubbles */}
+
+ {messages.map((message, index) => {
+ const isFirst = index === 0;
+ const isLast = index === messages.length - 1;
+ const isPending = message.id === pendingMessageId;
+ const isNew = Boolean(
+ !isCustomer && lastViewedTime && new Date(message.createdAt) > new Date(lastViewedTime)
+ );
+
+ return (
+
+ );
+ })}
+
+
+ {/* Avatar - only show for customer (on right side) */}
+ {isCustomer && (
+
+
+
+ )}
+
+ );
+}
+
/**
* Message bubble component for displaying individual messages
*/
-function MessageBubble({ message }: { message: CaseMessage }) {
+function MessageBubble({
+ message,
+ showHeader = true,
+ isPending = false,
+ isNew = false,
+ showSentConfirmation = false,
+}: {
+ message: CaseMessage;
+ showHeader?: boolean;
+ isPending?: boolean;
+ isNew?: boolean;
+ showSentConfirmation?: boolean;
+}) {
const isCustomer = message.author.isCustomer;
const isEmail = message.type === "email";
const hasAttachment = message.hasAttachment === true;
+ const displayName = isCustomer ? "You" : message.author.name;
return (
-
-
- {/* Author and type indicator */}
+
+ {/* Author and type indicator - only show on first message of group */}
+ {showHeader && (
)}
-
{message.author.name}
+
{displayName}
•
{formatIsoRelative(message.createdAt)}
{hasAttachment && (
@@ -431,11 +683,38 @@ function MessageBubble({ message }: { message: CaseMessage }) {
>
)}
+ {isNew && (
+
+ New
+
+ )}
+ )}
- {/* Message body */}
-
{message.body}
-
+ {/* Message body */}
+
{message.body}
+
+ {/* Pending/Sent status indicator */}
+ {isPending && (
+
+
+ Sending...
+
+ )}
+ {showSentConfirmation && !isPending && (
+
+
+ Sent
+
+ )}
);
}
diff --git a/packages/domain/support/providers/salesforce/mapper.ts b/packages/domain/support/providers/salesforce/mapper.ts
index 4382c0b7..678526ae 100644
--- a/packages/domain/support/providers/salesforce/mapper.ts
+++ b/packages/domain/support/providers/salesforce/mapper.ts
@@ -368,10 +368,12 @@ export function transformEmailMessageToCaseMessage(
const fromName = ensureString(email.FromName) ?? fromEmail ?? "Unknown";
// Determine if this is from the customer
- // Incoming emails are from customer, or check email match
+ // Incoming emails are from customer, or check email match (with trimming for robustness)
const isCustomer =
isIncoming ||
- (customerEmail ? fromEmail?.toLowerCase() === customerEmail.toLowerCase() : false);
+ (customerEmail && fromEmail
+ ? fromEmail.trim().toLowerCase() === customerEmail.trim().toLowerCase()
+ : false);
// Get the raw email body and clean it (strip quoted content)
const rawBody = ensureString(email.TextBody) ?? ensureString(email.HtmlBody) ?? "";
@@ -440,19 +442,21 @@ export function buildCaseCommentsForCaseQuery(caseId: string): string {
* Transform a Salesforce CaseComment to a unified CaseMessage.
*
* @param comment - Raw Salesforce CaseComment
- * @param customerContactId - Customer's Salesforce Contact ID for comparison
+ * @param customerEmail - Customer's email address for comparison
*/
export function transformCaseCommentToCaseMessage(
comment: SalesforceCaseComment,
- customerContactId?: string
+ customerEmail?: string
): CaseMessage {
const authorName = comment.CreatedBy?.Name ?? "Unknown";
const authorEmail = comment.CreatedBy?.Email ?? null;
- // Determine if this is from the customer
- // If CreatedById matches customer's contact ID, it's from customer
- // Otherwise, we assume it's from an agent (conservative approach)
- const isCustomer = customerContactId ? comment.CreatedById === customerContactId : false;
+ // Determine if this is from the customer using email-based matching
+ // (consistent with EmailMessage approach for unified attribution, with trimming for robustness)
+ const isCustomer =
+ customerEmail && authorEmail
+ ? authorEmail.trim().toLowerCase() === customerEmail.trim().toLowerCase()
+ : false;
return caseMessageSchema.parse({
id: comment.Id,
@@ -473,9 +477,9 @@ export function transformCaseCommentToCaseMessage(
*/
export function transformCaseCommentsToCaseMessages(
comments: SalesforceCaseComment[],
- customerContactId?: string
+ customerEmail?: string
): CaseMessage[] {
- return comments.map(comment => transformCaseCommentToCaseMessage(comment, customerContactId));
+ return comments.map(comment => transformCaseCommentToCaseMessage(comment, customerEmail));
}
// ============================================================================