Skip to content

Commit f47f973

Browse files
chore: split composer API and reintroduce RichTextComposerAPI (#41286)
1 parent b0b3ebe commit f47f973

3 files changed

Lines changed: 314 additions & 434 deletions

File tree

Lines changed: 26 additions & 215 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,8 @@
1-
import type { IMessage } from '@rocket.chat/core-typings';
2-
import { Emitter } from '@rocket.chat/emitter';
31
import type { RefObject } from 'react';
42

3+
import { createComposerAPICore, triggerEvent, type SetText } from './createComposerAPICore';
54
import { limitQuoteChain } from './limitQuoteChain';
6-
import type { FormattingButton } from './messageBoxFormatting';
7-
import { formattingButtons } from './messageBoxFormatting';
85
import type { ComposerAPI } from '../../../../client/lib/chats/ChatAPI';
9-
import { createUploadsAPI } from '../../../../client/lib/chats/uploads';
10-
import { settings } from '../../../../client/lib/settings';
11-
import { withDebouncing } from '../../../../lib/utils/highOrderFunctions';
126

137
export const createComposerAPI = (
148
input: HTMLTextAreaElement,
@@ -18,47 +12,11 @@ export const createComposerAPI = (
1812
composerRef: RefObject<HTMLElement | null>,
1913
{ rid, tmid }: { rid: string; tmid?: string },
2014
): ComposerAPI => {
21-
const triggerEvent = (input: HTMLTextAreaElement, evt: string): void => {
22-
const event = new Event(evt, { bubbles: true });
23-
// TODO: Remove this hack for react to trigger onChange
24-
const tracker = (input as any)._valueTracker;
25-
if (tracker) {
26-
tracker.setValue(new Date().toString());
27-
}
28-
input.dispatchEvent(event);
29-
};
30-
31-
const emitter = new Emitter<{
32-
quotedMessagesUpdate: void;
33-
editing: void;
34-
recording: void;
35-
recordingVideo: void;
36-
formatting: void;
37-
mircophoneDenied: void;
38-
}>();
39-
40-
let _quotedMessages: IMessage[] = [];
41-
42-
const persist = withDebouncing({ wait: 300 })(() => {
43-
persistDraft(input.value);
44-
});
45-
46-
const notifyQuotedMessagesUpdate = (): void => {
47-
emitter.emit('quotedMessagesUpdate');
15+
const focus = (): void => {
16+
input.focus();
4817
};
4918

50-
const setText = (
51-
text: string,
52-
{
53-
selection,
54-
skipFocus,
55-
}: {
56-
selection?:
57-
| { readonly start?: number; readonly end?: number }
58-
| ((previous: { readonly start: number; readonly end: number }) => { readonly start?: number; readonly end?: number });
59-
skipFocus?: boolean;
60-
} = {},
61-
): void => {
19+
const setText: SetText = (text, { selection, skipFocus } = {}) => {
6220
!skipFocus && focus();
6321

6422
const { selectionStart, selectionEnd } = input;
@@ -86,141 +44,18 @@ export const createComposerAPI = (
8644
!skipFocus && focus();
8745
};
8846

89-
const insertText = (text: string): void => {
90-
setText(text, {
91-
selection: ({ start, end }) => ({
92-
start: start + text.length,
93-
end: end + text.length,
94-
}),
95-
});
96-
};
97-
98-
const clear = (): void => {
99-
setText('');
100-
};
101-
102-
const focus = (): void => {
103-
input.focus();
104-
};
105-
106-
const replyWith = async (text: string): Promise<void> => {
107-
if (input) {
108-
input.value = text;
109-
input.focus();
110-
}
111-
};
112-
113-
const quoteMessage = async (message: IMessage): Promise<void> => {
114-
_quotedMessages = [..._quotedMessages.filter((_message) => _message._id !== message._id), limitQuoteChain(message, quoteChainLimit)];
115-
notifyQuotedMessagesUpdate();
116-
input.focus();
117-
};
118-
119-
const dismissQuotedMessage = async (mid: IMessage['_id']): Promise<void> => {
120-
_quotedMessages = _quotedMessages.filter((message) => message._id !== mid);
121-
notifyQuotedMessagesUpdate();
122-
};
123-
124-
const dismissAllQuotedMessages = async (): Promise<void> => {
125-
_quotedMessages = [];
126-
notifyQuotedMessagesUpdate();
127-
};
128-
129-
const quotedMessages = {
130-
get: () => _quotedMessages,
131-
subscribe: (callback: () => void) => emitter.on('quotedMessagesUpdate', callback),
132-
};
133-
134-
const [editing, setEditing] = (() => {
135-
let editing = false;
136-
137-
return [
138-
{
139-
get: () => editing,
140-
subscribe: (callback: () => void) => emitter.on('editing', callback),
141-
},
142-
(value: boolean) => {
143-
editing = value;
144-
emitter.emit('editing');
145-
},
146-
];
147-
})();
148-
149-
const [recording, setRecordingMode] = (() => {
150-
let recording = false;
151-
152-
return [
153-
{
154-
get: () => recording,
155-
subscribe: (callback: () => void) => emitter.on('recording', callback),
156-
},
157-
(value: boolean) => {
158-
recording = value;
159-
emitter.emit('recording');
160-
},
161-
];
162-
})();
163-
164-
const [recordingVideo, setRecordingVideo] = (() => {
165-
let recordingVideo = false;
166-
167-
return [
168-
{
169-
get: () => recordingVideo,
170-
subscribe: (callback: () => void) => emitter.on('recordingVideo', callback),
171-
},
172-
(value: boolean) => {
173-
recordingVideo = value;
174-
emitter.emit('recordingVideo');
175-
},
176-
];
177-
})();
178-
179-
const [isMicrophoneDenied, setIsMicrophoneDenied] = (() => {
180-
let isMicrophoneDenied = false;
181-
182-
return [
183-
{
184-
get: () => isMicrophoneDenied,
185-
subscribe: (callback: () => void) => emitter.on('mircophoneDenied', callback),
186-
},
187-
(value: boolean) => {
188-
isMicrophoneDenied = value;
189-
emitter.emit('mircophoneDenied');
190-
},
191-
];
192-
})();
193-
194-
const setEditingMode = (editing: boolean): void => {
195-
setEditing(editing);
196-
};
197-
198-
const [formatters, stopFormatterSubscription] = (() => {
199-
let actions: FormattingButton[] = [];
200-
201-
const recompute = (): void => {
202-
actions = formattingButtons.filter(({ condition }) => !condition || condition());
203-
emitter.emit('formatting');
204-
};
205-
recompute();
206-
// Coarse-grained: fires on every setting change, but the only condition()
207-
// today is Katex_Enabled and the recompute is a cheap zustand read, so the
208-
// extra work per unrelated setting change is negligible.
209-
const stop = settings.observe('*', recompute);
210-
211-
return [
212-
{
213-
get: () => actions,
214-
subscribe: (callback: () => void) => emitter.on('formatting', callback),
215-
},
216-
stop,
217-
];
218-
})();
47+
const core = createComposerAPICore({
48+
input,
49+
composerRef,
50+
room: { rid, tmid },
51+
initialValue: initialDraft,
52+
save: () => persistDraft(input.value),
53+
setText,
54+
focus,
55+
prepareQuotedMessage: (message) => limitQuoteChain(message, quoteChainLimit),
56+
});
21957

220-
const release = (): void => {
221-
input.removeEventListener('input', persist);
222-
stopFormatterSubscription();
223-
};
58+
const { insertText } = core;
22459

22560
const wrapSelection = (pattern: string): { selectionStart: number; selectionEnd: number; value: string } => {
22661
const { selectionEnd = input.value.length, selectionStart = 0 } = input;
@@ -278,14 +113,6 @@ export const createComposerAPI = (
278113
};
279114
};
280115

281-
const insertNewLine = (): void => insertText('\n');
282-
283-
setText(initialDraft, {
284-
skipFocus: true,
285-
});
286-
287-
input.addEventListener('input', persist);
288-
289116
// Gets the text that is connected to the cursor and replaces it with the given text
290117
const replaceText = (text: string, selection: { readonly start: number; readonly end: number }): void => {
291118
// Selects the text that is connected to the cursor
@@ -306,16 +133,22 @@ export const createComposerAPI = (
306133
focus();
307134
};
308135

136+
const replyWith = async (text: string): Promise<void> => {
137+
if (input) {
138+
input.value = text;
139+
input.focus();
140+
}
141+
};
142+
309143
return {
310-
composerRef,
144+
...core,
145+
setText,
146+
wrapSelection,
311147
replaceText,
312-
insertNewLine,
313-
blur: () => input.blur(),
314-
148+
replyWith,
315149
substring: (start: number, end?: number) => {
316150
return input.value.substring(start, end);
317151
},
318-
319152
getCursorPosition: () => {
320153
return input.selectionStart;
321154
},
@@ -329,8 +162,6 @@ export const createComposerAPI = (
329162
input.selectionEnd = input.selectionStart;
330163
focus();
331164
},
332-
release,
333-
wrapSelection,
334165
get text(): string {
335166
return input.value;
336167
},
@@ -340,25 +171,5 @@ export const createComposerAPI = (
340171
end: input.selectionEnd,
341172
};
342173
},
343-
344-
editing,
345-
setEditingMode,
346-
recording,
347-
setRecordingMode,
348-
recordingVideo,
349-
setRecordingVideo,
350-
insertText,
351-
setText,
352-
clear,
353-
focus,
354-
replyWith,
355-
quoteMessage,
356-
dismissQuotedMessage,
357-
dismissAllQuotedMessages,
358-
quotedMessages,
359-
formatters,
360-
isMicrophoneDenied,
361-
setIsMicrophoneDenied,
362-
uploads: createUploadsAPI({ rid, tmid }),
363174
};
364175
};

0 commit comments

Comments
 (0)