Skip to content

Commit dd3fed1

Browse files
committed
refactor(chat): move common logic into chat state class
1 parent c3f5e30 commit dd3fed1

File tree

9 files changed

+358
-277
lines changed

9 files changed

+358
-277
lines changed

src/components/chat/chat-input.ts

Lines changed: 41 additions & 126 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { consume } from '@lit/context';
2-
import { LitElement, html } from 'lit';
3-
import { property, query, state } from 'lit/decorators.js';
2+
import { html, LitElement, nothing } from 'lit';
3+
import { query, state } from 'lit/decorators.js';
44
import IgcIconButtonComponent from '../button/icon-button.js';
55
import IgcChipComponent from '../chip/chip.js';
66
import { chatContext } from '../common/context.js';
@@ -10,13 +10,9 @@ import IgcFileInputComponent from '../file-input/file-input.js';
1010
import IgcIconComponent from '../icon/icon.js';
1111
import { registerIconFromText } from '../icon/icon.registry.js';
1212
import IgcTextareaComponent from '../textarea/textarea.js';
13-
import type IgcChatComponent from './chat.js';
13+
import type { ChatState } from './chat-state.js';
1414
import { styles } from './themes/input.base.css.js';
15-
import {
16-
type IgcMessageAttachment,
17-
attachmentIcon,
18-
sendButtonIcon,
19-
} from './types.js';
15+
import { attachmentIcon, sendButtonIcon } from './types.js';
2016

