Skip to content

Commit cb37c34

Browse files
committed
fix: update the composer text with default value if config changes
1 parent d4a095d commit cb37c34

File tree

5 files changed

+131
-31
lines changed

5 files changed

+131
-31
lines changed

src/messageComposer/messageComposer.ts

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,7 @@ import type {
2525
} from '../types';
2626
import type { StreamChat } from '../client';
2727
import type { MessageComposerConfig } from './configuration/types';
28-
29-
// todo: move to a more global place to be reused
30-
type DeepPartial<T> = {
31-
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
32-
};
28+
import type { DeepPartial } from '../types.utility';
3329

3430
export type LastComposerChange = { draftUpdate: number | null; stateUpdate: number };
3531

@@ -371,7 +367,7 @@ export class MessageComposer {
371367
this.unsubscribeFunctions.add(this.subscribePollComposerStateChanged());
372368
this.unsubscribeFunctions.add(this.subscribeCustomDataManagerStateChanged());
373369
this.unsubscribeFunctions.add(this.subscribeMessageComposerStateChanged());
374-
370+
this.unsubscribeFunctions.add(this.subscribeMessageComposerConfigStateChanged());
375371
if (this.config.drafts.enabled) {
376372
this.unsubscribeFunctions.add(this.subscribeDraftUpdated());
377373
this.unsubscribeFunctions.add(this.subscribeDraftDeleted());
@@ -573,6 +569,17 @@ export class MessageComposer {
573569
}
574570
});
575571

572+
private subscribeMessageComposerConfigStateChanged = () =>
573+
this.configState.subscribe((nextValue) => {
574+
const { text } = nextValue;
575+
if (this.textComposer.text === '' && text.defaultValue) {
576+
this.textComposer.insertText({
577+
text: text.defaultValue,
578+
selection: { start: 0, end: 0 },
579+
});
580+
}
581+
});
582+
576583
setQuotedMessage = (quotedMessage: LocalMessage | null) => {
577584
this.state.partialNext({ quotedMessage });
578585
};

src/messageComposer/textComposer.ts

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -148,16 +148,28 @@ export class TextComposer {
148148
};
149149
const { maxLengthOnEdit } = this.composer.config.text ?? {};
150150
const currentText = this.text;
151-
const newText = [
151+
const textBeforeTrim = [
152152
currentText.slice(0, finalSelection.start),
153153
text,
154154
currentText.slice(finalSelection.end),
155155
].join('');
156+
const finalText = textBeforeTrim.slice(
157+
0,
158+
typeof maxLengthOnEdit === 'number' ? maxLengthOnEdit : textBeforeTrim.length,
159+
);
160+
const expectedCursorPosition =
161+
currentText.slice(0, finalSelection.start).length + text.length;
162+
const cursorPosition =
163+
expectedCursorPosition >= finalText.length
164+
? finalText.length
165+
: currentText.slice(0, expectedCursorPosition).length;
166+
156167
this.state.partialNext({
157-
text: newText.slice(
158-
0,
159-
typeof maxLengthOnEdit === 'number' ? maxLengthOnEdit : newText.length,
160-
),
168+
text: finalText,
169+
selection: {
170+
start: cursorPosition,
171+
end: cursorPosition,
172+
},
161173
});
162174
};
163175

src/types.utility.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export type DeepPartial<T> = {
2+
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
3+
};

test/unit/MessageComposer/messageComposer.test.ts

Lines changed: 81 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
StreamChat,
77
Thread,
88
} from '../../../src';
9+
import { DeepPartial } from '../../../src/types.utility';
910
import { MessageComposer } from '../../../src/messageComposer/messageComposer';
1011
import { StateStore } from '../../../src/store';
1112
import { DraftResponse, MessageResponse } from '../../../src/types';
@@ -20,14 +21,32 @@ vi.mock('../../../src/utils', () => ({
2021
isArray: vi.fn(),
2122
isDate: vi.fn(),
2223
isNumber: vi.fn(),
23-
24+
debounce: vi.fn().mockImplementation((fn) => fn),
2425
generateUUIDv4: vi.fn().mockReturnValue('test-uuid'),
2526
isLocalMessage: vi.fn().mockReturnValue(true),
2627
formatMessage: vi.fn().mockImplementation((msg) => msg),
2728
randomId: vi.fn().mockReturnValue('test-uuid'),
2829
throttle: vi.fn().mockImplementation((fn) => fn),
2930
}));
3031

