Skip to content

Commit 15d6214

Browse files
committed
refactor: Moved state functionality to appropriate inner components
* Segregate renderers into explicit containers to keep rendering hierarchy in React applications using Portal. * Tweaked chat types a bit. * Started to restructure unit test suite so it doesn't care that much for exact DOM representation.
1 parent 88848af commit 15d6214

File tree

6 files changed

+447
-1028
lines changed

6 files changed

+447
-1028
lines changed

src/components/chat/chat-input.ts

Lines changed: 16 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ type DefaultInputRenderers = {
3535
fileUploadButton: ChatTemplateRenderer<ChatRenderContext>;
3636
sendButton: ChatTemplateRenderer<ChatRenderContext>;
3737
};
38+
39+
/* blazorSuppress */
3840
/**
3941
* A web component that provides the input area for the `igc-chat` interface.
4042
*
@@ -113,6 +115,7 @@ export default class IgcChatInputComponent extends LitElement {
113115
addThemingController(this, all);
114116
}
115117

118+
/** @internal */
116119
public focusInput(): void {
117120
this._textInputElement.focus();
118121
}
@@ -145,9 +148,19 @@ export default class IgcChatInputComponent extends LitElement {
145148
this.focusInput();
146149
}
147150

151+
private _handleAttachmentRemoved(attachment: IgcChatMessageAttachment): void {
152+
const current = this._userInputState.inputAttachments;
153+
154+
if (this._state.emitAttachmentChange(attachment)) {
155+
this._state.inputAttachments = current.toSpliced(
156+
current.indexOf(attachment),
157+
1
158+
);
159+
}
160+
}
161+
148162
private _handleKeydown(event: KeyboardEvent): void {
149-
const isSendRequest =
150-
event.key === enterKey.toLowerCase() && !event.shiftKey;
163+
const isSendRequest = event.key === enterKey && !event.shiftKey;
151164

152165
if (isSendRequest) {
153166
event.preventDefault();
@@ -212,16 +225,7 @@ export default class IgcChatInputComponent extends LitElement {
212225
this.requestUpdate();
213226
}
214227

215-
/**
216-
* Handles input text changes.
217-
* Updates internal inputValue and emits 'igcInputChange' event.
218-
* @param e Input event from the text area
219-
*/
220228
private _handleInput({ detail }: CustomEvent<string>): void {
221-
if (detail === this._state.inputValue) {
222-
return;
223-
}
224-
225229
this._state.inputValue = detail;
226230
this._state.emitEvent('igcInputChange', { detail: { value: detail } });
227231
}
@@ -244,7 +248,7 @@ export default class IgcChatInputComponent extends LitElement {
244248
<div part="attachment-wrapper" role="listitem">
245249
<igc-chip
246250
removable
247-
@igcRemove=${() => this._state.removeAttachment(attachment)}
251+
@igcRemove=${() => this._handleAttachmentRemoved(attachment)}
248252
>
249253
<igc-icon
250254
slot="prefix"
@@ -279,11 +283,6 @@ export default class IgcChatInputComponent extends LitElement {
279283
`;
280284
}
281285

282-
/**
283-
* Default file upload button template used when no custom template is provided.
284-
* Renders a file input for attaching files.
285-
* @returns TemplateResult containing the file upload button
286-
*/
287286
private _renderFileUploadButton() {
288287
const accepted = this._state.options?.acceptedFiles;
289288
const attachmentsDisabled = this._state.options?.disableInputAttachments;
@@ -314,11 +313,6 @@ export default class IgcChatInputComponent extends LitElement {
314313
)}`;
315314
}
316315

317-
/**
318-
* Default send button template used when no custom template is provided.
319-
* Renders a send button that submits the current input value and attachments.
320-
* @returns TemplateResult containing the send button
321-
*/
322316
private _renderSendButton() {
323317
const enabled =
324318
this._state.hasInputValue || this._state.hasInputAttachments;

src/components/chat/chat-message.ts

Lines changed: 71 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -29,19 +29,18 @@ const COPY_CONTENT = 'copy_content';
2929
const REGENERATE = 'regenerate';
3030

3131
type DefaultMessageRenderers = {
32-
message: ChatTemplateRenderer<ChatMessageRenderContext>;
3332
messageHeader: ChatTemplateRenderer<ChatMessageRenderContext>;
3433
messageContent: ChatTemplateRenderer<ChatMessageRenderContext>;
3534
messageAttachments: ChatTemplateRenderer<ChatMessageRenderContext>;
3635
messageActions: ChatTemplateRenderer<ChatMessageRenderContext>;
3736
};
3837

38+
/* blazorSuppress */
3939
/**
4040
* A chat message component for displaying individual messages in `<igc-chat>`.
4141
*
4242
* @element igc-chat-message
4343
*
44-
* @fires igcMessageReact - Fired when a message is reacted to.
4544
*
4645
* This component renders a single chat message including:
4746
* - Message text (sanitized)
@@ -68,7 +67,6 @@ export default class IgcChatMessageComponent extends LitElement {
6867
}
6968

7069
private readonly _defaults = Object.freeze<DefaultMessageRenderers>({
71-
message: () => this._renderMessage(),
7270
messageHeader: () => this._renderHeader(),
7371
messageContent: () => this._renderContent(),
7472
messageAttachments: () => this._renderAttachments(),
@@ -82,7 +80,7 @@ export default class IgcChatMessageComponent extends LitElement {
8280
* The chat message to render.
8381
*/
8482
@property({ attribute: false })
85-
public message?: IgcChatMessage;
83+
public message!: IgcChatMessage;
8684

8785
constructor() {
8886
super();
@@ -99,36 +97,38 @@ export default class IgcChatMessageComponent extends LitElement {
9997
: this._defaults[name];
10098
}
10199

102-
private async _handleCopy() {
103-
if (!this.message) return;
100+
private async _handleCopy(): Promise<void> {
101+
const text = this.message.text;
102+
const separator = text ? '\n\n' : '';
103+
const attachments = this.message.attachments ?? [];
104+
const { attachmentLabel, attachmentsListLabel, messageCopied } =
105+
this._state.resourceStrings!;
104106

105-
let clipboardText = this.message.text;
107+
const attachmentsText = isEmpty(attachments)
108+
? ''
109+
: attachments
110+
.map(({ name, url }) => `${name ?? attachmentLabel}: ${url ?? ''}`)
111+
.join('\n');
106112

107-
const resourceStrings = this._state.resourceStrings!;
108-
if (this.message.attachments && !isEmpty(this.message.attachments)) {
109-
const attachmentList = this.message.attachments
110-
.map(
111-
(att) =>
112-
`${att.name ?? resourceStrings.attachmentLabel}: ${att.url ?? ''}`
113-
)
114-
.join('\n');
115-
clipboardText += `${clipboardText ? '\n\n' : ''}${resourceStrings.attachmentsListLabel}:\n${attachmentList}`;
116-
}
113+
const payload = attachmentsText
114+
? `${text}${separator}${attachmentsListLabel}:\n${attachmentsText}`
115+
: text;
117116

118-
if (navigator.clipboard?.writeText) {
119-
try {
120-
await navigator.clipboard.writeText(clipboardText);
121-
this._state.showActionToast(resourceStrings.messageCopied);
122-
} catch (err) {
123-
throw new Error(`Failed to copy message via Clipboard API: ${err}`);
124-
}
117+
try {
118+
await navigator.clipboard.writeText(payload);
119+
this._state.showActionToast(messageCopied);
120+
} catch (err) {
121+
throw new Error(`Failed to copy message: ${err}`);
125122
}
126123
}
127124

128125
private _handleMessageActionClick(event: PointerEvent): void {
129126
const targetButton = event.target as HTMLElement;
130127
const button = targetButton.closest(IgcIconButtonComponent.tagName);
131-
if (!button || !this.message) return;
128+
129+
if (!button) {
130+
return;
131+
}
132132

133133
let reaction = button.name;
134134

@@ -153,32 +153,26 @@ export default class IgcChatMessageComponent extends LitElement {
153153
reaction = REGENERATE;
154154
break;
155155
default:
156-
reaction = undefined;
156+
reaction = '';
157157
}
158158

159159
this.message.reactions = reaction ? [reaction] : [];
160+
this._state.emitMessageReaction({ message: this.message, reaction });
160161
this.requestUpdate();
161-
162-
this._state.emitEvent('igcMessageReact', {
163-
detail: { message: this.message, reaction },
164-
});
165162
}
166163

167164
private _renderHeader() {
168165
return nothing;
169166
}
170167

171168
private _renderContent() {
172-
return this.message?.text
173-
? html`<pre part="plain-text">${this.message.text}</pre>`
174-
: nothing;
169+
return html`${this.message.text}`;
175170
}
176171

177172
private _renderActions() {
178-
const isSent = this.message?.sender === this._state.currentUserId;
179-
const hasText = this.message?.text.trim();
180-
const hasAttachments =
181-
this.message?.attachments && !isEmpty(this.message?.attachments);
173+
const isSent = this.message.sender === this._state.currentUserId;
174+
const hasText = this.message.text.trim();
175+
const hasAttachments = !isEmpty(this.message.attachments ?? []);
182176
const isTyping = this._state._isTyping;
183177
const isLastMessage = this.message === this._state.messages.at(-1);
184178
const resourceStrings = this._state.resourceStrings!;
@@ -188,25 +182,23 @@ export default class IgcChatMessageComponent extends LitElement {
188182
}
189183

190184
return html`
191-
<div @click=${this._handleMessageActionClick} part="message-actions">
192-
${this._renderActionButton(COPY_CONTENT, resourceStrings.reactionCopy)}
193-
${this._renderActionButton(
194-
this.message?.reactions?.includes(LIKE_ACTIVE)
195-
? LIKE_ACTIVE
196-
: LIKE_INACTIVE,
197-
resourceStrings.reactionLike
198-
)}
199-
${this._renderActionButton(
200-
this.message?.reactions?.includes(DISLIKE_ACTIVE)
201-
? DISLIKE_ACTIVE
202-
: DISLIKE_INACTIVE,
203-
resourceStrings.reactionDislike
204-
)}
205-
${this._renderActionButton(
206-
REGENERATE,
207-
resourceStrings.reactionRegenerate
208-
)}
209-
</div>
185+
${this._renderActionButton(COPY_CONTENT, resourceStrings.reactionCopy)}
186+
${this._renderActionButton(
187+
this.message.reactions?.includes(LIKE_ACTIVE)
188+
? LIKE_ACTIVE
189+
: LIKE_INACTIVE,
190+
resourceStrings.reactionLike
191+
)}
192+
${this._renderActionButton(
193+
this.message.reactions?.includes(DISLIKE_ACTIVE)
194+
? DISLIKE_ACTIVE
195+
: DISLIKE_INACTIVE,
196+
resourceStrings.reactionDislike
197+
)}
198+
${this._renderActionButton(
199+
REGENERATE,
200+
resourceStrings.reactionRegenerate
201+
)}
210202
`;
211203
}
212204

@@ -224,9 +216,8 @@ export default class IgcChatMessageComponent extends LitElement {
224216
`;
225217
}
226218

227-
// Default rendering logic for attachments
228219
private _renderAttachments() {
229-
return isEmpty(this.message?.attachments ?? [])
220+
return isEmpty(this.message.attachments ?? [])
230221
? nothing
231222
: html`
232223
<igc-message-attachments
@@ -236,14 +227,31 @@ export default class IgcChatMessageComponent extends LitElement {
236227
}
237228

238229
private _renderMessage() {
239-
return this.message
240-
? html`${this._renderHeader()}${this._renderContent()}${this._renderAttachments()}${this._renderActions()}`
241-
: nothing;
230+
const ctx: ChatMessageRenderContext = {
231+
message: this.message,
232+
instance: this._state.host,
233+
};
234+
235+
return html`
236+
<div part="message-header">
237+
${until(this._getRenderer('messageHeader')(ctx))}
238+
</div>
239+
<div part="plain-text">
240+
${until(this._getRenderer('messageContent')(ctx))}
241+
</div>
242+
<div part="message-attachments">
243+
${until(this._getRenderer('messageAttachments')(ctx))}
244+
</div>
245+
<div part="message-actions" @click=${this._handleMessageActionClick}>
246+
${until(this._getRenderer('messageActions')(ctx))}
247+
</div>
248+
`;
242249
}
243250

244251
protected override render() {
252+
const messageRenderer = this._state.options?.renderers?.message;
245253
const ctx: ChatMessageRenderContext = {
246-
message: this.message!,
254+
message: this.message,
247255
instance: this._state.host,
248256
};
249257

@@ -258,14 +266,7 @@ export default class IgcChatMessageComponent extends LitElement {
258266
? html`
259267
<div part=${partMap(parts)}>
260268
${cache(
261-
this._state.options?.renderers?.message
262-
? html`${until(this._getRenderer('message')(ctx))}`
263-
: html`
264-
${until(this._getRenderer('messageHeader')(ctx))}
265-
${until(this._getRenderer('messageContent')(ctx))}
266-
${until(this._getRenderer('messageAttachments')(ctx))}
267-
${until(this._getRenderer('messageActions')(ctx))}
268-
`
269+
messageRenderer ? messageRenderer(ctx) : this._renderMessage()
269270
)}
270271
</div>
271272
`

0 commit comments

Comments
 (0)