import {cleanHTML} from "./clean-message"; import {type CSSResultGroup, html, nothing, unsafeCSS} from 'lit'; import {property, state} from "lit/decorators.js"; import {unsafeHTML} from "lit/directives/unsafe-html.js"; import ZincElement from '../../internal/zinc-element'; import ZnIcon from "../icon"; import styles from './chat-message.scss'; export type ChatMessageActionType = '' | 'connected.agent' | 'attachment.added' | 'multi.answer' | 'transfer' | 'ended' | 'error' | 'message-sending' | 'customer.ended' | 'customer.connected' | 'customer.disconnected' | 'internal'; /** * @summary A single message in a chat-style conversation: avatar, sender, * time, optional badge, and a message bubble with optional actions. Also renders * system events (connections, transfers, etc.) as a centred card. * @documentation https://zinc.style/components/chat-message * @status experimental * @since 1.0 * * @dependency zn-icon * * @slot - The message content. Ignored when the `message` attribute is set. * @slot badge - Rendered in the header after the sender and time (e.g. an INTERNAL NOTE chip). * @slot attachments - Attachments displayed beneath the message content. Use `zn-chat-message-attachment`, * which auto-assigns itself to this slot. * @slot edit-dialog-trigger - Action rendered at the end of the bubble (e.g. a remove icon button). * @slot edit-dialog - Pass-through for an associated dialog element. * * @csspart base - The component's base wrapper. * @csspart avatar - The avatar column. * @csspart body - The header and bubble column. * @csspart header - The sender/time/badge row. * @csspart bubble - The message bubble. * @csspart content - The message content within the bubble. * @csspart attachments - The attachments row beneath the message content. * @csspart system-card - The card rendered for system action types. * * @cssproperty --message-background - The bubble's background color. */ export default class ZnChatMessage extends ZincElement { static styles: CSSResultGroup = unsafeCSS(styles); static dependencies = { 'zn-icon': ZnIcon }; static readonly SYSTEM_ACTION_TYPES: readonly string[] = [ 'connected.agent', 'attachment.added', 'transfer', 'ended', 'customer.ended', 'customer.connected', 'customer.disconnected', ]; private static readonly DISPLAYED_ACTION_TYPES: readonly string[] = [ '', 'connected.agent', 'attachment.added', 'multi.answer', 'transfer', 'ended', 'error', 'customer.ended', 'message-sending', 'customer.connected', 'customer.disconnected', 'internal', ]; /** The sender's name, also used for the avatar. */ @property() sender: string = ''; /** * The message body as an HTML string. When set, it is sanitized (scripts and event * handlers stripped), newlines become line breaks and bare URLs become links. When * omitted the default slot is rendered instead. */ @property() message: string = ''; /** Unix timestamp (seconds) of the message, shown as HH:MM (with date if not today). */ @property() time: string = ''; /** Overrides the avatar source. Defaults to `sender`. */ @property() avatar: string = ''; /** The kind of message. Drives system-card rendering, the sending state and badges. */ @property({attribute: 'action-type', reflect: true}) actionType: ChatMessageActionType = ''; /** Marks the message as initiated by the customer (affects styling and grouping). */ @property({type: Boolean, reflect: true, attribute: 'customer-initiated'}) customerInitiated = false; /** Marks the message as initiated by an agent (affects styling and grouping). */ @property({type: Boolean, reflect: true, attribute: 'agent-initiated'}) agentInitiated = false; /** Hides the sender's name in the message header. */ @property({type: Boolean, reflect: true, attribute: 'hide-sender'}) hideSender = false; /** Whether any element is assigned to the `attachments` slot — drives the attachments row visibility. */ @state() private hasAttachments = false; connectedCallback() { const isContent = !this.actionType || this.actionType === 'internal'; const agentInitiated = isContent && this.agentInitiated; const customerInitiated = isContent && this.customerInitiated; const previous = this.previousElementSibling as ZnChatMessage; const inTime = parseInt(this.time) - parseInt(previous?.time); const previousActionType = previous?.getAttribute('action-type') ?? ''; const previousIsSystem = ZnChatMessage.SYSTEM_ACTION_TYPES.includes(previousActionType); const previousIsInternal = previousActionType === 'internal'; const thisIsInternal = this.actionType === 'internal'; if (previous && !previousIsSystem && inTime < 60 && previousIsInternal === thisIsInternal && ((previous?.hasAttribute('agent-initiated') && agentInitiated) || (previous?.hasAttribute('customer-initiated') && customerInitiated))) { this.classList.add('message-continued'); } super.connectedCallback(); } protected render() { if (!ZnChatMessage.DISPLAYED_ACTION_TYPES.includes(this.actionType)) { return nothing; } if (this.isSystemMessage()) { return this.renderSystemCard(); } const sending = this.actionType === 'message-sending'; return html`