Skip to main content

Overview

The CometChatConversationItem component is a standalone Angular component designed to render a single conversation item. It is part of the enterprise refactoring that decomposes the monolithic CometChatConversations component into smaller, focused pieces. This component provides:
  • State Management: Support for active, selected, focused, and unread states
  • Slot-Based Customization: Fine-grained control over individual UI elements
  • Granular Events: Separate events for avatar, title, subtitle, timestamp, and badge clicks
  • Backward Compatibility: Section templates for existing implementations
  • Full Accessibility: ARIA labels, roles, and keyboard navigation support

Key Features

  • Standalone Component: Can be used independently or within CometChatConversations
  • 11 Customization Slots: Override specific UI elements without affecting others
  • 8 Granular Events: Respond to precise user interactions
  • Display Configuration: Hide receipts, user status, or group type icons
  • Context Menu Support: Customizable actions for each conversation
  • OnPush Change Detection: Optimized for performance

Basic Usage

Simple Implementation

import { Component } from '@angular/core';
import { CometChat } from '@cometchat/chat-sdk-javascript';
import { CometChatConversationItemComponent } from '@cometchat/chat-uikit-angular';

@Component({
  selector: 'app-conversation-item-demo',
  standalone: true,
  imports: [CometChatConversationItemComponent],
  template: `
    <cometchat-conversation-item
      [conversation]="conversation"
      [isActive]="isActive"
      (itemClick)="onItemClick($event)"
    ></cometchat-conversation-item>
  `
})
export class ConversationItemDemoComponent {
  conversation!: CometChat.Conversation;
  isActive = false;

  onItemClick(conversation: CometChat.Conversation): void {
    console.log('Conversation clicked:', conversation);
    this.isActive = true;
  }
}
Live Preview — interact with the component directly in the embed below. Open in Storybook ↗

With State Management

<cometchat-conversation-item
  [conversation]="conversation"
  [isActive]="selectedConversationId === conversation.getConversationId()"
  [isSelected]="isInSelectionMode && selectedIds.has(conversation.getConversationId())"
  [isFocused]="focusedConversationId === conversation.getConversationId()"
  [typingIndicator]="typingIndicators.get(conversation.getConversationId())"
  (itemClick)="onItemClick($event)"
  (itemSelect)="onItemSelect($event)"
></cometchat-conversation-item>

Properties

Required Input Properties

PropertyTypeDefaultDescription
conversationCometChat.ConversationRequired. The conversation object to render

State Input Properties

PropertyTypeDefaultDescription
isActivebooleanfalseWhether this conversation is currently active/selected for viewing
isSelectedbooleanfalseWhether this conversation is selected in selection mode
isFocusedbooleanfalseWhether this conversation item currently has keyboard focus
tabIndexnumber-1Tab index for the conversation item element
typingIndicatorCometChat.TypingIndicator | nullnullThe typing indicator for this conversation, if someone is typing
loggedInUserCometChat.User | nullnullThe currently logged-in user, used for determining message alignment and receipts
Live Preview — all states (default, active, selected, focused, unread) side by side. Open in Storybook ↗

Display Configuration Properties

PropertyTypeDefaultDescription
hideReceiptsbooleanfalseHide message read receipts (sent, delivered, read indicators)
hideUserStatusbooleanfalseHide the user online/offline status indicator
hideGroupTypebooleanfalseHide the group type icon (public, private, password)
disableDefaultContextMenubooleantrueWhen true, prevents the browser’s native context menu and shows the custom context menu instead
dateFormatCalendarObjectundefinedCustom date format configuration for the timestamp display
textFormattersCometChatTextFormatter[][]Array of text formatters applied to the last message subtitle text

Customization Properties

PropertyTypeDefaultDescription
slotsPartial<ConversationSlots>undefinedSlot-based customization for fine-grained UI control
contextMenuOptionsCometChatOption[][]Options for the context menu displayed on hover/right-click

Section Template Properties (Backward Compatibility)

