Skip to content

Commit 0b0c82e

Browse files
committed
feat(chat): add initial chat implementation
1 parent 6f48208 commit 0b0c82e

19 files changed

+1831
-0
lines changed

src/components/chat/chat-header.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { LitElement, html } from 'lit';
2+
import { property } from 'lit/decorators.js';
3+
import { registerComponent } from '../common/definitions/register.js';
4+
import { styles } from './themes/header.base.css.js';
5+
6+
/**
7+
*
8+
* @element igc-chat-header
9+
*
10+
*/
11+
export default class IgcChatHeaderComponent extends LitElement {
12+
/** @private */
13+
public static readonly tagName = 'igc-chat-header';
14+
15+
public static override styles = styles;
16+
17+
/* blazorSuppress */
18+
public static register() {
19+
registerComponent(IgcChatHeaderComponent);
20+
}
21+
22+
@property({ type: String, reflect: true })
23+
public text = '';
24+
25+
protected override render() {
26+
return html`<div class="header">
27+
<div class="info">${this.text}</div>
28+
<div class="actions">
29+
<button class="action-button"></button>
30+
</div>
31+
</div>`;
32+
}
33+
}
34+
35+
declare global {
36+
interface HTMLElementTagNameMap {
37+
'igc-chat-header': IgcChatHeaderComponent;
38+
}
39+
}