2117
/**
2218
*
@@ -29,7 +25,7 @@ export default class IgcChatInputComponent extends LitElement {
2925
public static override styles = styles;
3026

3127
@consume({ context: chatContext, subscribe: true })
32-
private _chat?: IgcChatComponent;
28+
private _chatState?: ChatState;
3329

3430
/* blazorSuppress */
3531
public static register() {
@@ -48,7 +44,7 @@ export default class IgcChatInputComponent extends LitElement {
4844

4945
@watch('acceptedFiles', { waitUntilFirstUpdate: true })
5046
protected acceptedFilesChange(): void {
51-
this.updateAcceptedTypesCache();
47+
this._chatState?.updateAcceptedTypesCache();
5248
}
5349

5450
@state()
@@ -57,16 +53,6 @@ export default class IgcChatInputComponent extends LitElement {
5753
@state()
5854
private dragClass = '';
5955

60-
@property({ attribute: false })
61-
public attachments: IgcMessageAttachment[] = [];
62-
63-
// Cache for accepted file types
64-
private _acceptedTypesCache: {
65-
extensions: Set<string>;
66-
mimeTypes: Set<string>;
67-
wildcardTypes: Set<string>;
68-
} | null = null;
69-
7056
constructor() {
7157
super();
7258
registerIconFromText('attachment', attachmentIcon, 'material');
@@ -75,46 +61,40 @@ export default class IgcChatInputComponent extends LitElement {
7561

7662
protected override firstUpdated() {
7763
this.setupDragAndDrop();
78-
this.updateAcceptedTypesCache();
64+
this._chatState?.updateAcceptedTypesCache();
7965
}
8066

8167
private handleInput(e: Event) {
8268
const target = e.target as HTMLTextAreaElement;
8369
this.inputValue = target.value;
70+
this._chatState?.handleInputChange(this.inputValue);
8471
this.adjustTextareaHeight();
85-
const inputEvent = new CustomEvent('input-change', {
86-
detail: { value: this.inputValue },
87-
});
88-
this.dispatchEvent(inputEvent);
8972
}
9073

9174
private handleKeyDown(e: KeyboardEvent) {
9275
if (e.key === 'Enter' && !e.shiftKey) {
9376
e.preventDefault();
9477
this.sendMessage();
9578
} else {
96-
const typingEvent = new CustomEvent('typing-change', {
79+
this._chatState?.emitEvent('igcTypingChange', {
9780
detail: { isTyping: true },
9881
});
99-
this.dispatchEvent(typingEvent);
82+
10083
// wait 3 seconds and dispatch a stop-typing event
10184
setTimeout(() => {
102-
const stopTypingEvent = new CustomEvent('typing-change', {
85+
this._chatState?.emitEvent('igcTypingChange', {
10386
detail: { isTyping: false },
10487
});
105-
this.dispatchEvent(stopTypingEvent);
10688
}, 3000);
10789
}
10890
}
10991

11092
private handleFocus() {
111-
const focusEvent = new CustomEvent('focus-input');
112-
this.dispatchEvent(focusEvent);
93+
this._chatState?.emitEvent('igcInputFocus');
11394
}
11495

11596
private handleBlur() {
116-
const blurEvent = new CustomEvent('blur-input');
117-
this.dispatchEvent(blurEvent);
97+
this._chatState?.emitEvent('igcInputBlur');
11898
}
11999

120100
private setupDragAndDrop() {
@@ -137,13 +117,12 @@ export default class IgcChatInputComponent extends LitElement {
137117
(item) => item.kind === 'file'
138118
);
139119
const hasValidFiles = files.some((item) =>
140-
this.isFileTypeAccepted(item.getAsFile() as File, item.type)
120+
this._chatState?.isFileTypeAccepted(item.getAsFile() as File, item.type)
141121
);
142122

143123
this.dragClass = hasValidFiles ? 'dragging' : '';
144124

145-
const dragEvent = new CustomEvent('drag-attachment');
146-
this.dispatchEvent(dragEvent);
125+
this._chatState?.emitEvent('igcAttachmentDrag');
147126
}
148127

149128
private handleDragOver(e: DragEvent) {
@@ -178,12 +157,14 @@ export default class IgcChatInputComponent extends LitElement {
178157
const files = Array.from(e.dataTransfer?.files || []);
179158
if (files.length === 0) return;
180159

181-
const validFiles = files.filter((file) => this.isFileTypeAccepted(file));
160+
const validFiles = files.filter((file) =>
161+
this._chatState?.isFileTypeAccepted(file)
162+
);
182163

183-
const dropEvent = new CustomEvent('drop-attachment');
184-
this.dispatchEvent(dropEvent);
164+
this._chatState?.emitEvent('igcAttachmentDrop');
185165

186-
this.attachFiles(validFiles);
166+
this._chatState?.attachFiles(validFiles);
167+
this.requestUpdate();
187168
}
188169

189170
private adjustTextareaHeight() {
@@ -196,13 +177,16 @@ export default class IgcChatInputComponent extends LitElement {
196177
}
197178

198179
private sendMessage() {
199-
if (!this.inputValue.trim() && this.attachments.length === 0) return;
180+
if (
181+
!this.inputValue.trim() &&
182+
this._chatState?.inputAttachments.length === 0
183+
)
184+
return;
200185

201-
const messageEvent = new CustomEvent('message-created', {
202-
detail: { text: this.inputValue, attachments: this.attachments },
186+
this._chatState?.addMessage({
187+
text: this.inputValue,
188+
attachments: this._chatState?.inputAttachments,
203189
});
204-
205-
this.dispatchEvent(messageEvent);
206190
this.inputValue = '';
207191

208192
if (this.textInputElement) {
@@ -219,92 +203,22 @@ export default class IgcChatInputComponent extends LitElement {
219203
if (!input.files || input.files.length === 0) return;
220204

221205
const files = Array.from(input.files);
222-
this.attachFiles(files);
223-
}
224-
225-
private attachFiles(files: File[]) {
226-
const newAttachments: IgcMessageAttachment[] = [];
227-
let count = this.attachments.length;
228-
files.forEach((file) => {
229-
const isImage = file.type.startsWith('image/');
230-
newAttachments.push({
231-
id: Date.now().toString() + count++,
232-
// type: isImage ? 'image' : 'file',
233-
url: URL.createObjectURL(file),
234-
name: file.name,
235-
file: file,
236-
thumbnail: isImage ? URL.createObjectURL(file) : undefined,
237-
});
238-
});
239-
240-
const attachmentEvent = new CustomEvent('attachment-change', {
241-
detail: [...this.attachments, ...newAttachments],
242-
});
243-
this.dispatchEvent(attachmentEvent);
244-
}
245-
246-
private updateAcceptedTypesCache() {
247-
if (!this._chat?.options?.acceptedFiles) {
248-
this._acceptedTypesCache = null;
249-
return;
250-
}
251-
252-
const types = this._chat?.options?.acceptedFiles
253-
.split(',')
254-
.map((type) => type.trim().toLowerCase());
255-
this._acceptedTypesCache = {
256-
extensions: new Set(types.filter((t) => t.startsWith('.'))),
257-
mimeTypes: new Set(
258-
types.filter((t) => !t.startsWith('.') && !t.endsWith('/*'))
259-
),
260-
wildcardTypes: new Set(
261-
types.filter((t) => t.endsWith('/*')).map((t) => t.slice(0, -2))
262-
),
263-
};
264-
}
265-
266-
private isFileTypeAccepted(file: File, type = ''): boolean {
267-
if (!this._acceptedTypesCache) return true;
268-
269-
if (file === null && type === '') return false;
270-
271-
const fileType =
272-
file != null ? file.type.toLowerCase() : type.toLowerCase();
273-
const fileExtension =
274-
file != null
275-
? `.${file.name.split('.').pop()?.toLowerCase()}`
276-
: `.${type.split('/').pop()?.toLowerCase()}`;
277-
278-
// Check file extension
279-
if (this._acceptedTypesCache.extensions.has(fileExtension)) {
280-
return true;
281-
}
282-
283-
// Check exact MIME type
284-
if (this._acceptedTypesCache.mimeTypes.has(fileType)) {
285-
return true;
286-
}
287-
288-
// Check wildcard MIME types
289-
const [fileBaseType] = fileType.split('/');
290-
return this._acceptedTypesCache.wildcardTypes.has(fileBaseType);
206+
this._chatState?.attachFiles(files);
207+
this.requestUpdate();
291208
}
292209

293210
private removeAttachment(index: number) {
294-
const attachmentEvent = new CustomEvent('attachment-change', {
295-
detail: this.attachments.filter((_, i) => i !== index),
296-
});
297-
298-
this.dispatchEvent(attachmentEvent);
211+
this._chatState?.removeAttachment(index);
212+
this.requestUpdate();
299213
}
300214

301215
private renderFileUploadArea() {
302-
return html` ${this._chat?.options?.disableAttachments
303-
? ''
216+
return html`${this._chatState?.options?.disableAttachments
217+
? nothing
304218
: html`
305219
<igc-file-input
306220
multiple
307-
.accept=${this._chat?.options?.acceptedFiles}
221+
.accept=${this._chatState?.options?.acceptedFiles}
308222
@igcChange=${this.handleFileUpload}
309223
>
310224
<igc-icon
@@ -317,21 +231,22 @@ export default class IgcChatInputComponent extends LitElement {
317231
}
318232

319233
private renderActionsArea() {
320-
return html` <div class="buttons-container">
234+
return html`<div class="buttons-container">
321235
<igc-icon-button
322236
name="send-message"
323237
collection="material"
324238
variant="contained"
325239
class="small"
326-
?disabled=${!this.inputValue.trim() && this.attachments.length === 0}
240+
?disabled=${!this.inputValue.trim() &&
241+
this._chatState?.inputAttachments.length === 0}
327242
@click=${this.sendMessage}
328243
></igc-icon-button>
329244
</div>`;
330245
}
331246

332247
private renderAttachmentsArea() {
333-
return html` <div>
334-
${this.attachments?.map(
248+
return html`<div>
249+
${this._chatState?.inputAttachments?.map(
335250
(attachment, index) => html`
336251
<div class="attachment-wrapper">
337252
<igc-chip

src/components/chat/chat-message-list.ts

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import { consume } from '@lit/context';
2-
import { LitElement, html } from 'lit';
2+
import { html, LitElement, nothing } from 'lit';
33
import { repeat } from 'lit/directives/repeat.js';
44
import { chatContext } from '../common/context.js';
55
import { registerComponent } from '../common/definitions/register.js';
66
import IgcChatMessageComponent from './chat-message.js';
7-
import type IgcChatComponent from './chat.js';
7+
import type { ChatState } from './chat-state.js';
88
import { styles } from './themes/message-list.base.css.js';
99
import type { IgcMessage } from './types.js';
1010

@@ -19,7 +19,7 @@ export default class IgcChatMessageListComponent extends LitElement {
1919
public static override styles = styles;
2020

2121
@consume({ context: chatContext, subscribe: true })
22-
private _chat?: IgcChatComponent;
22+
private _chatState?: ChatState;
2323

2424
/* blazorSuppress */
2525
public static register() {
@@ -75,20 +75,20 @@ export default class IgcChatMessageListComponent extends LitElement {
7575
}
7676

7777
protected override updated() {
78-
if (!this._chat?.options?.disableAutoScroll) {
78+
if (!this._chatState?.options?.disableAutoScroll) {
7979
this.scrollToBottom();
8080
}
8181
}
8282

8383
protected override firstUpdated() {
84-
if (!this._chat?.options?.disableAutoScroll) {
84+
if (!this._chatState?.options?.disableAutoScroll) {
8585
this.scrollToBottom();
8686
}
8787
}
8888

8989
protected *renderLoadingTemplate() {
90-
yield html` ${this._chat?.options?.templates?.composingIndicatorTemplate
91-
? this._chat.options.templates.composingIndicatorTemplate
90+
yield html`${this._chatState?.options?.templates?.composingIndicatorTemplate
91+
? this._chatState.options.templates.composingIndicatorTemplate
9292
: html`<div class="typing-indicator">
9393
<div class="typing-dot"></div>
9494
<div class="typing-dot"></div>
@@ -98,7 +98,7 @@ export default class IgcChatMessageListComponent extends LitElement {
9898

9999
protected override render() {
100100
const groupedMessages = this.groupMessagesByDate(
101-
this._chat?.messages ?? []
101+
this._chatState?.messages ?? []
102102
);
103103

104104
return html`
@@ -118,7 +118,9 @@ export default class IgcChatMessageListComponent extends LitElement {
118118
`
119119
)}
120120
${
121-
this._chat?.options?.isComposing ? this.renderLoadingTemplate() : ''
121+
this._chatState?.options?.isComposing
122+
? this.renderLoadingTemplate()
123+
: nothing
122124
}
123125
</div>
124126
</div>

0 commit comments

Comments
 (0)