32+
// // Mock dependencies
33+
// vi.mock('../../../src/utils', () => ({
34+
// axiosParamsSerializer: vi.fn(),
35+
// isFunction: vi.fn(),
36+
// isString: vi.fn(),
37+
// isObject: vi.fn(),
38+
// isArray: vi.fn(),
39+
// isDate: vi.fn(),
40+
// isNumber: vi.fn(),
41+
// logChatPromiseExecution: vi.fn(),
42+
// generateUUIDv4: vi.fn().mockReturnValue('test-uuid'),
43+
// debounce: vi.fn().mockImplementation((fn) => fn),
44+
// randomId: vi.fn().mockReturnValue('test-uuid'),
45+
// isLocalMessage: vi.fn().mockReturnValue(true),
46+
// formatMessage: vi.fn().mockImplementation((msg) => msg),
47+
// throttle: vi.fn().mockImplementation((fn) => fn),
48+
// }));
49+
3150
vi.mock('../../../src/messageComposer/attachmentManager', () => ({
3251
AttachmentManager: vi.fn().mockImplementation(() => ({
3352
state: new StateStore({ attachments: [] }),
@@ -46,14 +65,14 @@ vi.mock('../../../src/messageComposer/linkPreviewsManager', () => ({
4665
})),
4766
}));
4867

49-
vi.mock('../../../src/messageComposer/textComposer', () => ({
50-
TextComposer: vi.fn().mockImplementation(() => ({
51-
state: new StateStore({ text: '', mentionedUsers: [] }),
52-
initState: vi.fn(),
53-
clear: vi.fn(),
54-
textIsEmpty: vi.fn().mockReturnValue(true),
55-
})),
56-
}));
68+
// vi.mock('../../../src/messageComposer/textComposer', () => ({
69+
// TextComposer: vi.fn().mockImplementation(() => ({
70+
// state: new StateStore({ text: '', mentionedUsers: [] }),
71+
// initState: vi.fn(),
72+
// clear: vi.fn(),
73+
// textIsEmpty: vi.fn().mockReturnValue(true),
74+
// })),
75+
// }));
5776

5877
vi.mock('../../../src/messageComposer/pollComposer', () => ({
5978
PollComposer: vi.fn().mockImplementation(() => ({
@@ -74,7 +93,7 @@ vi.mock('../../../src/messageComposer/CustomDataManager', () => ({
7493
})),
7594
}));
7695

77-
vi.mock('../../../src/messageComposer/middleware', () => ({
96+
vi.mock('../../../src/messageComposer/middleware/messageComposer', () => ({
7897
MessageComposerMiddlewareExecutor: vi.fn().mockImplementation(() => ({
7998
execute: vi.fn().mockResolvedValue({ state: {} }),
8099
})),
@@ -134,7 +153,7 @@ const setup = ({
134153
}: {
135154
composition?: LocalMessage | DraftResponse | MessageResponse | undefined;
136155
compositionContext?: Channel | Thread | LocalMessage | undefined;
137-
config?: Partial<MessageComposerConfig>;
156+
config?: DeepPartial<MessageComposerConfig>;
138157
} = {}) => {
139158
const mockClient = new StreamChat('test-api-key');
140159
mockClient.user = user;
@@ -379,21 +398,26 @@ describe('MessageComposer', () => {
379398

380399
it('should return the correct compositionIsEmpty', () => {
381400
const { messageComposer } = setup();
382-
const textComposerMock = messageComposer.textComposer as any;
383-
401+
const spyTextComposerTextIsEmpty = vi
402+
.spyOn(messageComposer.textComposer, 'textIsEmpty', 'get')
403+
.mockReturnValueOnce(true)
404+
.mockReturnValueOnce(false);
384405
// First case - empty composition
385-
Object.defineProperty(textComposerMock, 'textIsEmpty', {
386-
get: () => true,
406+
messageComposer.textComposer.state.partialNext({
407+
text: '',
408+
mentionedUsers: [],
409+
selection: { start: 0, end: 0 },
387410
});
388-
textComposerMock.state.next({ text: '' });
389411
expect(messageComposer.compositionIsEmpty).toBe(true);
390412

391413
// Second case - non-empty composition
392-
Object.defineProperty(textComposerMock, 'textIsEmpty', {
393-
get: () => false,
414+
messageComposer.textComposer.state.partialNext({
415+
text: 'Hello world',
416+
mentionedUsers: [],
417+
selection: { start: 0, end: 0 },
394418
});
395-
textComposerMock.state.next({ text: 'Hello world' });
396419
expect(messageComposer.compositionIsEmpty).toBe(false);
420+
spyTextComposerTextIsEmpty.mockRestore();
397421
});
398422
});
399423

@@ -982,7 +1006,7 @@ describe('MessageComposer', () => {
9821006
description: '',
9831007
enforce_unique_vote: false,
9841008
is_closed: false,
985-
max_votes_allowed: 1,
1009+
max_votes_allowed: '1',
9861010
user_id: 'user-id',
9871011
voting_visibility: 'public',
9881012
},
@@ -1024,5 +1048,42 @@ describe('MessageComposer', () => {
10241048
expect(spy).toHaveBeenCalled();
10251049
});
10261050
});
1051+
1052+
describe('subscribeMessageComposerConfigStateChanged', () => {
1053+
const defaultValue = 'Default text';
1054+
1055+
it('should insert default text when text is empty and config has a default value', () => {
1056+
const { messageComposer } = setup();
1057+
const spy = vi.spyOn(messageComposer.textComposer, 'insertText');
1058+
messageComposer.registerSubscriptions();
1059+
expect(spy).not.toHaveBeenCalled();
1060+
1061+
messageComposer.textComposer.defaultValue = defaultValue;
1062+
1063+
expect(spy).toHaveBeenCalledWith({
1064+
text: defaultValue,
1065+
selection: { start: 0, end: 0 },
1066+
});
1067+
spy.mockRestore();
1068+
});
1069+
1070+
it('should not insert default text when text is not empty', () => {
1071+
const { messageComposer } = setup();
1072+
messageComposer.registerSubscriptions();
1073+
const spy = vi.spyOn(messageComposer.textComposer, 'insertText');
1074+
1075+
messageComposer.textComposer.state.next({
1076+
text: 'Hello world',
1077+
mentionedUsers: [],
1078+
selection: { start: 0, end: 0 },
1079+
});
1080+
expect(spy).not.toHaveBeenCalled();
1081+
1082+
messageComposer.textComposer.defaultValue = defaultValue;
1083+
1084+
expect(spy).not.toHaveBeenCalled();
1085+
spy.mockRestore();
1086+
});
1087+
});
10271088
});
10281089
});

test/unit/MessageComposer/textComposer.test.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -443,6 +443,23 @@ describe('TextComposer', () => {
443443
textComposer.insertText({ text: 'Hi', selection: { start: 0, end: 5 } });
444444
expect(textComposer.text).toBe('Hi world');
445445
});
446+
447+
it('should handle insertion with multi-character selection and maxLengthOnEdit restricting the size', () => {
448+
const message: LocalMessage = {
449+
id: 'test-message',
450+
type: 'regular',
451+
text: 'Hello world',
452+
};
453+
const {
454+
messageComposer: { textComposer },
455+
} = setup({
456+
config: { maxLengthOnEdit: 10 },
457+
composition: message,
458+
});
459+
const insertedText = 'Hi world';
460+
textComposer.insertText({ text: insertedText, selection: { start: 7, end: 9 } });
461+
expect(textComposer.text).toBe('Hello wHi ');
462+
});
446463
});
447464

448465
describe('closeSuggestions', () => {

0 commit comments

Comments
 (0)