src/components/chat/chat-input.ts

Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
import { LitElement, html } from 'lit';
2+
import { property, query, state } from 'lit/decorators.js';
3+
import IgcIconButtonComponent from '../button/icon-button.js';
4+
import IgcChipComponent from '../chip/chip.js';
5+
import { registerComponent } from '../common/definitions/register.js';
6+
import IgcFileInputComponent from '../file-input/file-input.js';
7+
import IgcIconComponent from '../icon/icon.js';
8+
import { registerIconFromText } from '../icon/icon.registry.js';
9+
import IgcTextareaComponent from '../textarea/textarea.js';
10+
import IgcEmojiPickerComponent from './emoji-picker.js';
11+
import { styles } from './themes/input.base.css.js';
12+
import {
13+
type IgcMessageAttachment,
14+
attachmentIcon,
15+
emojiPickerIcon,
16+
sendButtonIcon,
17+
} from './types.js';
18+
19+
/**
20+
*
21+
* @element igc-chat
22+
*
23+
*/
24+
export default class IgcChatInputComponent extends LitElement {
25+
/** @private */
26+
public static readonly tagName = 'igc-chat-input';
27+
28+
public static override styles = styles;
29+
30+
/* blazorSuppress */
31+
public static register() {
32+
registerComponent(
33+
IgcChatInputComponent,
34+
IgcTextareaComponent,
35+
IgcIconButtonComponent,
36+
IgcChipComponent,
37+
IgcEmojiPickerComponent,
38+
IgcFileInputComponent,
39+
IgcIconComponent
40+
);
41+
}
42+
43+
@property({ type: Boolean, attribute: 'enable-attachments' })
44+
public enableAttachments = true;
45+
46+
@property({ type: Boolean, attribute: 'enable-emoji-picker' })
47+
public enableEmojiPicker = true;
48+
49+
@query('textarea')
50+
private textInputElement!: HTMLTextAreaElement;
51+
52+
@state()
53+
private inputValue = '';
54+
55+
@state()
56+
private attachments: IgcMessageAttachment[] = [];
57+
58+
@state()
59+
private showEmojiPicker = false;
60+
61+
constructor() {
62+
super();
63+
registerIconFromText('emoji-picker', emojiPickerIcon, 'material');
64+
registerIconFromText('attachment', attachmentIcon, 'material');
65+
registerIconFromText('send-message', sendButtonIcon, 'material');
66+
}
67+
68+
private handleInput(e: Event) {
69+
const target = e.target as HTMLTextAreaElement;
70+
this.inputValue = target.value;
71+
this.adjustTextareaHeight();
72+
}
73+
74+
private handleKeyDown(e: KeyboardEvent) {
75+
if (e.key === 'Enter' && !e.shiftKey) {
76+
e.preventDefault();
77+
this.sendMessage();
78+
}
79+
}
80+
81+
private adjustTextareaHeight() {
82+
const textarea = this.textInputElement;
83+
if (!textarea) return;
84+
85+
textarea.style.height = 'auto';
86+
const newHeight = Math.min(textarea.scrollHeight, 120);
87+
textarea.style.height = `${newHeight}px`;
88+
}
89+
90+
private sendMessage() {
91+
if (!this.inputValue.trim() && this.attachments.length === 0) return;
92+
93+
const messageEvent = new CustomEvent('message-send', {
94+
detail: { text: this.inputValue, attachments: this.attachments },
95+
});
96+
97+
this.dispatchEvent(messageEvent);
98+
this.inputValue = '';
99+
this.attachments = [];
100+
101+
if (this.textInputElement) {
102+
this.textInputElement.style.height = 'auto';
103+
}
104+
105+
setTimeout(() => {
106+
this.textInputElement?.focus();
107+
}, 0);
108+
}
109+
110+
private toggleEmojiPicker() {
111+
this.showEmojiPicker = !this.showEmojiPicker;
112+
}
113+
114+
private addEmoji(e: CustomEvent) {
115+
const emoji = e.detail.emoji;
116+
this.inputValue += emoji;
117+
this.showEmojiPicker = false;
118+
119+
// Focus back on input after selecting an emoji
120+
this.updateComplete.then(() => {
121+
const textarea = this.shadowRoot?.querySelector('textarea');
122+
if (textarea) {
123+
textarea.focus();
124+
}
125+
});
126+
}
127+
128+
private handleFileUpload(e: Event) {
129+
const input = (e.target as any).input as HTMLInputElement;
130+
if (!input.files || input.files.length === 0) return;
131+
132+
const files = Array.from(input.files);
133+
const newAttachments: IgcMessageAttachment[] = [];
134+
files.forEach((file) => {
135+
const isImage = file.type.startsWith('image/');
136+
newAttachments.push({
137+
id: `attach_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
138+
type: isImage ? 'image' : 'file',
139+
url: URL.createObjectURL(file),
140+
name: file.name,
141+
size: file.size,
142+
thumbnail: isImage ? URL.createObjectURL(file) : undefined,
143+
});
144+
});
145+
this.attachments = [...this.attachments, ...newAttachments];
146+
}
147+
148+
private removeAttachment(index: number) {
149+
this.attachments = this.attachments.filter((_, i) => i !== index);
150+
}
151+
152+
protected override render() {
153+
return html`
154+
<div class="input-container">
155+
${this.enableAttachments
156+
? html`
157+
<igc-file-input multiple @igcChange=${this.handleFileUpload}>
158+
<igc-icon
159+
slot="file-selector-text"
160+
name="attachment"
161+
collection="material"
162+
></igc-icon>
163+
</igc-file-input>
164+
`
165+
: ''}
166+
167+
<div class="input-wrapper">
168+
<igc-textarea
169+
class="text-input"
170+
placeholder="Type a message..."
171+
rows="1"
172+
.value=${this.inputValue}
173+
@input=${this.handleInput}
174+
@keydown=${this.handleKeyDown}
175+
></igc-textarea>
176+
</div>
177+
178+
<div class="buttons-container">
179+
${this.enableEmojiPicker
180+
? html`
181+
<igc-icon-button
182+
name="emoji-picker"
183+
collection="material"
184+
variant="contained"
185+
class="small"
186+
@click=${this.toggleEmojiPicker}
187+
></igc-icon-button>
188+
`
189+
: ''}
190+
191+
<igc-icon-button
192+
name="send-message"
193+
collection="material"
194+
variant="contained"
195+
class="small"
196+
?disabled=${!this.inputValue.trim() &&
197+
this.attachments.length === 0}
198+
@click=${this.sendMessage}
199+
></igc-icon-button>
200+
</div>
201+
202+
${this.showEmojiPicker
203+
? html`
204+
<div class="emoji-picker-container">
205+
<igc-emoji-picker @emoji-selected=${this.addEmoji}></emoji-picker>
206+
</div>
207+
`
208+
: ''}
209+
</div>
210+
<div>
211+
${this.attachments?.map(
212+
(attachment, index) => html`
213+
<div class="attachment-wrapper">
214+
<igc-chip
215+
removable
216+
@igcRemove=${() => this.removeAttachment(index)}
217+
>
218+
<span class="attachment-name">${attachment.name}</span>
219+
</igc-chip>
220+
</div>
221+
`
222+
)}
223+
</div>
224+
`;
225+
}
226+
}
227+
228+
declare global {
229+
interface HTMLElementTagNameMap {
230+
'igc-chat-input': IgcChatInputComponent;
231+
}
232+
}

0 commit comments

Comments
 (0)