diff --git a/src/components/chat/chat-input.ts b/src/components/chat/chat-input.ts
new file mode 100644
index 000000000..1c74f9414
--- /dev/null
+++ b/src/components/chat/chat-input.ts
@@ -0,0 +1,222 @@
+import { LitElement, html } from 'lit';
+import { property, query, state } from 'lit/decorators.js';
+import IgcIconButtonComponent from '../button/icon-button.js';
+import IgcChipComponent from '../chip/chip.js';
+import { registerComponent } from '../common/definitions/register.js';
+import IgcFileInputComponent from '../file-input/file-input.js';
+import IgcIconComponent from '../icon/icon.js';
+import { registerIconFromText } from '../icon/icon.registry.js';
+import IgcTextareaComponent from '../textarea/textarea.js';
+import IgcEmojiPickerComponent from './emoji-picker.js';
+import { styles } from './themes/input.base.css.js';
+import {
+ type IgcMessageAttachment,
+ attachmentIcon,
+ sendButtonIcon,
+} from './types.js';
+
+/**
+ *
+ * @element igc-chat
+ *
+ */
+export default class IgcChatInputComponent extends LitElement {
+ /** @private */
+ public static readonly tagName = 'igc-chat-input';
+
+ public static override styles = styles;
+
+ /* blazorSuppress */
+ public static register() {
+ registerComponent(
+ IgcChatInputComponent,
+ IgcTextareaComponent,
+ IgcIconButtonComponent,
+ IgcChipComponent,
+ IgcEmojiPickerComponent,
+ IgcFileInputComponent,
+ IgcIconComponent
+ );
+ }
+
+ @property({ type: Boolean, attribute: 'disable-attachments' })
+ public disableAttachments = false;
+
+ @property({ type: Boolean, attribute: 'disable-emojis' })
+ public disableEmojis = false;
+
+ @query('textarea')
+ private textInputElement!: HTMLTextAreaElement;
+
+ @state()
+ private inputValue = '';
+
+ @state()
+ private attachments: IgcMessageAttachment[] = [];
+
+ constructor() {
+ super();
+ registerIconFromText('attachment', attachmentIcon, 'material');
+ registerIconFromText('send-message', sendButtonIcon, 'material');
+ }
+
+ private handleInput(e: Event) {
+ const target = e.target as HTMLTextAreaElement;
+ this.inputValue = target.value;
+ this.adjustTextareaHeight();
+ }
+
+ private handleKeyDown(e: KeyboardEvent) {
+ if (e.key === 'Enter' && !e.shiftKey) {
+ e.preventDefault();
+ this.sendMessage();
+ } else {
+ const typingEvent = new CustomEvent('typing-change', {
+ detail: { isTyping: true },
+ });
+ this.dispatchEvent(typingEvent);
+ // wait 3 seconds and dispatch a stop-typing event
+ setTimeout(() => {
+ const stopTypingEvent = new CustomEvent('typing-change', {
+ detail: { isTyping: false },
+ });
+ this.dispatchEvent(stopTypingEvent);
+ }, 3000);
+ }
+ }
+
+ private adjustTextareaHeight() {
+ const textarea = this.textInputElement;
+ if (!textarea) return;
+
+ textarea.style.height = 'auto';
+ const newHeight = Math.min(textarea.scrollHeight, 120);
+ textarea.style.height = `${newHeight}px`;
+ }
+
+ private sendMessage() {
+ if (!this.inputValue.trim() && this.attachments.length === 0) return;
+
+ const messageEvent = new CustomEvent('message-send', {
+ detail: { text: this.inputValue, attachments: this.attachments },
+ });
+
+ this.dispatchEvent(messageEvent);
+ this.inputValue = '';
+ this.attachments = [];
+
+ if (this.textInputElement) {
+ this.textInputElement.style.height = 'auto';
+ }
+
+ setTimeout(() => {
+ this.textInputElement?.focus();
+ }, 0);
+ }
+
+ private addEmoji(e: CustomEvent) {
+ const emoji = e.detail.emoji;
+ this.inputValue += emoji;
+
+ // Focus back on input after selecting an emoji
+ this.updateComplete.then(() => {
+ const textarea = this.shadowRoot?.querySelector('textarea');
+ if (textarea) {
+ textarea.focus();
+ }
+ });
+ }
+
+ private handleFileUpload(e: Event) {
+ const input = (e.target as any).input as HTMLInputElement;
+ if (!input.files || input.files.length === 0) return;
+
+ const files = Array.from(input.files);
+ const newAttachments: IgcMessageAttachment[] = [];
+ files.forEach((file) => {
+ const isImage = file.type.startsWith('image/');
+ newAttachments.push({
+ id: `attach_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
+ type: isImage ? 'image' : 'file',
+ url: URL.createObjectURL(file),
+ name: file.name,
+ size: file.size,
+ thumbnail: isImage ? URL.createObjectURL(file) : undefined,
+ });
+ });
+ this.attachments = [...this.attachments, ...newAttachments];
+ }
+
+ private removeAttachment(index: number) {
+ this.attachments = this.attachments.filter((_, i) => i !== index);
+ }
+
+ protected override render() {
+ return html`
+
+
+ ${this.attachments?.map(
+ (attachment, index) => html`
+
+ this.removeAttachment(index)}
+ >
+ ${attachment.name}
+
+
+ `
+ )}
+
+ `;
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'igc-chat-input': IgcChatInputComponent;
+ }
+}
diff --git a/src/components/chat/chat-message-list.ts b/src/components/chat/chat-message-list.ts
new file mode 100644
index 000000000..0c6108e60
--- /dev/null
+++ b/src/components/chat/chat-message-list.ts
@@ -0,0 +1,153 @@
+import { LitElement, html } from 'lit';
+import { property } from 'lit/decorators.js';
+import { repeat } from 'lit/directives/repeat.js';
+import { registerComponent } from '../common/definitions/register.js';
+import IgcChatMessageComponent from './chat-message.js';
+import { styles } from './themes/message-list.base.css.js';
+import type { IgcMessage, IgcUser } from './types.js';
+
+/**
+ *
+ * @element igc-chat-message-list
+ *
+ */
+export default class IgcChatMessageListComponent extends LitElement {
+ /** @private */
+ public static readonly tagName = 'igc-chat-message-list';
+
+ public static override styles = styles;
+
+ /* blazorSuppress */
+ public static register() {
+ registerComponent(IgcChatMessageListComponent, IgcChatMessageComponent);
+ }
+
+ @property({ reflect: true, attribute: false })
+ public user: IgcUser | undefined;
+
+ @property({ reflect: true, attribute: false })
+ public messages: IgcMessage[] = [];
+
+ @property({ reflect: true, attribute: false })
+ public typingUsers: IgcUser[] = [];
+
+ @property({ type: Boolean, attribute: 'hide-avatar' })
+ public hideAvatar = false;
+
+ @property({ type: Boolean, attribute: 'hide-user-name' })
+ public hideUserName = false;
+
+ @property({ type: Boolean, attribute: 'hide-meta-data' })
+ public hideMetaData = false;
+
+ @property({ type: Boolean, attribute: 'disable-auto-scroll' })
+ public disableAutoScroll = false;
+
+ @property({ type: Boolean, attribute: 'disable-reactions' })
+ public disableReactions = false;
+
+ private formatDate(date: Date): string {
+ const today = new Date();
+ const yesterday = new Date(today);
+ yesterday.setDate(yesterday.getDate() - 1);
+
+ if (date.toDateString() === today.toDateString()) {
+ return 'Today';
+ }
+
+ if (date.toDateString() === yesterday.toDateString()) {
+ return 'Yesterday';
+ }
+
+ return date.toLocaleDateString('en-US', {
+ weekday: 'long',
+ month: 'short',
+ day: 'numeric',
+ });
+ }
+
+ private groupMessagesByDate(
+ messages: IgcMessage[]
+ ): { date: string; messages: IgcMessage[] }[] {
+ const grouped: { [key: string]: IgcMessage[] } = {};
+
+ messages.forEach((message) => {
+ const dateStr = this.formatDate(message.timestamp);
+ if (!grouped[dateStr]) {
+ grouped[dateStr] = [];
+ }
+ grouped[dateStr].push(message);
+ });
+
+ return Object.keys(grouped).map((date) => ({
+ date,
+ messages: grouped[date],
+ }));
+ }
+
+ private scrollToBottom() {
+ requestAnimationFrame(() => {
+ const container = this.shadowRoot?.host as HTMLElement;
+ if (container) {
+ container.scrollTop = container.scrollHeight;
+ }
+ });
+ }
+
+ protected override updated() {
+ if (!this.disableAutoScroll) {
+ this.scrollToBottom();
+ }
+ }
+
+ protected override firstUpdated() {
+ if (!this.disableAutoScroll) {
+ this.scrollToBottom();
+ }
+ }
+
+ protected override render() {
+ const groupedMessages = this.groupMessagesByDate(this.messages);
+
+ return html`
+
+ ${repeat(
+ groupedMessages,
+ (group) => group.date,
+ (group) => html`
+
${group.date}
+ ${repeat(
+ group.messages,
+ (message) => message.id,
+ (message) => html`
+
+ `
+ )}
+ `
+ )}
+ ${this.typingUsers.filter((u) => u !== this.user).length > 0
+ ? html`
+
+ `
+ : ''}
+
+ `;
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'igc-chat-message-list': IgcChatMessageListComponent;
+ }
+}
diff --git a/src/components/chat/chat-message.ts b/src/components/chat/chat-message.ts
new file mode 100644
index 000000000..153b19920
--- /dev/null
+++ b/src/components/chat/chat-message.ts
@@ -0,0 +1,147 @@
+import { LitElement, html } from 'lit';
+import { property } from 'lit/decorators.js';
+import { ifDefined } from 'lit/directives/if-defined.js';
+import IgcAvatarComponent from '../avatar/avatar.js';
+import { registerComponent } from '../common/definitions/register.js';
+import { IgcMessageAttachmentsComponent } from './message-attachments.js';
+import { IgcMessageReactionsComponent } from './message-reactions.js';
+import { styles } from './themes/message.base.css.js';
+import type { IgcMessage, IgcUser } from './types.js';
+
+/**
+ *
+ * @element igc-chat-message
+ *
+ */
+export default class IgcChatMessageComponent extends LitElement {
+ /** @private */
+ public static readonly tagName = 'igc-chat-message';
+
+ public static override styles = styles;
+
+ /* blazorSuppress */
+ public static register() {
+ registerComponent(
+ IgcChatMessageComponent,
+ IgcMessageAttachmentsComponent,
+ IgcMessageReactionsComponent,
+ IgcAvatarComponent
+ );
+ }
+
+ @property({ reflect: true, attribute: false })
+ public message: IgcMessage | undefined;
+
+ @property({ reflect: true, attribute: false })
+ public user: IgcUser | undefined;
+
+ @property({ type: Boolean, attribute: 'hide-avatar' })
+ public hideAvatar = false;
+
+ @property({ type: Boolean, attribute: 'hide-user-name' })
+ public hideUserName = false;
+
+ @property({ type: Boolean, attribute: 'hide-meta-data' })
+ public hideMetaData = false;
+
+ @property({ type: Boolean, attribute: 'disable-reactions' })
+ public disableReactions = false;
+
+ private formatTime(date: Date | undefined): string | undefined {
+ return date?.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
+ }
+
+ private renderStatusIcon(status: string) {
+ if (status === 'sent') {
+ return '✓';
+ }
+
+ if (status === 'delivered') {
+ return '✓✓';
+ }
+
+ if (status === 'read') {
+ return '✓✓';
+ }
+ return '';
+ }
+
+ private handleAddReaction(e: CustomEvent) {
+ const { emojiId } = e.detail;
+
+ this.dispatchEvent(
+ new CustomEvent('add-reaction', {
+ detail: { messageId: this.message?.id, emojiId },
+ bubbles: true,
+ composed: true,
+ })
+ );
+ }
+
+ private isCurrentUser() {
+ return this.message?.sender.id === this.user?.id;
+ }
+
+ protected override render() {
+ const sender = this.message?.sender;
+ const containerClass = `message-container ${this.isCurrentUser() ? 'sent' : 'received'}`;
+
+ return html`
+
+ ${this.hideAvatar
+ ? ''
+ : html`
+ `}
+
+
+ ${this.hideUserName || this.isCurrentUser()
+ ? ''
+ : html`
${ifDefined(sender?.name)}`}
+ ${this.message?.text.trim()
+ ? html`
${this.message?.text}
`
+ : ''}
+ ${this.message?.attachments && this.message?.attachments.length > 0
+ ? html`
+ `
+ : ''}
+ ${this.hideMetaData
+ ? ''
+ : html`
+
+ ${this.formatTime(this.message?.timestamp)}
+ ${this.isCurrentUser()
+ ? html`${this.renderStatusIcon(
+ this.message?.status || 'sent'
+ )}`
+ : ''}
+
+ `}
+
+ ${this.disableReactions
+ ? ''
+ : html`
`}
+
+ `;
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'igc-chat-message': IgcChatMessageComponent;
+ }
+}
diff --git a/src/components/chat/chat.ts b/src/components/chat/chat.ts
new file mode 100644
index 000000000..b8565d120
--- /dev/null
+++ b/src/components/chat/chat.ts
@@ -0,0 +1,243 @@
+import { LitElement, html } from 'lit';
+import { property } from 'lit/decorators.js';
+import IgcButtonComponent from '../button/button.js';
+import { registerComponent } from '../common/definitions/register.js';
+import type { Constructor } from '../common/mixins/constructor.js';
+import { EventEmitterMixin } from '../common/mixins/event-emitter.js';
+import IgcChatInputComponent from './chat-input.js';
+import IgcChatMessageListComponent from './chat-message-list.js';
+import { styles } from './themes/chat.base.css.js';
+import type {
+ IgcMessage,
+ IgcMessageAttachment,
+ IgcMessageReaction,
+ IgcUser,
+} from './types.js';
+
+export interface IgcChatComponentEventMap {
+ igcMessageSend: CustomEvent;
+ igcTypingChange: CustomEvent;
+ igcAttachmentClick: CustomEvent;
+}
+
+export interface IgcTypingChangeEventArgs {
+ user: IgcUser;
+ isTyping: boolean;
+}
+
+/**
+ *
+ * @element igc-chat
+ *
+ */
+export default class IgcChatComponent extends EventEmitterMixin<
+ IgcChatComponentEventMap,
+ Constructor
+>(LitElement) {
+ /** @private */
+ public static readonly tagName = 'igc-chat';
+
+ public static styles = styles;
+
+ /* blazorSuppress */
+ public static register() {
+ registerComponent(
+ IgcChatComponent,
+ IgcChatInputComponent,
+ IgcChatMessageListComponent,
+ IgcButtonComponent
+ );
+ }
+
+ @property({ attribute: false })
+ public user: IgcUser | undefined;
+
+ @property({ reflect: true, attribute: false })
+ public messages: IgcMessage[] = [];
+
+ @property({ reflect: true, attribute: false })
+ public typingUsers: IgcUser[] = [];
+
+ @property({ type: Boolean, attribute: 'hide-avatar' })
+ public hideAvatar = false;
+
+ @property({ type: Boolean, attribute: 'hide-user-name' })
+ public hideUserName = false;
+
+ @property({ type: Boolean, attribute: 'hide-meta-data' })
+ public hideMetaData = false;
+
+ @property({ type: Boolean, attribute: 'disable-auto-scroll' })
+ public disableAutoScroll = false;
+
+ @property({ type: Boolean, attribute: 'disable-reactions' })
+ public disableReactions = false;
+
+ @property({ type: Boolean, attribute: 'disable-attachments' })
+ public disableAttachments = false;
+
+ @property({ type: Boolean, attribute: 'disable-emojis' })
+ public disableEmojis = false;
+
+ @property({ type: String, attribute: 'header-text', reflect: true })
+ public headerText = '';
+
+ public override connectedCallback() {
+ super.connectedCallback();
+ this.addEventListener(
+ 'add-reaction',
+ this.handleAddReaction as EventListener
+ );
+ this.addEventListener(
+ 'message-send',
+ this.handleSendMessage as EventListener
+ );
+ this.addEventListener(
+ 'attachment-click',
+ this.handleAttachmentClick as EventListener
+ );
+ }
+
+ public override disconnectedCallback() {
+ super.disconnectedCallback();
+ this.removeEventListener(
+ 'message-send',
+ this.handleSendMessage as EventListener
+ );
+ this.removeEventListener(
+ 'add-reaction',
+ this.handleAddReaction as EventListener
+ );
+ this.removeEventListener(
+ 'attachment-click',
+ this.handleAttachmentClick as EventListener
+ );
+ }
+
+ private handleSendMessage(e: CustomEvent) {
+ const text = e.detail.text;
+ const attachments = e.detail.attachments || [];
+
+ if ((!text.trim() && attachments.length === 0) || !this.user) return;
+
+ const newMessage: IgcMessage = {
+ id: Date.now().toString(),
+ text,
+ sender: this.user,
+ timestamp: new Date(),
+ status: 'sent',
+ attachments,
+ reactions: [],
+ };
+
+ this.messages = [...this.messages, newMessage];
+ this.emitEvent('igcMessageSend', { detail: newMessage });
+ }
+
+ private handleTypingChange(e: CustomEvent) {
+ const isTyping = e.detail.isTyping;
+ const user = this.user;
+ if (!user) return;
+ const typingArgs = { user, isTyping };
+ this.emitEvent('igcTypingChange', { detail: typingArgs });
+ }
+
+ private handleAddReaction(e: CustomEvent) {
+ const { messageId, emojiId } = e.detail;
+
+ if (!messageId) return;
+
+ this.messages = this.messages.map((message) => {
+ if (message.id === messageId) {
+ const userReaction = message.reactions?.find(
+ (r) => this.user && r.users.includes(this.user.id)
+ );
+
+ if (userReaction) {
+ // Remove reaction
+ message.reactions?.forEach((r) => {
+ if (r.id === userReaction.id) {
+ r.users = (r.users ?? []).filter((id) => id !== this.user?.id);
+ }
+ });
+
+ message.reactions =
+ message.reactions?.filter((r) => r.users.length > 0) || [];
+ }
+
+ const existingReaction = message.reactions?.find(
+ (r) => r.id === emojiId
+ );
+
+ if (existingReaction && userReaction?.id !== emojiId) {
+ // Update existing reaction
+ message.reactions?.forEach((r) => {
+ if (r.id === emojiId) {
+ if (this.user) {
+ r.users.push(this.user.id);
+ }
+ }
+ });
+
+ message.reactions = [...(message.reactions || [])];
+ }
+
+ if (!existingReaction && userReaction?.id !== emojiId) {
+ // Create new reaction
+ const newReaction: IgcMessageReaction = {
+ id: emojiId,
+ users: this.user ? [this.user.id] : [],
+ };
+
+ message.reactions = [...(message.reactions || []), newReaction];
+ }
+ }
+
+ return { ...message };
+ });
+ }
+
+ private handleAttachmentClick(e: CustomEvent) {
+ const attachmentArgs = e.detail.attachment;
+ this.emitEvent('igcAttachmentClick', { detail: attachmentArgs });
+ }
+
+ protected override render() {
+ return html`
+
+
+
+
+
+
+ `;
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'igc-chat': IgcChatComponent;
+ }
+}
diff --git a/src/components/chat/emoji-picker.ts b/src/components/chat/emoji-picker.ts
new file mode 100644
index 000000000..7dbfff193
--- /dev/null
+++ b/src/components/chat/emoji-picker.ts
@@ -0,0 +1,418 @@
+import { LitElement, html } from 'lit';
+import { property, query, state } from 'lit/decorators.js';
+import IgcButtonComponent from '../button/button.js';
+import IgcIconButtonComponent from '../button/icon-button.js';
+import { addRootClickHandler } from '../common/controllers/root-click.js';
+import { watch } from '../common/decorators/watch.js';
+import { registerComponent } from '../common/definitions/register.js';
+import { registerIconFromText } from '../icon/icon.registry.js';
+import IgcInputComponent from '../input/input.js';
+import IgcPopoverComponent from '../popover/popover.js';
+import { styles } from './themes/emoji-picker.base.css.js';
+import { emojiPickerIcon } from './types.js';
+
+export const EMOJI_CATEGORIES = [
+ {
+ name: 'Smileys & Emotion',
+ icon: '😊',
+ emojis: [
+ { id: 'grinning_face', emoji: '😀', name: 'Grinning Face' },
+ {
+ id: 'grinning_face_with_big_eyes',
+ emoji: '😃',
+ name: 'Grinning Face with Big Eyes',
+ },
+ {
+ id: 'grinning_face_with_smiling_eyes',
+ emoji: '😄',
+ name: 'Grinning Face with Smiling Eyes',
+ },
+ {
+ id: 'beaming_face_with_smiling_eyes',
+ emoji: '😁',
+ name: 'Beaming Face with Smiling Eyes',
+ },
+ {
+ id: 'grinning_squinting_face',
+ emoji: '😆',
+ name: 'Grinning Squinting Face',
+ },
+ {
+ id: 'grinning_face_with_sweat',
+ emoji: '😅',
+ name: 'Grinning Face with Sweat',
+ },
+ {
+ id: 'rolling_on_the_floor_laughing',
+ emoji: '🤣',
+ name: 'Rolling on the Floor Laughing',
+ },
+ {
+ id: 'face_with_tears_of_joy',
+ emoji: '😂',
+ name: 'Face with Tears of Joy',
+ },
+ {
+ id: 'slightly_smiling_face',
+ emoji: '🙂',
+ name: 'Slightly Smiling Face',
+ },
+ { id: 'upside_down_face', emoji: '🙃', name: 'Upside-Down Face' },
+ { id: 'winking_face', emoji: '😉', name: 'Winking Face' },
+ {
+ id: 'smiling_face_with_smiling_eyes',
+ emoji: '😊',
+ name: 'Smiling Face with Smiling Eyes',
+ },
+ {
+ id: 'smiling_face_with_halo',
+ emoji: '😇',
+ name: 'Smiling Face with Halo',
+ },
+ {
+ id: 'smiling_face_with_heart_eyes',
+ emoji: '😍',
+ name: 'Smiling Face with Heart-Eyes',
+ },
+ {
+ id: 'smiling_face_with_hearts',
+ emoji: '🥰',
+ name: 'Smiling Face with Hearts',
+ },
+ { id: 'face_blowing_a_kiss', emoji: '😘', name: 'Face Blowing a Kiss' },
+ ],
+ },
+ {
+ name: 'People & Body',
+ icon: '👋',
+ emojis: [
+ { id: 'waving_hand', emoji: '👋', name: 'Waving Hand' },
+ { id: 'raised_back_of_hand', emoji: '🤚', name: 'Raised Back of Hand' },
+ {
+ id: 'hand_with_fingers_splayed',
+ emoji: '🖐️',
+ name: 'Hand with Fingers Splayed',
+ },
+ { id: 'raised_hand', emoji: '✋', name: 'Raised Hand' },
+ { id: 'vulcan_salute', emoji: '🖖', name: 'Vulcan Salute' },
+ { id: 'ok_hand', emoji: '👌', name: 'OK Hand' },
+ { id: 'pinched_fingers', emoji: '🤌', name: 'Pinched Fingers' },
+ { id: 'pinching_hand', emoji: '🤏', name: 'Pinching Hand' },
+ { id: 'victory_hand', emoji: '✌️', name: 'Victory Hand' },
+ { id: 'crossed_fingers', emoji: '🤞', name: 'Crossed Fingers' },
+ { id: 'love_you_gesture', emoji: '🤟', name: 'Love-You Gesture' },
+ { id: 'sign_of_the_horns', emoji: '🤘', name: 'Sign of the Horns' },
+ { id: 'call_me_hand', emoji: '🤙', name: 'Call Me Hand' },
+ {
+ id: 'backhand_index_pointing_left',
+ emoji: '👈',
+ name: 'Backhand Index Pointing Left',
+ },
+ {
+ id: 'backhand_index_pointing_right',
+ emoji: '👉',
+ name: 'Backhand Index Pointing Right',
+ },
+ { id: 'thumbs_up', emoji: '👍', name: 'Thumbs Up' },
+ ],
+ },
+ {
+ name: 'Animals & Nature',
+ icon: '🐶',
+ emojis: [
+ { id: 'dog_face', emoji: '🐶', name: 'Dog Face' },
+ { id: 'cat_face', emoji: '🐱', name: 'Cat Face' },
+ { id: 'mouse_face', emoji: '🐭', name: 'Mouse Face' },
+ { id: 'hamster_face', emoji: '🐹', name: 'Hamster Face' },
+ { id: 'rabbit_face', emoji: '🐰', name: 'Rabbit Face' },
+ { id: 'fox_face', emoji: '🦊', name: 'Fox Face' },
+ { id: 'bear_face', emoji: '🐻', name: 'Bear Face' },
+ { id: 'panda_face', emoji: '🐼', name: 'Panda Face' },
+ { id: 'koala_face', emoji: '🐨', name: 'Koala Face' },
+ { id: 'tiger_face', emoji: '🐯', name: 'Tiger Face' },
+ { id: 'lion_face', emoji: '🦁', name: 'Lion Face' },
+ { id: 'cow_face', emoji: '🐮', name: 'Cow Face' },
+ { id: 'pig_face', emoji: '🐷', name: 'Pig Face' },
+ { id: 'frog_face', emoji: '🐸', name: 'Frog Face' },
+ { id: 'monkey_face', emoji: '🐵', name: 'Monkey Face' },
+ { id: 'chicken', emoji: '🐔', name: 'Chicken' },
+ ],
+ },
+ {
+ name: 'Food & Drink',
+ icon: '🍔',
+ emojis: [
+ { id: 'red_apple', emoji: '🍎', name: 'Red Apple' },
+ { id: 'pear', emoji: '🍐', name: 'Pear' },
+ { id: 'orange', emoji: '🍊', name: 'Orange' },
+ { id: 'lemon', emoji: '🍋', name: 'Lemon' },
+ { id: 'banana', emoji: '🍌', name: 'Banana' },
+ { id: 'watermelon', emoji: '🍉', name: 'Watermelon' },
+ { id: 'grapes', emoji: '🍇', name: 'Grapes' },
+ { id: 'strawberry', emoji: '🍓', name: 'Strawberry' },
+ { id: 'blueberries', emoji: '🫐', name: 'Blueberries' },
+ { id: 'melon', emoji: '🍈', name: 'Melon' },
+ { id: 'cherries', emoji: '🍒', name: 'Cherries' },
+ { id: 'peach', emoji: '🍑', name: 'Peach' },
+ { id: 'mango', emoji: '🥭', name: 'Mango' },
+ { id: 'pineapple', emoji: '🍍', name: 'Pineapple' },
+ { id: 'coconut', emoji: '🥥', name: 'Coconut' },
+ { id: 'kiwi_fruit', emoji: '🥝', name: 'Kiwi Fruit' },
+ ],
+ },
+ {
+ name: 'Travel & Places',
+ icon: '🚗',
+ emojis: [
+ { id: 'car', emoji: '🚗', name: 'Car' },
+ { id: 'taxi', emoji: '🚕', name: 'Taxi' },
+ {
+ id: 'sport_utility_vehicle',
+ emoji: '🚙',
+ name: 'Sport Utility Vehicle',
+ },
+ { id: 'bus', emoji: '🚌', name: 'Bus' },
+ { id: 'trolleybus', emoji: '🚎', name: 'Trolleybus' },
+ { id: 'racing_car', emoji: '🏎️', name: 'Racing Car' },
+ { id: 'police_car', emoji: '🚓', name: 'Police Car' },
+ { id: 'ambulance', emoji: '🚑', name: 'Ambulance' },
+ { id: 'fire_engine', emoji: '🚒', name: 'Fire Engine' },
+ { id: 'minibus', emoji: '🚐', name: 'Minibus' },
+ { id: 'pickup_truck', emoji: '🛻', name: 'Pickup Truck' },
+ { id: 'delivery_truck', emoji: '🚚', name: 'Delivery Truck' },
+ { id: 'articulated_lorry', emoji: '🚛', name: 'Articulated Lorry' },
+ { id: 'tractor', emoji: '🚜', name: 'Tractor' },
+ { id: 'kick_scooter', emoji: '🛴', name: 'Kick Scooter' },
+ { id: 'bicycle', emoji: '🚲', name: 'Bicycle' },
+ ],
+ },
+ {
+ name: 'Activities',
+ icon: '⚽',
+ emojis: [
+ { id: 'soccer_ball', emoji: '⚽', name: 'Soccer Ball' },
+ { id: 'basketball', emoji: '🏀', name: 'Basketball' },
+ { id: 'american_football', emoji: '🏈', name: 'American Football' },
+ { id: 'baseball', emoji: '⚾', name: 'Baseball' },
+ { id: 'softball', emoji: '🥎', name: 'Softball' },
+ { id: 'tennis', emoji: '🎾', name: 'Tennis' },
+ { id: 'volleyball', emoji: '🏐', name: 'Volleyball' },
+ { id: 'rugby_football', emoji: '🏉', name: 'Rugby Football' },
+ { id: 'flying_disc', emoji: '🥏', name: 'Flying Disc' },
+ { id: 'pool_8_ball', emoji: '🎱', name: 'Pool 8 Ball' },
+ { id: 'yo_yo', emoji: '🪀', name: 'Yo-Yo' },
+ { id: 'ping_pong', emoji: '🏓', name: 'Ping Pong' },
+ { id: 'badminton', emoji: '🏸', name: 'Badminton' },
+ { id: 'ice_hockey', emoji: '🏒', name: 'Ice Hockey' },
+ { id: 'field_hockey', emoji: '🏑', name: 'Field Hockey' },
+ { id: 'lacrosse', emoji: '🥍', name: 'Lacrosse' },
+ ],
+ },
+ {
+ name: 'Objects',
+ icon: '💡',
+ emojis: [
+ { id: 'watch', emoji: '⌚', name: 'Watch' },
+ { id: 'mobile_phone', emoji: '📱', name: 'Mobile Phone' },
+ {
+ id: 'mobile_phone_with_arrow',
+ emoji: '📲',
+ name: 'Mobile Phone with Arrow',
+ },
+ { id: 'laptop', emoji: '💻', name: 'Laptop' },
+ { id: 'keyboard', emoji: '⌨️', name: 'Keyboard' },
+ { id: 'desktop_computer', emoji: '🖥️', name: 'Desktop Computer' },
+ { id: 'printer', emoji: '🖨️', name: 'Printer' },
+ { id: 'computer_mouse', emoji: '🖱️', name: 'Computer Mouse' },
+ { id: 'trackball', emoji: '🖲️', name: 'Trackball' },
+ { id: 'joystick', emoji: '🕹️', name: 'Joystick' },
+ { id: 'clamp', emoji: '🗜️', name: 'Clamp' },
+ { id: 'computer_disk', emoji: '💽', name: 'Computer Disk' },
+ { id: 'floppy_disk', emoji: '💾', name: 'Floppy Disk' },
+ { id: 'optical_disk', emoji: '💿', name: 'Optical Disk' },
+ { id: 'dvd', emoji: '📀', name: 'DVD' },
+ { id: 'videocassette', emoji: '📼', name: 'Videocassette' },
+ ],
+ },
+ {
+ name: 'Symbols',
+ icon: '❤️',
+ emojis: [
+ { id: 'red_heart', emoji: '❤️', name: 'Red Heart' },
+ { id: 'orange_heart', emoji: '🧡', name: 'Orange Heart' },
+ { id: 'yellow_heart', emoji: '💛', name: 'Yellow Heart' },
+ { id: 'green_heart', emoji: '💚', name: 'Green Heart' },
+ { id: 'blue_heart', emoji: '💙', name: 'Blue Heart' },
+ { id: 'purple_heart', emoji: '💜', name: 'Purple Heart' },
+ { id: 'black_heart', emoji: '🖤', name: 'Black Heart' },
+ { id: 'white_heart', emoji: '🤍', name: 'White Heart' },
+ { id: 'brown_heart', emoji: '🤎', name: 'Brown Heart' },
+ { id: 'broken_heart', emoji: '💔', name: 'Broken Heart' },
+ { id: 'heart_exclamation', emoji: '❣️', name: 'Heart Exclamation' },
+ { id: 'two_hearts', emoji: '💕', name: 'Two Hearts' },
+ { id: 'revolving_hearts', emoji: '💞', name: 'Revolving Hearts' },
+ { id: 'beating_heart', emoji: '💓', name: 'Beating Heart' },
+ { id: 'growing_heart', emoji: '💗', name: 'Growing Heart' },
+ { id: 'sparkling_heart', emoji: '💖', name: 'Sparkling Heart' },
+ ],
+ },
+];
+
+/**
+ *
+ * @element igc-emoji-picker
+ *
+ */
+export default class IgcEmojiPickerComponent extends LitElement {
+ /** @private */
+ public static readonly tagName = 'igc-emoji-picker';
+
+ public static override styles = styles;
+
+ protected _rootClickController = addRootClickHandler(this);
+
+ /* blazorSuppress */
+ public static register() {
+ registerComponent(
+ IgcEmojiPickerComponent,
+ IgcPopoverComponent,
+ IgcIconButtonComponent,
+ IgcButtonComponent,
+ IgcInputComponent
+ );
+ }
+
+ /**
+ * Sets the open state of the component.
+ * @attr
+ */
+ @property({ type: Boolean, reflect: true })
+ public open = false;
+
+ @state()
+ private _target?: HTMLElement;
+
+ @query('slot[name="target"]', true)
+ protected trigger!: HTMLSlotElement;
+
+ @state()
+ private _activeCategory = 0;
+
+ @watch('open', { waitUntilFirstUpdate: true })
+ protected openStateChange() {
+ this._rootClickController.update();
+
+ if (!this.open) {
+ this._target = undefined;
+ this._rootClickController.update({ target: undefined });
+ }
+ }
+
+ constructor() {
+ super();
+ this._rootClickController.update({ hideCallback: this.handleClosing });
+ registerIconFromText('target', emojiPickerIcon, 'material');
+ }
+
+ protected handleClosing() {
+ this.hide();
+ }
+
+ public async hide(): Promise {
+ if (!this.open) {
+ return false;
+ }
+
+ this.open = false;
+
+ return true;
+ }
+
+ protected handleAnchorClick() {
+ this.open = !this.open;
+ }
+
+ private handleCategoryChange(index: number) {
+ this._activeCategory = index;
+ }
+
+ private handleEmojiClick(emojiId: string, emoji: string) {
+ this.dispatchEvent(
+ new CustomEvent('emoji-selected', {
+ detail: { emojiId, emoji },
+ bubbles: true,
+ composed: true,
+ })
+ );
+ this.hide();
+ }
+
+ private getFilteredEmojis() {
+ return EMOJI_CATEGORIES[this._activeCategory].emojis;
+ }
+
+ protected override render() {
+ const filteredEmojis = this.getFilteredEmojis();
+
+ return html`
+
+ e.stopPropagation()}
+ >
+
+ ${EMOJI_CATEGORIES.map(
+ (category, index) => html`
+ this.handleCategoryChange(index)}
+ title=${category.name}
+ >
+ ${category.icon}
+
+ `
+ )}
+
+
+
+ ${filteredEmojis.map(
+ (emoji) => html`
+
this.handleEmojiClick(emoji.id, emoji.emoji)}>
+ ${emoji.emoji}
+
+ `
+ )}
+ ${filteredEmojis.length === 0
+ ? html`
+ No emojis found
+
`
+ : ''}
+
+
+ `;
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'igc-chat-igc-emoji-picker': IgcEmojiPickerComponent;
+ }
+}
diff --git a/src/components/chat/message-attachments.ts b/src/components/chat/message-attachments.ts
new file mode 100644
index 000000000..58f7dbaef
--- /dev/null
+++ b/src/components/chat/message-attachments.ts
@@ -0,0 +1,163 @@
+import { LitElement, html } from 'lit';
+import { property } from 'lit/decorators.js';
+import IgcIconButtonComponent from '../button/icon-button.js';
+import { registerComponent } from '../common/definitions/register.js';
+import IgcExpansionPanelComponent from '../expansion-panel/expansion-panel.js';
+import IgcIconComponent from '../icon/icon.js';
+import { registerIconFromText } from '../icon/icon.registry.js';
+import { styles } from './themes/message-attachments.base.css';
+import {
+ type IgcMessageAttachment,
+ closeIcon,
+ fileIcon,
+ imageIcon,
+ moreIcon,
+ previewIcon,
+} from './types.js';
+
+/**
+ *
+ * @element igc-message-attachments
+ *
+ */
+export class IgcMessageAttachmentsComponent extends LitElement {
+ /** @private */
+ public static readonly tagName = 'igc-message-attachments';
+
+ public static override styles = styles;
+
+ /* blazorSuppress */
+ public static register() {
+ registerComponent(
+ IgcMessageAttachmentsComponent,
+ IgcIconComponent,
+ IgcIconButtonComponent,
+ IgcExpansionPanelComponent
+ );
+ }
+ @property({ type: Array })
+ attachments: IgcMessageAttachment[] = [];
+
+ @property({ type: String })
+ previewImage = '';
+
+ constructor() {
+ super();
+ registerIconFromText('close', closeIcon, 'material');
+ registerIconFromText('file', fileIcon, 'material');
+ registerIconFromText('image', imageIcon, 'material');
+ registerIconFromText('preview', previewIcon, 'material');
+ registerIconFromText('more', moreIcon, 'material');
+ }
+
+ private formatFileSize(bytes = 0): string {
+ if (bytes < 1024) return `${bytes} B`;
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
+ }
+
+ private openImagePreview(url: string) {
+ this.previewImage = url;
+ }
+
+ private closeImagePreview() {
+ this.previewImage = '';
+ }
+
+ private handleToggle(e: CustomEvent, attachment: IgcMessageAttachment) {
+ this.handleAttachmentClick(attachment);
+ e.preventDefault();
+ }
+
+ private handleAttachmentClick(attachment: IgcMessageAttachment) {
+ this.dispatchEvent(
+ new CustomEvent('attachment-click', {
+ detail: { attachment },
+ bubbles: true,
+ composed: true,
+ })
+ );
+ }
+
+ protected override render() {
+ return html`
+
+ ${this.attachments.map(
+ (attachment) => html`
+
+ this.handleToggle(ev, attachment)}
+ @igcOpening=${(ev: CustomEvent) =>
+ this.handleToggle(ev, attachment)}
+ >
+
+
+ ${attachment.type === 'image'
+ ? html``
+ : html``}
+ ${attachment.name}
+
+
+ ${attachment.type === 'image'
+ ? html` this.openImagePreview(attachment.url)}
+ >`
+ : ''}
+
+
+
+
+ ${attachment.type === 'image'
+ ? html`
`
+ : ''}
+
+ `
+ )}
+
+
+ ${this.previewImage
+ ? html`
+
+

+
+
+ `
+ : ''}
+ `;
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'igc-message-attachmants': IgcMessageAttachmentsComponent;
+ }
+}
diff --git a/src/components/chat/message-reactions.ts b/src/components/chat/message-reactions.ts
new file mode 100644
index 000000000..1e6665ba0
--- /dev/null
+++ b/src/components/chat/message-reactions.ts
@@ -0,0 +1,113 @@
+import { LitElement, html } from 'lit';
+import { property } from 'lit/decorators.js';
+import IgcButtonComponent from '../button/button.js';
+import IgcIconButtonComponent from '../button/icon-button.js';
+import { registerComponent } from '../common/definitions/register.js';
+import IgcEmojiPickerComponent, { EMOJI_CATEGORIES } from './emoji-picker.js';
+import { styles } from './themes/reaction.base.css';
+import type { IgcMessageReaction } from './types.js';
+
+/**
+ *
+ * @element igc-message-reactions
+ *
+ */
+export class IgcMessageReactionsComponent extends LitElement {
+ /** @private */
+ public static readonly tagName = 'igc-message-reactions';
+
+ public static override styles = styles;
+
+ /* blazorSuppress */
+ public static register() {
+ registerComponent(
+ IgcMessageReactionsComponent,
+ IgcButtonComponent,
+ IgcIconButtonComponent,
+ IgcEmojiPickerComponent
+ );
+ }
+
+ @property({ type: Array })
+ reactions: IgcMessageReaction[] = [];
+
+ @property({ type: String })
+ messageId = '';
+
+ @property({ type: String })
+ currentUserId = '';
+
+ public override connectedCallback() {
+ super.connectedCallback();
+ }
+
+ public override disconnectedCallback() {
+ super.disconnectedCallback();
+ }
+
+ private addEmoji(e: CustomEvent) {
+ const { emojiId } = e.detail;
+ this.toggleReaction(emojiId);
+ }
+
+ private hasUserReacted(reaction: IgcMessageReaction): boolean {
+ return reaction.users.includes(this.currentUserId);
+ }
+
+ private toggleReaction(emojiId: string) {
+ this.dispatchEvent(
+ new CustomEvent('add-reaction', {
+ detail: { emojiId },
+ bubbles: true,
+ composed: true,
+ })
+ );
+ }
+
+ private getReactionById(reaction: IgcMessageReaction) {
+ for (const category of EMOJI_CATEGORIES) {
+ const foundReaction = category.emojis.find(
+ (emoji) => emoji.id === reaction.id
+ );
+ if (foundReaction) {
+ return {
+ id: foundReaction.id,
+ emoji: foundReaction.emoji,
+ count: reaction.users.length,
+ users: reaction.users,
+ };
+ }
+ }
+ return undefined;
+ }
+
+ protected override render() {
+ return html`
+
+ ${this.reactions?.map((_reaction) => {
+ const reaction = this.getReactionById(_reaction);
+ return reaction
+ ? html`
+ this.toggleReaction(reaction.id)}
+ >
+ ${reaction.emoji}
+ ${reaction.count}
+
+ `
+ : html``;
+ })}
+
+
+
+ `;
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'igc-message-reactions': IgcMessageReactionsComponent;
+ }
+}
diff --git a/src/components/chat/themes/chat.base.scss b/src/components/chat/themes/chat.base.scss
new file mode 100644
index 000000000..51f4be0b0
--- /dev/null
+++ b/src/components/chat/themes/chat.base.scss
@@ -0,0 +1,77 @@
+@use 'styles/common/component';
+@use 'styles/utilities' as *;
+
+:host {
+ width: 100%;
+ height: 600px;
+ border-radius: 12px;
+ overflow: hidden;
+ box-shadow: 0 8px 24px #1f1f1f;
+ display: flex;
+ flex-direction: column;
+ }
+
+ .chat-container {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ }
+
+ .header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 10px;
+ }
+
+ .info {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ }
+
+ .avatar {
+ width: 40px;
+ height: 40px;
+ border-radius: 50%;
+ object-fit: cover;
+ }
+
+ .avatar-container {
+ position: relative;
+ }
+
+ .status-indicator {
+ position: absolute;
+ bottom: 0;
+ right: 0;
+ width: 12px;
+ height: 12px;
+ border-radius: 50%;
+ background-color: #30D158;
+ border: 2px solid white;
+ }
+
+ .actions {
+ display: flex;
+ gap: 16px;
+ }
+
+ .action-button {
+ background: none;
+ border: none;
+ color: #0A84FF;
+ cursor: pointer;
+ font-size: 1.2rem;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 40px;
+ height: 40px;
+ border-radius: 50%;
+ transition: white 0.2s;
+ }
+
+ .action-button:hover {
+ background-color: #E5E5EA;
+ }
\ No newline at end of file
diff --git a/src/components/chat/themes/emoji-picker.base.scss b/src/components/chat/themes/emoji-picker.base.scss
new file mode 100644
index 000000000..c74ddd036
--- /dev/null
+++ b/src/components/chat/themes/emoji-picker.base.scss
@@ -0,0 +1,99 @@
+:host {
+ display: block;
+ }
+
+ .emoji-picker-container {
+ width: 250px;
+ max-width: 100vw;
+ background-color: white;
+ border-radius: 0.5rem;
+ box-shadow: 0 4px 6px -1px #292929, 0 2px 4px -1px #161616;
+ overflow: hidden;
+ display: flex;
+ flex-direction: column;
+ }
+
+ .emoji-categories {
+ display: flex;
+ padding: 0.5rem;
+ border-bottom: 1px solid #bdbcbc;
+ overflow-x: auto;
+ scrollbar-width: thin;
+ }
+
+ .emoji-categories::-webkit-scrollbar {
+ display: none;
+ }
+
+ .category-button {
+ background: transparent;
+ border: none;
+ font-size: 1.25rem;
+ width: 2rem;
+ height: 2rem;
+ border-radius: 0.25rem;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+ flex-shrink: 0;
+ margin-right: 0.25rem;
+ padding: 0;
+
+ igc-button::part(base){
+ display: none;
+ }
+ }
+
+ .category-button.active {
+ background-color: #cfcfcf;
+ }
+
+ .category-button:hover {
+ background-color: #cfcfcf;
+ }
+
+ .emoji-grid {
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ gap: 0.25rem;
+ padding: 0.5rem;
+ height: 150px;
+ overflow: auto;
+ }
+
+ .emoji-button {
+ font-size: 1.5rem;
+ background: none;
+ border: none;
+ cursor: pointer;
+ border-radius: 0.25rem;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ height: 2rem;
+ transition: transform 0.1s;
+ }
+
+ .emoji-button:hover {
+ background-color: #cfcfcf;
+ transform: scale(1.1);
+ }
+
+ .emoji-search {
+ padding: 0.5rem;
+ border-bottom: 1px solid #a0a0a0;
+ }
+
+ .search-input {
+ width: 100%;
+ padding: 0.5rem;
+ border-radius: 0.25rem;
+ border: 1px solid #85878a;
+ font-size: 0.875rem;
+ }
+
+ .search-input:focus {
+ outline: none;
+ border-color:#0A84FF;
+ }
diff --git a/src/components/chat/themes/input.base.scss b/src/components/chat/themes/input.base.scss
new file mode 100644
index 000000000..d7f45ea67
--- /dev/null
+++ b/src/components/chat/themes/input.base.scss
@@ -0,0 +1,127 @@
+@use 'styles/common/component';
+@use 'styles/utilities' as *;
+
+:host {
+ display: block;
+ padding: 12px 16px;
+ border-top: 1px solid #E5E5EA;
+}
+
+igc-file-input{
+ width: fit-content;
+}
+
+igc-file-input::part(file-names){
+ display: none;
+}
+
+.input-container {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+}
+
+.input-wrapper {
+ flex: 1;
+ position: relative;
+ border-radius: 24px;
+ overflow: hidden;
+ transition: box-shadow 0.2s;
+}
+
+.buttons-container {
+ display: flex;
+ align-items: center;
+}
+
+.input-button {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 2rem;
+ height: 2rem;
+ border-radius: 50%;
+ margin-left: 0.25rem;
+ background: transparent;
+ border: none;
+ outline: none;
+ cursor: pointer;
+ color: #8E8E93;
+ transition: all 0.2s ease;
+}
+
+.input-button:hover {
+ color: #0A84FF;
+ background-color: #a1a1a1;
+}
+
+.text-input {
+ width: 100%;
+ border: none;
+ padding: 12px 16px;
+ font-size: 0.95rem;
+ line-height: 1.5;
+ outline: none;
+ resize: none;
+ max-height: 120px;
+ font-family: inherit;
+}
+
+.input-wrapper:focus-within {
+ box-shadow: 0 0 0 2px #0A84FF;
+}
+
+.attachment-button,
+.send-button {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 40px;
+ height: 40px;
+ border-radius: 50%;
+ background-color: transparent;
+ border: none;
+ cursor: pointer;
+ color: #0A84FF;
+ transition: white 0.2s;
+}
+
+.attachment-button:hover,
+.send-button:hover {
+ background-color: #8E8E93;
+}
+
+.attachment-wrapper {
+ border: #C7C7CC solid 1px;
+ width: fit-content;
+ padding: 5px;
+ border-radius: 50px;
+}
+
+.attachment-name {
+ font-size: small;
+ font-style: italic;
+ margin: 0 5px;
+}
+
+.send-button {
+ background-color: #0A84FF;
+ color: white;
+}
+
+.send-button:hover {
+ background-color: #5AC8FA;
+}
+
+.send-button:disabled {
+ background-color: #C7C7CC;
+ cursor: not-allowed;
+}
+
+.emoji-picker-container {
+ position: absolute;
+ right: 20px;
+ margin-bottom: 0.5rem;
+ z-index: 10;
+}
+
diff --git a/src/components/chat/themes/message-attachments.base.scss b/src/components/chat/themes/message-attachments.base.scss
new file mode 100644
index 000000000..69203bb99
--- /dev/null
+++ b/src/components/chat/themes/message-attachments.base.scss
@@ -0,0 +1,109 @@
+@use 'styles/common/component';
+@use 'styles/utilities' as *;
+
+:host {
+ display: block;
+ margin-top: 0.5rem;
+}
+
+.attachments-container {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.5rem;
+}
+
+.attachment-preview {
+ position: relative;
+ border-radius: 0.375rem;
+ overflow: hidden;
+ max-width: 200px;
+}
+
+.image-attachment {
+ max-width: 200px;
+ max-height: 150px;
+ object-fit: cover;
+ cursor: pointer;
+ border-radius: 0.375rem;
+}
+
+.file-attachment {
+ display: flex;
+ align-items: center;
+ padding: 0.5rem;
+ background-color: var(--gray-100);
+ border-radius: 0.375rem;
+ max-width: 200px;
+}
+
+.file-icon {
+ margin-right: 0.5rem;
+ color: var(--gray-600);
+}
+
+.file-info {
+ overflow: hidden;
+}
+
+.file-name {
+ font-size: 0.75rem;
+ font-weight: 500;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ color: var(--gray-800);
+}
+
+.file-size {
+ font-size: 0.625rem;
+ color: var(--gray-500);
+}
+
+.image-overlay {
+ position: fixed;
+ inset: 0;
+ background-color: #1f1f1f;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 1000;
+}
+
+.overlay-image {
+ max-width: 90%;
+ max-height: 90%;
+}
+
+.close-overlay {
+ position: absolute;
+ top: 1rem;
+ right: 1rem;
+ color: white;
+ background: #1f1f1f;
+ width: 2rem;
+ height: 2rem;
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+ border: none;
+}
+
+.close-overlay:hover {
+ background: #1f1f1f;
+}
+
+.large {
+ --ig-size: var(--ig-size-large);
+}
+
+.actions {
+ display: flex;
+}
+
+.attachment {
+ display: flex;
+ justify-content: space-between;
+ gap: 2rem;
+}
\ No newline at end of file
diff --git a/src/components/chat/themes/message-list.base.scss b/src/components/chat/themes/message-list.base.scss
new file mode 100644
index 000000000..c14dda941
--- /dev/null
+++ b/src/components/chat/themes/message-list.base.scss
@@ -0,0 +1,90 @@
+@use 'styles/common/component';
+@use 'styles/utilities' as *;
+
+:host {
+ display: block;
+ flex: 1;
+ overflow-y: auto;
+ padding: 16px;
+}
+
+.message-list {
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+}
+
+.day-separator {
+ display: flex;
+ align-items: center;
+ margin: 16px 0;
+ color: #636366;
+ font-size: 0.8rem;
+}
+
+.day-separator::before,
+.day-separator::after {
+ content: '';
+ flex: 1;
+ height: 1px;
+ background-color: #a5a5a5;
+ margin: 0 8px;
+}
+
+.typing-indicator {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ padding: 8px;
+ margin-top: 8px;
+ animation: fadeIn 0.3s ease-in;
+}
+
+.typing-dot {
+ width: 8px;
+ height: 8px;
+ border-radius: 50%;
+ background-color: #7e7e81;
+ opacity: 0.6;
+}
+
+.typing-dot:nth-child(1) {
+ animation: bounce 1.2s infinite 0s;
+}
+
+.typing-dot:nth-child(2) {
+ animation: bounce 1.2s infinite 0.2s;
+}
+
+.typing-dot:nth-child(3) {
+ animation: bounce 1.2s infinite 0.4s;
+}
+
+@keyframes fade-in {
+ from {
+ opacity: 0;
+ }
+
+ to {
+ opacity: 1;
+ }
+}
+
+@keyframes bounce {
+ 0%,
+ 80%,
+ 100% {
+ transform: translateY(0);
+ }
+
+ 40% {
+ transform: translateY(-6px);
+ }
+}
+
+@media (prefers-color-scheme: dark) {
+ .day-separator::before,
+ .day-separator::after {
+ background-color: #525253;
+ }
+}
\ No newline at end of file
diff --git a/src/components/chat/themes/message.base.scss b/src/components/chat/themes/message.base.scss
new file mode 100644
index 000000000..42a6c7956
--- /dev/null
+++ b/src/components/chat/themes/message.base.scss
@@ -0,0 +1,114 @@
+@use 'styles/common/component';
+@use 'styles/utilities' as *;
+
+:host {
+ display: block;
+
+ --message-max-width: 75%;
+ }
+
+ .message-container {
+ display: flex;
+ justify-content: flex-start;
+ align-items: flex-end;
+ gap: 8px;
+ margin-bottom: 4px;
+ animation: fadeIn 0.2s ease-out;
+ }
+
+ .message-container.sent {
+ flex-direction: row-reverse;
+ }
+
+ .avatar {
+ width: 32px;
+ height: 32px;
+ border-radius: 50%;
+ object-fit: cover;
+ flex-shrink: 0;
+ opacity: 0;
+ }
+
+ .message-container.show-avatar .avatar {
+ opacity: 1;
+ }
+
+ .message-content {
+ display: flex;
+ flex-direction: column;
+ max-width: var(--message-max-width);
+ }
+
+ .bubble {
+ padding: 12px 16px;
+ border-radius: 18px;
+ background-color: #E5E5EA;
+ color: black;
+ word-break: break-all;
+ font-weight: 400;
+ line-height: 1.4;
+ position: relative;
+ transition: all 0.2s ease;
+ }
+
+ .sent .bubble {
+ border-radius: 18px 18px 4px;
+ background-color: #0A84FF;
+ color: white;
+ }
+
+ .received .bubble {
+ border-radius: 18px 18px 18px 4px;
+ }
+
+ .meta {
+ display: flex;
+ font-size: 0.7rem;
+ color: #636366;
+ margin-top: 4px;
+ opacity: 0.8;
+ }
+
+ .sent .meta {
+ justify-content: flex-end;
+ }
+
+ .time {
+ margin-right: 4px;
+ }
+
+ .status {
+ display: flex;
+ align-items: center;
+ }
+
+ .status-icon {
+ width: 16px;
+ height: 16px;
+ }
+
+ .reaction {
+ position: absolute;
+ bottom: -10px;
+ right: 8px;
+ background-color: white;
+ border-radius: 10px;
+ padding: 2px 5px;
+ font-size: 0.8rem;
+ box-shadow: 0 1px 2px #1f1f1f;
+ display: flex;
+ align-items: center;
+ z-index: 1;
+ }
+
+ @keyframes fade-in {
+ from {
+ opacity: 0;
+ transform: translateY(10px);
+ }
+
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+ }
diff --git a/src/components/chat/themes/reaction.base.scss b/src/components/chat/themes/reaction.base.scss
new file mode 100644
index 000000000..f2f5275dc
--- /dev/null
+++ b/src/components/chat/themes/reaction.base.scss
@@ -0,0 +1,72 @@
+@use 'styles/common/component';
+@use 'styles/utilities' as *;
+
+:host {
+ display: block;
+ margin-top: 0.25rem;
+ align-self: center;
+}
+
+.reactions-container {
+ display: flex;
+ gap: 0.25rem;
+}
+
+.reaction-button {
+ display: inline-flex;
+ align-items: center;
+ padding: 0.25rem 0.375rem;
+ border-radius: 1rem;
+ font-size: 0.875rem;
+ cursor: pointer;
+ transition: all 0.2s;
+ border: 1px solid transparent;
+}
+
+.reaction-button.active {
+ background-color: transparent;
+ color: white;
+}
+
+.reaction-button:hover {
+ background-color: transparent;
+}
+
+.reaction-button.active:hover {
+ background-color: transparent;
+ opacity: 0.9;
+}
+
+.emoji {
+ margin-right: 0.25rem;
+}
+
+.count {
+ font-size: 0.75rem;
+}
+
+.add-reaction {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ padding: 0.25rem;
+ background-color: transparent;
+ border-radius: 1rem;
+ font-size: 0.75rem;
+ cursor: pointer;
+ border: 1px dashed #7b7b7e;
+ color: #5b5b5c;
+ height: 1.5rem;
+ width: 1.5rem;
+}
+
+.add-reaction:hover {
+ background-color: #b8b8b9;
+ color: #636366;
+}
+
+.emoji-picker-container {
+ position: absolute;
+ margin-top: 0.25rem;
+ z-index: 10;
+}
\ No newline at end of file
diff --git a/src/components/chat/types.ts b/src/components/chat/types.ts
new file mode 100644
index 000000000..00f04b070
--- /dev/null
+++ b/src/components/chat/types.ts
@@ -0,0 +1,52 @@
+export type IgcMessageStatusType = 'sent' | 'delivered' | 'read';
+export type IgcMessageAttachmentType = 'image' | 'file';
+
+export interface IgcUser {
+ id: string;
+ name: string;
+ avatar: string;
+ status?: any;
+ //isTyping?: boolean;
+}
+
+export interface IgcMessage {
+ id: string;
+ text: string;
+ sender: IgcUser;
+ timestamp: Date;
+ chatId?: string;
+ status?: IgcMessageStatusType;
+ attachments?: IgcMessageAttachment[];
+ reactions?: IgcMessageReaction[];
+}
+
+export interface IgcMessageAttachment {
+ id: string;
+ type: IgcMessageAttachmentType;
+ url: string;
+ name: string;
+ size?: number;
+ thumbnail?: string;
+}
+
+export interface IgcMessageReaction {
+ id: string;
+ users: string[];
+}
+
+export const emojiPickerIcon =
+ '';
+export const attachmentIcon =
+ '';
+export const sendButtonIcon =
+ '';
+export const closeIcon =
+ '';
+export const fileIcon =
+ '';
+export const imageIcon =
+ '';
+export const moreIcon =
+ '';
+export const previewIcon =
+ '';
diff --git a/src/components/common/definitions/defineAllComponents.ts b/src/components/common/definitions/defineAllComponents.ts
index 14da35572..b0c033bd1 100644
--- a/src/components/common/definitions/defineAllComponents.ts
+++ b/src/components/common/definitions/defineAllComponents.ts
@@ -15,6 +15,7 @@ import IgcCardMediaComponent from '../../card/card.media.js';
import IgcCarouselIndicatorComponent from '../../carousel/carousel-indicator.js';
import IgcCarouselSlideComponent from '../../carousel/carousel-slide.js';
import IgcCarouselComponent from '../../carousel/carousel.js';
+import IgcChatComponent from '../../chat/chat.js';
import IgcCheckboxComponent from '../../checkbox/checkbox.js';
import IgcSwitchComponent from '../../checkbox/switch.js';
import IgcChipComponent from '../../chip/chip.js';
@@ -86,6 +87,7 @@ const allComponents: IgniteComponent[] = [
IgcCarouselComponent,
IgcCarouselIndicatorComponent,
IgcCarouselSlideComponent,
+ IgcChatComponent,
IgcCheckboxComponent,
IgcChipComponent,
IgcComboComponent,
diff --git a/src/index.ts b/src/index.ts
index 024c00237..8c3c38e9a 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -14,6 +14,7 @@ export { default as IgcCardMediaComponent } from './components/card/card.media.j
export { default as IgcCarouselComponent } from './components/carousel/carousel.js';
export { default as IgcCarouselIndicatorComponent } from './components/carousel/carousel-indicator.js';
export { default as IgcCarouselSlideComponent } from './components/carousel/carousel-slide.js';
+export { default as IgcChatComponent } from './components/chat/chat.js';
export { default as IgcCheckboxComponent } from './components/checkbox/checkbox.js';
export { default as IgcCircularProgressComponent } from './components/progress/circular-progress.js';
export { default as IgcCircularGradientComponent } from './components/progress/circular-gradient.js';
diff --git a/stories/chat.stories.ts b/stories/chat.stories.ts
new file mode 100644
index 000000000..8e7edfd03
--- /dev/null
+++ b/stories/chat.stories.ts
@@ -0,0 +1,223 @@
+import type { Meta, StoryObj } from '@storybook/web-components-vite';
+import { html } from 'lit';
+
+import { IgcChatComponent, defineComponents } from 'igniteui-webcomponents';
+
+defineComponents(IgcChatComponent);
+
+// region default
+const metadata: Meta = {
+ title: 'Chat',
+ component: 'igc-chat',
+ parameters: { docs: { description: { component: '' } } },
+ argTypes: {
+ hideAvatar: {
+ type: 'boolean',
+ control: 'boolean',
+ table: { defaultValue: { summary: 'false' } },
+ },
+ hideUserName: {
+ type: 'boolean',
+ control: 'boolean',
+ table: { defaultValue: { summary: 'false' } },
+ },
+ hideMetaData: {
+ type: 'boolean',
+ control: 'boolean',
+ table: { defaultValue: { summary: 'false' } },
+ },
+ disableAutoScroll: {
+ type: 'boolean',
+ control: 'boolean',
+ table: { defaultValue: { summary: 'false' } },
+ },
+ disableReactions: {
+ type: 'boolean',
+ control: 'boolean',
+ table: { defaultValue: { summary: 'false' } },
+ },
+ disableAttachments: {
+ type: 'boolean',
+ control: 'boolean',
+ table: { defaultValue: { summary: 'false' } },
+ },
+ disableEmojis: {
+ type: 'boolean',
+ control: 'boolean',
+ table: { defaultValue: { summary: 'false' } },
+ },
+ headerText: {
+ type: 'string',
+ control: 'text',
+ table: { defaultValue: { summary: '' } },
+ },
+ },
+ args: {
+ hideAvatar: false,
+ hideUserName: false,
+ hideMetaData: false,
+ disableAutoScroll: false,
+ disableReactions: false,
+ disableAttachments: false,
+ disableEmojis: false,
+ headerText: '',
+ },
+};
+
+export default metadata;
+
+interface IgcChatArgs {
+ hideAvatar: boolean;
+ hideUserName: boolean;
+ hideMetaData: boolean;
+ disableAutoScroll: boolean;
+ disableReactions: boolean;
+ disableAttachments: boolean;
+ disableEmojis: boolean;
+ headerText: string;
+}
+type Story = StoryObj;
+
+// endregion
+
+const userJohn: any = {
+ id: 'user1',
+ name: 'John',
+ avatar: 'https://www.infragistics.com/angular-demos/assets/images/men/1.jpg',
+};
+
+const userRichard: any = {
+ id: 'user2',
+ name: 'Richard',
+ avatar: 'https://www.infragistics.com/angular-demos/assets/images/men/2.jpg',
+};
+
+const userSam: any = {
+ id: 'user3',
+ name: 'Sam',
+ avatar: 'https://www.infragistics.com/angular-demos/assets/images/men/3.jpg',
+};
+
+const messages: any[] = [
+ {
+ id: '1',
+ text: 'Hey there! How are you doing today?',
+ sender: userRichard,
+ timestamp: new Date(2025, 4, 5),
+ status: 'read',
+ },
+ {
+ id: '2',
+ text: "I'm doing well, thanks for asking! How about you?",
+ sender: userJohn,
+ timestamp: new Date(Date.now() - 3500000),
+ status: 'read',
+ },
+ {
+ id: '3',
+ text: 'Pretty good! I was wondering if you wanted to grab coffee sometime this week?',
+ sender: userRichard,
+ timestamp: new Date(Date.now() - 3400000),
+ status: 'read',
+ reactions: [
+ {
+ id: 'red_heart',
+ users: ['user3'],
+ },
+ ],
+ },
+ {
+ id: '4',
+ text: 'Hi guys! I just joined the chat.',
+ sender: userSam,
+ timestamp: new Date(Date.now() - 3300000),
+ status: 'read',
+ attachments: [
+ {
+ id: 'men3_img',
+ type: 'image',
+ url: 'https://www.infragistics.com/angular-demos/assets/images/men/3.jpg',
+ name: 'men3.png',
+ },
+ ],
+ },
+];
+
+export const Basic: Story = {
+ render: (args) => html`
+
+
+ `,
+};
+
+function handleMessageEntered(e: CustomEvent) {
+ const newMessage = e.detail;
+ messages.push(newMessage);
+ const chatElements = document.querySelectorAll('igc-chat');
+ chatElements.forEach((chat) => {
+ chat.messages = [...messages];
+ });
+}
+
+function handleTypingChange(e: CustomEvent) {
+ const user = e.detail.user;
+ const isTyping = e.detail.isTyping;
+ const chatElements = document.querySelectorAll('igc-chat');
+ chatElements.forEach((chat) => {
+ if (!isTyping) {
+ chat.typingUsers = chat.typingUsers.filter((u) => u !== user);
+ } else if (isTyping && !chat.typingUsers.includes(user)) {
+ chat.typingUsers = [...chat.typingUsers, user];
+ }
+ });
+}
+
+export const SideBySide: Story = {
+ render: (args) => html`
+
+
+
+
+
+
+ `,
+};