Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
241 changes: 26 additions & 215 deletions apps/meteor/app/ui-message/client/messageBox/createComposerAPI.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,8 @@
import type { IMessage } from '@rocket.chat/core-typings';
import { Emitter } from '@rocket.chat/emitter';
import type { RefObject } from 'react';

import { createComposerAPICore, triggerEvent, type SetText } from './createComposerAPICore';
import { limitQuoteChain } from './limitQuoteChain';
import type { FormattingButton } from './messageBoxFormatting';
import { formattingButtons } from './messageBoxFormatting';
import type { ComposerAPI } from '../../../../client/lib/chats/ChatAPI';
import { createUploadsAPI } from '../../../../client/lib/chats/uploads';
import { settings } from '../../../../client/lib/settings';
import { withDebouncing } from '../../../../lib/utils/highOrderFunctions';

export const createComposerAPI = (
input: HTMLTextAreaElement,
Expand All @@ -18,47 +12,11 @@ export const createComposerAPI = (
composerRef: RefObject<HTMLElement | null>,
{ rid, tmid }: { rid: string; tmid?: string },
): ComposerAPI => {
const triggerEvent = (input: HTMLTextAreaElement, evt: string): void => {
const event = new Event(evt, { bubbles: true });
// TODO: Remove this hack for react to trigger onChange
const tracker = (input as any)._valueTracker;
if (tracker) {
tracker.setValue(new Date().toString());
}
input.dispatchEvent(event);
};

const emitter = new Emitter<{
quotedMessagesUpdate: void;
editing: void;
recording: void;
recordingVideo: void;
formatting: void;
mircophoneDenied: void;
}>();

let _quotedMessages: IMessage[] = [];

const persist = withDebouncing({ wait: 300 })(() => {
persistDraft(input.value);
});

const notifyQuotedMessagesUpdate = (): void => {
emitter.emit('quotedMessagesUpdate');
const focus = (): void => {
input.focus();
};

const setText = (
text: string,
{
selection,
skipFocus,
}: {
selection?:
| { readonly start?: number; readonly end?: number }
| ((previous: { readonly start: number; readonly end: number }) => { readonly start?: number; readonly end?: number });
skipFocus?: boolean;
} = {},
): void => {
const setText: SetText = (text, { selection, skipFocus } = {}) => {
!skipFocus && focus();

const { selectionStart, selectionEnd } = input;
Expand Down Expand Up @@ -86,141 +44,18 @@ export const createComposerAPI = (
!skipFocus && focus();
};

const insertText = (text: string): void => {
setText(text, {
selection: ({ start, end }) => ({
start: start + text.length,
end: end + text.length,
}),
});
};

const clear = (): void => {
setText('');
};

const focus = (): void => {
input.focus();
};

const replyWith = async (text: string): Promise<void> => {
if (input) {
input.value = text;
input.focus();
}
};

const quoteMessage = async (message: IMessage): Promise<void> => {
_quotedMessages = [..._quotedMessages.filter((_message) => _message._id !== message._id), limitQuoteChain(message, quoteChainLimit)];
notifyQuotedMessagesUpdate();
input.focus();
};

const dismissQuotedMessage = async (mid: IMessage['_id']): Promise<void> => {
_quotedMessages = _quotedMessages.filter((message) => message._id !== mid);
notifyQuotedMessagesUpdate();
};

const dismissAllQuotedMessages = async (): Promise<void> => {
_quotedMessages = [];
notifyQuotedMessagesUpdate();
};

const quotedMessages = {
get: () => _quotedMessages,
subscribe: (callback: () => void) => emitter.on('quotedMessagesUpdate', callback),
};

const [editing, setEditing] = (() => {
let editing = false;

return [
{
get: () => editing,
subscribe: (callback: () => void) => emitter.on('editing', callback),
},
(value: boolean) => {
editing = value;
emitter.emit('editing');
},
];
})();

const [recording, setRecordingMode] = (() => {
let recording = false;

return [
{
get: () => recording,
subscribe: (callback: () => void) => emitter.on('recording', callback),
},
(value: boolean) => {
recording = value;
emitter.emit('recording');
},
];
})();

const [recordingVideo, setRecordingVideo] = (() => {
let recordingVideo = false;

return [
{
get: () => recordingVideo,
subscribe: (callback: () => void) => emitter.on('recordingVideo', callback),
},
(value: boolean) => {
recordingVideo = value;
emitter.emit('recordingVideo');
},
];
})();

const [isMicrophoneDenied, setIsMicrophoneDenied] = (() => {
let isMicrophoneDenied = false;

return [
{
get: () => isMicrophoneDenied,
subscribe: (callback: () => void) => emitter.on('mircophoneDenied', callback),
},
(value: boolean) => {
isMicrophoneDenied = value;
emitter.emit('mircophoneDenied');
},
];
})();

const setEditingMode = (editing: boolean): void => {
setEditing(editing);
};

const [formatters, stopFormatterSubscription] = (() => {
let actions: FormattingButton[] = [];

const recompute = (): void => {
actions = formattingButtons.filter(({ condition }) => !condition || condition());
emitter.emit('formatting');
};
recompute();
// Coarse-grained: fires on every setting change, but the only condition()
// today is Katex_Enabled and the recompute is a cheap zustand read, so the
// extra work per unrelated setting change is negligible.
const stop = settings.observe('*', recompute);

return [
{
get: () => actions,
subscribe: (callback: () => void) => emitter.on('formatting', callback),
},
stop,
];
})();
const core = createComposerAPICore({
Comment thread
MartinSchoeler marked this conversation as resolved.
input,
composerRef,
room: { rid, tmid },
initialValue: initialDraft,
save: () => persistDraft(input.value),
setText,
focus,
prepareQuotedMessage: (message) => limitQuoteChain(message, quoteChainLimit),
});

const release = (): void => {
input.removeEventListener('input', persist);
stopFormatterSubscription();
};
const { insertText } = core;

const wrapSelection = (pattern: string): { selectionStart: number; selectionEnd: number; value: string } => {
const { selectionEnd = input.value.length, selectionStart = 0 } = input;
Expand Down Expand Up @@ -278,14 +113,6 @@ export const createComposerAPI = (
};
};

const insertNewLine = (): void => insertText('\n');

setText(initialDraft, {
skipFocus: true,
});

input.addEventListener('input', persist);

// Gets the text that is connected to the cursor and replaces it with the given text
const replaceText = (text: string, selection: { readonly start: number; readonly end: number }): void => {
// Selects the text that is connected to the cursor
Expand All @@ -306,16 +133,22 @@ export const createComposerAPI = (
focus();
};

const replyWith = async (text: string): Promise<void> => {
if (input) {
input.value = text;
input.focus();
}
};

return {
composerRef,
...core,
setText,
wrapSelection,
replaceText,
insertNewLine,
blur: () => input.blur(),

replyWith,
substring: (start: number, end?: number) => {
return input.value.substring(start, end);
},

getCursorPosition: () => {
return input.selectionStart;
},
Expand All @@ -329,8 +162,6 @@ export const createComposerAPI = (
input.selectionEnd = input.selectionStart;
focus();
},
release,
wrapSelection,
get text(): string {
return input.value;
},
Expand All @@ -340,25 +171,5 @@ export const createComposerAPI = (
end: input.selectionEnd,
};
},

editing,
setEditingMode,
recording,
setRecordingMode,
recordingVideo,
setRecordingVideo,
insertText,
setText,
clear,
focus,
replyWith,
quoteMessage,
dismissQuotedMessage,
dismissAllQuotedMessages,
quotedMessages,
formatters,
isMicrophoneDenied,
setIsMicrophoneDenied,
uploads: createUploadsAPI({ rid, tmid }),
};
};
Loading
Loading