Skip to content

Commit 6814780

Browse files
committed
refactor(chat): Streamline rendering
* Abstracted chat input state into another context so it does not re-render the messages while the user is typing. * Various other cleanups
1 parent 95ab978 commit 6814780

File tree

10 files changed

+180
-181
lines changed

10 files changed

+180
-181
lines changed

src/components/chat/chat-input.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { until } from 'lit/directives/until.js';
77
import { addThemingController } from '../../theming/theming-controller.js';
88
import IgcIconButtonComponent from '../button/icon-button.js';
99
import IgcChipComponent from '../chip/chip.js';
10-
import { chatContext } from '../common/context.js';
10+
import { chatContext, chatUserInputContext } from '../common/context.js';
1111
import { addKeybindings } from '../common/controllers/key-bindings.js';
1212
import { watch } from '../common/decorators/watch.js';
1313
import { registerComponent } from '../common/definitions/register.js';
@@ -77,6 +77,9 @@ export default class IgcChatInputComponent extends LitElement {
7777
@consume({ context: chatContext, subscribe: true })
7878
private readonly _chatState!: ChatState;
7979

80+
@consume({ context: chatUserInputContext, subscribe: true })
81+
private readonly _userInputState!: ChatState;
82+
8083
private get _acceptedTypes() {
8184
return this._chatState.acceptedFileTypes;
8285
}
@@ -193,10 +196,10 @@ export default class IgcChatInputComponent extends LitElement {
193196
* @param e Input event from the text area
194197
*/
195198
private _handleInput({ detail }: CustomEvent<string>): void {
196-
if (detail === this._chatState?.inputValue) return;
199+
if (detail === this._chatState.inputValue) return;
197200

198201
this._chatState.inputValue = detail;
199-
this._chatState?.emitEvent('igcInputChange', { detail: { value: detail } });
202+
this._chatState.emitEvent('igcInputChange', { detail: { value: detail } });
200203
}
201204

202205
private _handleFileUpload(event: Event): void {
@@ -244,7 +247,7 @@ export default class IgcChatInputComponent extends LitElement {
244247
placeholder=${ifDefined(this._chatState?.options?.inputPlaceholder)}
245248
resize="auto"
246249
rows="1"
247-
.value=${this._chatState?.inputValue}
250+
.value=${this._userInputState?.inputValue}
248251
@igcInput=${this._handleInput}
249252
@focus=${this._handleFocusState}
250253
@blur=${this._handleFocusState}

src/components/chat/chat-message.ts

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -66,17 +66,15 @@ export default class IgcChatMessageComponent extends LitElement {
6666
* Injected chat state context. Provides message data, user info, and options.
6767
*/
6868
@consume({ context: chatContext, subscribe: true })
69-
private readonly _chatState?: ChatState;
69+
private readonly _state!: ChatState;
7070

71-
private readonly _defaults: Readonly<DefaultMessageRenderers> = Object.freeze(
72-
{
73-
message: () => this._renderMessage(),
74-
messageHeader: () => this._renderHeader(),
75-
messageContent: () => this._renderContent(),
76-
messageAttachments: () => this._renderAttachments(),
77-
messageActions: () => this._renderActions(),
78-
}
79-
);
71+
private readonly _defaults = Object.freeze<DefaultMessageRenderers>({
72+
message: () => this._renderMessage(),
73+
messageHeader: () => this._renderHeader(),
74+
messageContent: () => this._renderContent(),
75+
messageAttachments: () => this._renderAttachments(),
76+
messageActions: () => this._renderActions(),
77+
});
8078

8179
/**
8280
* The chat message to render.
@@ -94,8 +92,8 @@ export default class IgcChatMessageComponent extends LitElement {
9492
}
9593

9694
private _getRenderer(name: keyof DefaultMessageRenderers) {
97-
return this._chatState?.options?.renderers
98-
? (this._chatState.options.renderers[name] ?? this._defaults[name])
95+
return this._state.options?.renderers
96+
? (this._state.options.renderers[name] ?? this._defaults[name])
9997
: this._defaults[name];
10098
}
10199

@@ -120,7 +118,7 @@ export default class IgcChatMessageComponent extends LitElement {
120118
this.requestUpdate();
121119
}
122120

123-
this._chatState?.emitEvent('igcMessageReact', {
121+
this._state.emitEvent('igcMessageReact', {
124122
detail: { message: this.message, reaction },
125123
});
126124
}
@@ -136,11 +134,11 @@ export default class IgcChatMessageComponent extends LitElement {
136134
}
137135

138136
private _renderActions() {
139-
const isSent = this.message?.sender === this._chatState?.currentUserId;
137+
const isSent = this.message?.sender === this._state.currentUserId;
140138
const hasText = this.message?.text.trim();
141-
const isTyping = this._chatState?._isTyping;
142-
const isLastMessage = this.message === this._chatState?.messages.at(-1);
143-
const resourceStrings = this._chatState?.resourceStrings!;
139+
const isTyping = this._state._isTyping;
140+
const isLastMessage = this.message === this._state.messages.at(-1);
141+
const resourceStrings = this._state.resourceStrings!;
144142

145143
if (isSent || !hasText || (isLastMessage && isTyping)) {
146144
return nothing;
@@ -180,6 +178,8 @@ export default class IgcChatMessageComponent extends LitElement {
180178
variant="flat"
181179
@pointerenter=${({ target }: PointerEvent) =>
182180
showChatActionsTooltip(target as Element, tooltipMessage)}
181+
@focus=${({ target }: FocusEvent) =>
182+
showChatActionsTooltip(target as Element, tooltipMessage)}
183183
></igc-icon-button>
184184
`;
185185
}
@@ -220,10 +220,10 @@ export default class IgcChatMessageComponent extends LitElement {
220220

221221
const parts = {
222222
'message-container': true,
223-
sent: this._chatState?.currentUserId === this.message.sender,
223+
sent: this._state.isCurrentUserMessage(this.message),
224224
};
225225

226-
const options = this._chatState?.options;
226+
const options = this._state.options;
227227
const ctx = {
228228
param: this.message,
229229
defaults: this._defaults,

src/components/chat/chat-state.ts

Lines changed: 39 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { createRef, type Ref } from 'lit/directives/ref.js';
22
import { enterKey } from '../common/controllers/key-bindings.js';
33
import { IgcChatResourceStringEN } from '../common/i18n/chat.resources.js';
4+
import { nanoid } from '../common/util.js';
45
import type IgcTextareaComponent from '../textarea/textarea.js';
56
import type IgcChatComponent from './chat.js';
67
import type { IgcChatComponentEventMap } from './chat.js';
@@ -19,6 +20,10 @@ import { type ChatAcceptedFileTypes, parseAcceptedFileTypes } from './utils.js';
1920
export class ChatState {
2021
//#region Internal properties and state /** The host `<igc-chat>` component instance */
2122
private readonly _host: IgcChatComponent;
23+
24+
private readonly _contextUpdateFn: () => unknown;
25+
private readonly _userInputContextUpdateFn: () => unknown;
26+
2227
/** Reference to the text area input component */
2328
private _textArea: IgcTextareaComponent | null = null;
2429
/** The current list of messages */
@@ -85,11 +90,7 @@ export class ChatState {
8590
*/
8691
public set options(value: IgcChatOptions) {
8792
this._options = value;
88-
this._host.requestUpdate();
89-
// Notify context consumers about the state change
90-
if (this._host.updateContextValue) {
91-
this._host.updateContextValue();
92-
}
93+
this._contextUpdateFn.call(this._host);
9394
}
9495

9596
/**
@@ -146,11 +147,7 @@ export class ChatState {
146147
*/
147148
public set inputAttachments(value: IgcMessageAttachment[]) {
148149
this._inputAttachments = value;
149-
this._host.requestUpdate(); // Notify the host component to re-render
150-
// Notify context consumers about the state change
151-
if (this._host.updateContextValue) {
152-
this._host.updateContextValue();
153-
}
150+
this._userInputContextUpdateFn.call(this._host);
154151
}
155152

156153
/**
@@ -165,31 +162,27 @@ export class ChatState {
165162
*/
166163
public set inputValue(value: string) {
167164
this._inputValue = value;
168-
this._host.requestUpdate();
169-
// Notify context consumers about the state change
170-
if (this._host.updateContextValue) {
171-
this._host.updateContextValue();
172-
}
165+
this._userInputContextUpdateFn.call(this._host);
173166
}
174167

175168
//#endregion
176169

177-
/**
178-
* Creates an instance of ChatState.
179-
* @param chat The host `<igc-chat>` component.
180-
*/
181-
constructor(chat: IgcChatComponent) {
170+
constructor(
171+
chat: IgcChatComponent,
172+
contextUpdateFn: () => unknown,
173+
userInputContextUpdateFn: () => unknown
174+
) {
182175
this._host = chat;
176+
this._contextUpdateFn = contextUpdateFn;
177+
this._userInputContextUpdateFn = userInputContextUpdateFn;
178+
}
179+
180+
public isCurrentUserMessage(message?: IgcMessage): boolean {
181+
return this.currentUserId === message?.sender;
183182
}
184183

185184
//#region Event handlers
186185

187-
/**
188-
* Emits a custom event from the host component.
189-
* @param name Event name (key of IgcChatComponentEventMap)
190-
* @param args Event detail or options
191-
* @returns true if event was not canceled, false otherwise
192-
*/
193186
public emitEvent = (name: keyof IgcChatComponentEventMap, args?: any) => {
194187
return this._host.emitEvent(name, args);
195188
};
@@ -204,16 +197,10 @@ export class ChatState {
204197
* Clears input value and attachments on success.
205198
* @param message Partial message object with optional id, sender, timestamp
206199
*/
207-
public addMessage = (message: {
208-
id?: string;
209-
text: string;
210-
sender?: string;
211-
timestamp?: Date;
212-
attachments?: IgcMessageAttachment[];
213-
}): void => {
200+
public addMessage(message: Partial<IgcMessage>): void {
214201
const newMessage: IgcMessage = {
215-
id: message.id ?? Date.now().toString(),
216-
text: message.text,
202+
id: message.id ?? nanoid(),
203+
text: message.text ?? '',
217204
sender: message.sender ?? this.currentUserId,
218205
timestamp: message.timestamp ?? new Date(),
219206
attachments: message.attachments || [],
@@ -228,10 +215,12 @@ export class ChatState {
228215
if (!this.messages.some((msg) => msg.id === newMessage.id)) {
229216
this.messages = [...this.messages, newMessage];
230217
}
218+
this._host.requestUpdate('messages');
219+
231220
this.inputValue = '';
232221
this.inputAttachments = [];
233222
}
234-
};
223+
}
235224

236225
/**
237226
* Adds files as attachments to the input.
@@ -240,21 +229,26 @@ export class ChatState {
240229
*/
241230
public attachFiles(files: File[]) {
242231
const newAttachments: IgcMessageAttachment[] = [];
243-
let count = this.inputAttachments.length;
244-
files.forEach((file) => {
245-
if (this.inputAttachments.find((a) => a.name === file.name)) {
246-
return;
232+
const fileNames = new Set(
233+
this.inputAttachments.map((attachment) => attachment.file?.name ?? '')
234+
);
235+
236+
for (const file of files) {
237+
if (fileNames.has(file.name)) {
238+
continue;
247239
}
248240

249241
const isImage = file.type.startsWith('image/');
242+
const url = URL.createObjectURL(file);
243+
250244
newAttachments.push({
251-
id: Date.now().toString() + count++,
252-
url: URL.createObjectURL(file),
245+
id: nanoid(),
246+
url,
253247
name: file.name,
254-
file: file,
255-
thumbnail: isImage ? URL.createObjectURL(file) : undefined,
248+
file,
249+
thumbnail: isImage ? url : undefined,
256250
});
257-
});
251+
}
258252

259253
const allowed = this.emitEvent('igcAttachmentChange', {
260254
detail: [...this.inputAttachments, ...newAttachments],
@@ -389,7 +383,3 @@ export class ChatState {
389383

390384
//#endregion
391385
}
392-
393-
export function createChatState(host: IgcChatComponent): ChatState {
394-
return new ChatState(host);
395-
}

0 commit comments

Comments
 (0)