PropertyTypeDefaultDescription
leadingViewTemplateRef<{$implicit: Conversation}>undefinedCustom template for the leading section (avatar area)
titleViewTemplateRef<{$implicit: Conversation}>undefinedCustom template for the title section
subtitleViewTemplateRef<{$implicit: Conversation}>undefinedCustom template for the subtitle section
trailingViewTemplateRef<{$implicit: Conversation}>undefinedCustom template for the trailing section (timestamp, badge area)

Events

EventPayload TypeDescription
itemClickCometChat.ConversationEmitted when the conversation item is clicked
itemSelect{conversation: Conversation, selected: boolean}Emitted when the conversation is selected/deselected
avatarClickCometChat.ConversationEmitted when the avatar is clicked
titleClickCometChat.ConversationEmitted when the title is clicked
subtitleClickCometChat.ConversationEmitted when the subtitle is clicked
timestampClickCometChat.ConversationEmitted when the timestamp is clicked
badgeClickCometChat.ConversationEmitted when the unread badge is clicked
contextMenuOpenCometChat.ConversationEmitted when the context menu is opened
contextMenuOptionClick{option: CometChatOption, conversation: Conversation}Emitted when a context menu option is clicked
contextMenuKeyboardOpenCometChat.ConversationEmitted when the context menu is triggered via keyboard, allowing the parent to handle display

Slot-Based Customization

The component supports 11 slots via the ConversationSlots interface, organized into three sections:

Slot Overview

SectionSlot NameDescription
LeadingavatarCustom avatar element
LeadingstatusIndicatorUser online/offline status indicator
LeadinggroupTypeIconGroup type icon (public/private/password)
LeadingtypingIndicatorTyping animation indicator
BodytitleConversation title (user/group name)
BodysubtitleLast message preview
BodysubtitleReceiptReceipt indicator in subtitle area
TrailingtimestampMessage timestamp display
TrailingunreadBadgeUnread message count badge
TrailingreceiptMessage receipt indicator
TrailingcontextMenuTriggerContext menu trigger button

Slot Context

All slot templates receive a ConversationSlotContext object:
interface ConversationSlotContext {
  $implicit: CometChat.Conversation;  // Implicit context variable
  conversation: CometChat.Conversation;  // Explicit named variable
  isActive: boolean;  // Whether conversation is active
  isSelected: boolean;  // Whether conversation is selected
  unreadCount: number;  // Number of unread messages
  isTyping: boolean;  // Whether someone is typing
}
Some slots receive additional context properties:
  • statusIndicator: { status: string } - User status (‘online’, ‘offline’)
  • groupTypeIcon: { groupType: string } - Group type (‘public’, ‘private’, ‘password’)
  • title: { title: string } - Display name
  • subtitle: { subtitle: string } - Last message preview
  • timestamp: { timestamp: number } - Unix timestamp
  • unreadBadge: { count: number } - Unread count
  • receipt: { receiptStatus: string } - Receipt status

Custom Avatar Slot Example

import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { CometChat } from '@cometchat/chat-sdk-javascript';
import { 
  CometChatConversationItemComponent,
  ConversationSlots 
} from '@cometchat/chat-uikit-angular';

@Component({
  selector: 'app-custom-avatar',
  standalone: true,
  imports: [CommonModule, CometChatConversationItemComponent],
  template: `
    <cometchat-conversation-item
      [conversation]="conversation"
      [slots]="customSlots"
      (itemClick)="onItemClick($event)"
    >
      <ng-template #customAvatarSlot let-conversation let-isActive="isActive">
        <div class="custom-avatar" [class.active]="isActive">
          <img 
            [src]="getAvatarUrl(conversation)" 
            [alt]="conversation.getConversationWith().getName()"
            class="avatar-image"
          />
          @if (isVIP(conversation)) {
            <span class="avatar-badge">VIP</span>
          }
        </div>
      </ng-template>
    </cometchat-conversation-item>
  `,
  styles: [`
    .custom-avatar {
      position: relative;
      width: 48px;
      height: 48px;
    }
    .custom-avatar.active {
      border: 2px solid var(--cometchat-primary-color);
      border-radius: 50%;
    }
    .avatar-image {
      width: 100%;
      height: 100%;
      border-radius: 50%;
      object-fit: cover;
    }
    .avatar-badge {
      position: absolute;
      top: -4px;
      right: -4px;
      background: gold;
      color: black;
      font-size: 8px;
      padding: 2px 4px;
      border-radius: 4px;
      font-weight: bold;
    }
  `]
})
export class CustomAvatarComponent {
  conversation!: CometChat.Conversation;
  customSlots: Partial<ConversationSlots> = {};

  ngAfterViewInit(): void {
    // Reference the template after view init
    // In practice, use ViewChild to get the template reference
  }

  getAvatarUrl(conversation: CometChat.Conversation): string {
    const conversationWith = conversation.getConversationWith();
    if (conversationWith instanceof CometChat.User) {
      return conversationWith.getAvatar() || '';
    }
    return (conversationWith as CometChat.Group).getIcon() || '';
  }

  isVIP(conversation: CometChat.Conversation): boolean {
    // Custom logic to determine VIP status
    return false;
  }

  onItemClick(conversation: CometChat.Conversation): void {
    console.log('Conversation clicked:', conversation);
  }
}

Custom Unread Badge Slot Example

import { Component, ViewChild, TemplateRef, AfterViewInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { CometChat } from '@cometchat/chat-sdk-javascript';
import { 
  CometChatConversationItemComponent,
  ConversationSlots 
} from '@cometchat/chat-uikit-angular';

@Component({
  selector: 'app-custom-badge',
  standalone: true,
  imports: [CommonModule, CometChatConversationItemComponent],
  template: `
    <cometchat-conversation-item
      [conversation]="conversation"
      [slots]="customSlots"
      (itemClick)="onItemClick($event)"
      (badgeClick)="onBadgeClick($event)"
    >
      <ng-template #customBadgeSlot let-count="count" let-conversation>
        <div class="custom-badge" [class.high-priority]="count > 10">
          <span class="badge-icon">📬</span>
          <span class="badge-count">{{ count > 99 ? '99+' : count }}</span>
        </div>
      </ng-template>
    </cometchat-conversation-item>
  `,
  styles: [`
    .custom-badge {
      display: flex;
      align-items: center;
      gap: 4px;
      background: var(--cometchat-primary-color);
      color: white;
      padding: 4px 8px;
      border-radius: 12px;
      font-size: 12px;
    }
    .custom-badge.high-priority {
      background: #FF3B30;
      animation: pulse 1s infinite;
    }
    .badge-icon {
      font-size: 14px;
    }
    @keyframes pulse {
      0%, 100% { transform: scale(1); }
      50% { transform: scale(1.1); }
    }
  `]
})
export class CustomBadgeComponent implements AfterViewInit {
  @ViewChild('customBadgeSlot') customBadgeSlot!: TemplateRef<any>;
  
  conversation!: CometChat.Conversation;
  customSlots: Partial<ConversationSlots> = {};

  ngAfterViewInit(): void {
    this.customSlots = {
      unreadBadge: this.customBadgeSlot
    };
  }

  onItemClick(conversation: CometChat.Conversation): void {
    console.log('Conversation clicked:', conversation);
  }

  onBadgeClick(conversation: CometChat.Conversation): void {
    console.log('Badge clicked - mark as read:', conversation);
    // Implement mark as read logic
  }
}

Custom Timestamp Slot Example

import { Component, ViewChild, TemplateRef, AfterViewInit } from '@angular/core';
import { CommonModule, DatePipe } from '@angular/common';
import { CometChat } from '@cometchat/chat-sdk-javascript';
import { 
  CometChatConversationItemComponent,
  ConversationSlots 
} from '@cometchat/chat-uikit-angular';

@Component({
  selector: 'app-custom-timestamp',
  standalone: true,
  imports: [CommonModule, CometChatConversationItemComponent, DatePipe],
  template: `
    <cometchat-conversation-item
      [conversation]="conversation"
      [slots]="customSlots"
      (itemClick)="onItemClick($event)"
      (timestampClick)="onTimestampClick($event)"
    >
      <ng-template #customTimestampSlot let-timestamp="timestamp">
        <div class="custom-timestamp">
          <span class="time-icon">🕐</span>
          <span class="time-text">{{ getRelativeTime(timestamp) }}</span>
        </div>
      </ng-template>
    </cometchat-conversation-item>
  `,
  styles: [`
    .custom-timestamp {
      display: flex;
      align-items: center;
      gap: 4px;
      color: var(--cometchat-text-color-tertiary);
      font-size: 11px;
    }
    .time-icon {
      font-size: 12px;
    }
  `]
})
export class CustomTimestampComponent implements AfterViewInit {
  @ViewChild('customTimestampSlot') customTimestampSlot!: TemplateRef<any>;
  
  conversation!: CometChat.Conversation;
  customSlots: Partial<ConversationSlots> = {};

  ngAfterViewInit(): void {
    this.customSlots = {
      timestamp: this.customTimestampSlot
    };
  }

  getRelativeTime(timestamp: number): string {
    if (!timestamp) return '';
    
    const now = Math.floor(Date.now() / 1000);
    const diff = now - timestamp;
    
    if (diff < 60) return 'Just now';
    if (diff < 3600) return `${Math.floor(diff / 60)}m ago`;
    if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`;
    if (diff < 604800) return `${Math.floor(diff / 86400)}d ago`;
    return new Date(timestamp * 1000).toLocaleDateString();
  }

  onItemClick(conversation: CometChat.Conversation): void {
    console.log('Conversation clicked:', conversation);
  }

  onTimestampClick(conversation: CometChat.Conversation): void {
    console.log('Timestamp clicked:', conversation);
    // Show full date/time in a tooltip or modal
  }
}

Event Handling

Granular Click Events

The component emits separate events for each clickable element, allowing precise handling:
import { Component } from '@angular/core';
import { CometChat } from '@cometchat/chat-sdk-javascript';
import { CometChatConversationItemComponent, CometChatOption } from '@cometchat/chat-uikit-angular';

@Component({
  selector: 'app-event-handling',
  standalone: true,
  imports: [CometChatConversationItemComponent],
  template: `
    <cometchat-conversation-item
      [conversation]="conversation"
      [contextMenuOptions]="menuOptions"
      (itemClick)="onItemClick($event)"
      (avatarClick)="onAvatarClick($event)"
      (titleClick)="onTitleClick($event)"
      (subtitleClick)="onSubtitleClick($event)"
      (timestampClick)="onTimestampClick($event)"
      (badgeClick)="onBadgeClick($event)"
      (contextMenuOpen)="onContextMenuOpen($event)"
      (contextMenuOptionClick)="onContextMenuOptionClick($event)"
    ></cometchat-conversation-item>
  `
})
export class EventHandlingComponent {
  conversation!: CometChat.Conversation;
  
  menuOptions: CometChatOption[] = [
    { id: 'pin', title: 'Pin', iconURL: 'assets/pin.svg' },
    { id: 'mute', title: 'Mute', iconURL: 'assets/mute.svg' },
    { id: 'delete', title: 'Delete', iconURL: 'assets/delete.svg' }
  ];

  onItemClick(conversation: CometChat.Conversation): void {
    console.log('Item clicked - open conversation:', conversation);
    // Navigate to messages view
  }

  onAvatarClick(conversation: CometChat.Conversation): void {
    console.log('Avatar clicked - show profile:', conversation);
    // Open user/group profile modal
  }

  onTitleClick(conversation: CometChat.Conversation): void {
    console.log('Title clicked:', conversation);
    // Could open rename dialog for groups
  }

  onSubtitleClick(conversation: CometChat.Conversation): void {
    console.log('Subtitle clicked - jump to message:', conversation);
    // Navigate to the last message
  }

  onTimestampClick(conversation: CometChat.Conversation): void {
    console.log('Timestamp clicked - show details:', conversation);
    // Show message details or full timestamp
  }

  onBadgeClick(conversation: CometChat.Conversation): void {
    console.log('Badge clicked - mark as read:', conversation);
    // Mark conversation as read
  }

  onContextMenuOpen(conversation: CometChat.Conversation): void {
    console.log('Context menu opened for:', conversation);
  }

  onContextMenuOptionClick(event: { option: CometChatOption; conversation: CometChat.Conversation }): void {
    console.log('Menu option clicked:', event.option.id, event.conversation);
    
    switch (event.option.id) {
      case 'pin':
        this.pinConversation(event.conversation);
        break;
      case 'mute':
        this.muteConversation(event.conversation);
        break;
      case 'delete':
        this.deleteConversation(event.conversation);
        break;
    }
  }

  private pinConversation(conversation: CometChat.Conversation): void {
    // Implement pin logic
  }

  private muteConversation(conversation: CometChat.Conversation): void {
    // Implement mute logic
  }

  private deleteConversation(conversation: CometChat.Conversation): void {
    // Implement delete logic
  }
}

Selection Mode

Handle selection events for multi-select scenarios:
import { Component } from '@angular/core';
import { CometChat } from '@cometchat/chat-sdk-javascript';
import { CometChatConversationItemComponent } from '@cometchat/chat-uikit-angular';

@Component({
  selector: 'app-selection-mode',
  standalone: true,
  imports: [CometChatConversationItemComponent],
  template: `
    <div class="conversation-list">
      @for (conversation of conversations; track conversation.getConversationId()) {
        <cometchat-conversation-item
          [conversation]="conversation"
          [isSelected]="selectedIds.has(conversation.getConversationId())"
          (itemClick)="onItemClick($event)"
          (itemSelect)="onItemSelect($event)"
        ></cometchat-conversation-item>
      }
    </div>
    
    @if (selectedIds.size > 0) {
      <div class="selection-toolbar">
        <span>{{ selectedIds.size }} selected</span>
        <button (click)="deleteSelected()">Delete</button>
        <button (click)="clearSelection()">Clear</button>
      </div>
    }
  `
})
export class SelectionModeComponent {
  conversations: CometChat.Conversation[] = [];
  selectedIds = new Set<string>();

  onItemClick(conversation: CometChat.Conversation): void {
    // Toggle selection on click
    const id = conversation.getConversationId();
    if (this.selectedIds.has(id)) {
      this.selectedIds.delete(id);
    } else {
      this.selectedIds.add(id);
    }
  }

  onItemSelect(event: { conversation: CometChat.Conversation; selected: boolean }): void {
    const id = event.conversation.getConversationId();
    if (event.selected) {
      this.selectedIds.add(id);
    } else {
      this.selectedIds.delete(id);
    }
  }

  deleteSelected(): void {
    console.log('Deleting:', Array.from(this.selectedIds));
    // Implement bulk delete
    this.selectedIds.clear();
  }

  clearSelection(): void {
    this.selectedIds.clear();
  }
}

Styling with CSS Variables

The CometChatConversationItem component uses CSS variables for comprehensive theming:
cometchat-conversation-item {
  /* Item container */
  --cometchat-conversations-item-background: var(--cometchat-background-color-01);
  --cometchat-conversations-item-background-hover: var(--cometchat-background-color-02);
  --cometchat-conversations-item-background-active: var(--cometchat-background-color-03);
  --cometchat-conversations-item-background-selected: var(--cometchat-background-color-03);
  --cometchat-conversations-item-padding: var(--cometchat-spacing-3) var(--cometchat-spacing-4);
  --cometchat-conversations-item-gap: var(--cometchat-spacing-3);
  --cometchat-conversations-item-border-bottom: 1px solid var(--cometchat-border-color-light);
  
  /* Avatar */
  --cometchat-conversations-avatar-size: 48px;
  --cometchat-conversations-avatar-border-radius: 50%;
  
  /* Status indicator */
  --cometchat-conversations-status-size: 14px;
  --cometchat-conversations-status-online-color: var(--cometchat-success-color);
  --cometchat-conversations-status-offline-color: var(--cometchat-neutral-color-400);
  
  /* Title */
  --cometchat-conversations-title-text-font: var(--cometchat-font-body-medium);
  --cometchat-conversations-title-text-color: var(--cometchat-text-color-primary);
  
  /* Subtitle */
  --cometchat-conversations-subtitle-font: var(--cometchat-font-caption1-regular);
  --cometchat-conversations-subtitle-color: var(--cometchat-text-color-secondary);
  
  /* Timestamp */
  --cometchat-conversations-timestamp-font: var(--cometchat-font-caption2-regular);
  --cometchat-conversations-timestamp-color: var(--cometchat-text-color-tertiary);
  
  /* Unread badge */
  --cometchat-conversations-badge-background: var(--cometchat-primary-color);
  --cometchat-conversations-badge-color: #ffffff;
  --cometchat-conversations-badge-font-size: 10px;
  --cometchat-conversations-badge-min-width: 18px;
  --cometchat-conversations-badge-height: 18px;
  --cometchat-conversations-badge-border-radius: var(--cometchat-radius-max);
  
  /* Focus state */
  --cometchat-conversations-item-focus-box-shadow: inset 0 0 0 2px var(--cometchat-primary-color);
}

Dark Theme Example

.dark-theme cometchat-conversation-item {
  --cometchat-conversations-item-background: #1a1a1a;
  --cometchat-conversations-item-background-hover: #2a2a2a;
  --cometchat-conversations-item-background-active: #3a3a4a;
  --cometchat-conversations-title-text-color: #ffffff;
  --cometchat-conversations-subtitle-color: #cccccc;
  --cometchat-conversations-timestamp-color: #999999;
  --cometchat-conversations-item-border-bottom: 1px solid #333333;
}

Custom Brand Colors

.branded cometchat-conversation-item {
  --cometchat-conversations-badge-background: #FF6B35;
  --cometchat-conversations-status-online-color: #00D084;
  --cometchat-conversations-item-background-active: #fff5f2;
  --cometchat-conversations-item-focus-box-shadow: inset 0 0 0 2px #FF6B35;
}

Accessibility

The CometChatConversationItem component includes comprehensive accessibility features:

ARIA Attributes

  • role="listitem" on the conversation item container
  • aria-selected indicates selection state
  • aria-label with conversation name and unread count
  • role="status" on status indicators and badges
  • role="img" on group type icons with appropriate labels

Keyboard Navigation

KeyAction
TabFocus the conversation item
Enter / SpaceActivate the focused item
Arrow KeysNavigate between items (when used in a list)

Screen Reader Support

The component announces:
  • Conversation name
  • Unread message count (e.g., “5 unread messages”)
  • Online/offline status for user conversations
  • Group type for group conversations (private, password-protected)

Focus Management

  • Clear focus indicators using CSS box-shadow
  • Focus state is visually distinct from hover and active states
  • Supports high contrast mode

Integration with CometChatConversations

The CometChatConversationItem is designed to work seamlessly within CometChatConversations:
// CometChatConversations automatically uses CometChatConversationItem internally
// You can customize items through the parent component's properties

<cometchat-conversations
  [hideReceipts]="true"
  [hideUserStatus]="false"
  [hideGroupType]="false"
  (itemClick)="onConversationClick($event)"
></cometchat-conversations>
For advanced customization, you can provide a custom itemView template that uses CometChatConversationItem:
<cometchat-conversations [itemView]="customItemView">
  <ng-template #customItemView let-conversation>
    <cometchat-conversation-item
      [conversation]="conversation"
      [hideReceipts]="true"
      [slots]="customSlots"
      (avatarClick)="showProfile($event)"
    ></cometchat-conversation-item>
  </ng-template>
</cometchat-conversations>

Best Practices

Use slot-based customization for targeted changes to specific UI elements. This keeps default behavior for other elements and reduces code complexity.
When handling granular click events (avatarClick, titleClick, etc.), the component automatically calls stopPropagation() to prevent the main itemClick event from firing. Design your event handlers accordingly.
The component uses OnPush change detection strategy. If you’re passing objects that change internally, ensure you create new object references to trigger change detection.
  • CometChatConversations: Parent component that renders a list of conversation items
  • CometChatAvatar: Avatar component used in the leading section
  • CometChatDate: Date formatting component used for timestamps
  • CometChatContextMenu: Context menu component for conversation actions

Technical Details

  • Standalone Component: Can be imported and used independently
  • Change Detection: Uses OnPush strategy for optimal performance
  • Bundle Size: Lightweight, focused component
  • Dependencies: CometChatAvatar, CometChatDate, CometChatContextMenu, TranslatePipe
  • BEM CSS: Follows Block Element Modifier naming convention
  • Accessibility: WCAG 2.1 Level AA compliant