Skip to content
This repository was archived by the owner on Sep 11, 2024. It is now read-only.

Commit bca9caa

Browse files
authored
Settings toggle to disable Composer Markdown (#8358)
1 parent f4d935d commit bca9caa

File tree

12 files changed

+260
-98
lines changed

12 files changed

+260
-98
lines changed

res/css/views/elements/_SettingsFlag.scss

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,4 +41,10 @@ limitations under the License.
4141
font-size: $font-12px;
4242
line-height: $font-15px;
4343
color: $secondary-content;
44+
45+
// Support code/pre elements in settings flag descriptions
46+
pre, code {
47+
font-family: $monospace-font-family !important;
48+
background-color: $rte-code-bg-color;
49+
}
4450
}

src/components/views/rooms/BasicMessageComposer.tsx

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ interface IProps {
103103
}
104104

105105
interface IState {
106+
useMarkdown: boolean;
106107
showPillAvatar: boolean;
107108
query?: string;
108109
showVisualBell?: boolean;
@@ -124,6 +125,7 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
124125
private lastCaret: DocumentOffset;
125126
private lastSelection: ReturnType<typeof cloneSelection>;
126127

128+
private readonly useMarkdownHandle: string;
127129
private readonly emoticonSettingHandle: string;
128130
private readonly shouldShowPillAvatarSettingHandle: string;
129131
private readonly surroundWithHandle: string;
@@ -133,10 +135,13 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
133135
super(props);
134136
this.state = {
135137
showPillAvatar: SettingsStore.getValue("Pill.shouldShowPillAvatar"),
138+
useMarkdown: SettingsStore.getValue("MessageComposerInput.useMarkdown"),
136139
surroundWith: SettingsStore.getValue("MessageComposerInput.surroundWith"),
137140
showVisualBell: false,
138141
};
139142

143+
this.useMarkdownHandle = SettingsStore.watchSetting('MessageComposerInput.useMarkdown', null,
144+
this.configureUseMarkdown);
140145
this.emoticonSettingHandle = SettingsStore.watchSetting('MessageComposerInput.autoReplaceEmoji', null,
141146
this.configureEmoticonAutoReplace);
142147
this.configureEmoticonAutoReplace();
@@ -442,7 +447,7 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
442447
}
443448
} else if (!selection.isCollapsed && !isEmpty) {
444449
this.hasTextSelected = true;
445-
if (this.formatBarRef.current) {
450+
if (this.formatBarRef.current && this.state.useMarkdown) {
446451
const selectionRect = selection.getRangeAt(0).getBoundingClientRect();
447452
this.formatBarRef.current.showAt(selectionRect);
448453
}
@@ -630,6 +635,14 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
630635
this.setState({ completionIndex });
631636
};
632637

638+
private configureUseMarkdown = (): void => {
639+
const useMarkdown = SettingsStore.getValue("MessageComposerInput.useMarkdown");
640+
this.setState({ useMarkdown });
641+
if (!useMarkdown && this.formatBarRef.current) {
642+
this.formatBarRef.current.hide();
643+
}
644+
};
645+
633646
private configureEmoticonAutoReplace = (): void => {
634647
this.props.model.setTransformCallback(this.transform);
635648
};
@@ -654,6 +667,7 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
654667
this.editorRef.current.removeEventListener("input", this.onInput, true);
655668
this.editorRef.current.removeEventListener("compositionstart", this.onCompositionStart, true);
656669
this.editorRef.current.removeEventListener("compositionend", this.onCompositionEnd, true);
670+
SettingsStore.unwatchSetting(this.useMarkdownHandle);
657671
SettingsStore.unwatchSetting(this.emoticonSettingHandle);
658672
SettingsStore.unwatchSetting(this.shouldShowPillAvatarSettingHandle);
659673
SettingsStore.unwatchSetting(this.surroundWithHandle);
@@ -694,6 +708,10 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
694708
}
695709

696710
public onFormatAction = (action: Formatting): void => {
711+
if (!this.state.useMarkdown) {
712+
return;
713+
}
714+
697715
const range: Range = getRangeForSelection(this.editorRef.current, this.props.model, document.getSelection());
698716

699717
this.historyManager.ensureLastChangesPushed(this.props.model);

src/components/views/rooms/EditMessageComposer.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,10 @@ function createEditContent(
9595
body: `${plainPrefix} * ${body}`,
9696
};
9797

98-
const formattedBody = htmlSerializeIfNeeded(model, { forceHTML: isReply });
98+
const formattedBody = htmlSerializeIfNeeded(model, {
99+
forceHTML: isReply,
100+
useMarkdown: SettingsStore.getValue("MessageComposerInput.useMarkdown"),
101+
});
99102
if (formattedBody) {
100103
newContent.format = "org.matrix.custom.html";
101104
newContent.formatted_body = formattedBody;
@@ -404,7 +407,9 @@ class EditMessageComposer extends React.Component<IEditMessageComposerProps, ISt
404407
} else {
405408
// otherwise, either restore serialized parts from localStorage or parse the body of the event
406409
const restoredParts = this.restoreStoredEditorState(partCreator);
407-
parts = restoredParts || parseEvent(editState.getEvent(), partCreator);
410+
parts = restoredParts || parseEvent(editState.getEvent(), partCreator, {
411+
shouldEscape: SettingsStore.getValue("MessageComposerInput.useMarkdown"),
412+
});
408413
isRestored = !!restoredParts;
409414
}
410415
this.model = new EditorModel(parts, partCreator);

src/components/views/rooms/SendMessageComposer.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,10 @@ export function createMessageContent(
9191
msgtype: isEmote ? "m.emote" : "m.text",
9292
body: body,
9393
};
94-
const formattedBody = htmlSerializeIfNeeded(model, { forceHTML: !!replyToEvent });
94+
const formattedBody = htmlSerializeIfNeeded(model, {
95+
forceHTML: !!replyToEvent,
96+
useMarkdown: SettingsStore.getValue("MessageComposerInput.useMarkdown"),
97+
});
9598
if (formattedBody) {
9699
content.format = "org.matrix.custom.html";
97100
content.formatted_body = formattedBody;

src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ export default class PreferencesUserSettingsTab extends React.Component<IProps,
6363

6464
static COMPOSER_SETTINGS = [
6565
'MessageComposerInput.autoReplaceEmoji',
66+
'MessageComposerInput.useMarkdown',
6667
'MessageComposerInput.suggestEmoji',
6768
'sendTypingNotifications',
6869
'MessageComposerInput.ctrlEnterToSend',

src/editor/deserialize.ts

Lines changed: 42 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -52,12 +52,12 @@ function isListChild(n: Node): boolean {
5252
return LIST_TYPES.includes(n.parentNode?.nodeName);
5353
}
5454

55-
function parseAtRoomMentions(text: string, pc: PartCreator, shouldEscape = true): Part[] {
55+
function parseAtRoomMentions(text: string, pc: PartCreator, opts: IParseOptions): Part[] {
5656
const ATROOM = "@room";
5757
const parts: Part[] = [];
5858
text.split(ATROOM).forEach((textPart, i, arr) => {
5959
if (textPart.length) {
60-
parts.push(...pc.plainWithEmoji(shouldEscape ? escape(textPart) : textPart));
60+
parts.push(...pc.plainWithEmoji(opts.shouldEscape ? escape(textPart) : textPart));
6161
}
6262
// it's safe to never append @room after the last textPart
6363
// as split will report an empty string at the end if
@@ -70,7 +70,7 @@ function parseAtRoomMentions(text: string, pc: PartCreator, shouldEscape = true)
7070
return parts;
7171
}
7272

73-
function parseLink(n: Node, pc: PartCreator): Part[] {
73+
function parseLink(n: Node, pc: PartCreator, opts: IParseOptions): Part[] {
7474
const { href } = n as HTMLAnchorElement;
7575
const resourceId = getPrimaryPermalinkEntity(href); // The room/user ID
7676

@@ -81,18 +81,18 @@ function parseLink(n: Node, pc: PartCreator): Part[] {
8181

8282
const children = Array.from(n.childNodes);
8383
if (href === n.textContent && children.every(c => c.nodeType === Node.TEXT_NODE)) {
84-
return parseAtRoomMentions(n.textContent, pc);
84+
return parseAtRoomMentions(n.textContent, pc, opts);
8585
} else {
86-
return [pc.plain("["), ...parseChildren(n, pc), pc.plain(`](${href})`)];
86+
return [pc.plain("["), ...parseChildren(n, pc, opts), pc.plain(`](${href})`)];
8787
}
8888
}
8989

90-
function parseImage(n: Node, pc: PartCreator): Part[] {
90+
function parseImage(n: Node, pc: PartCreator, opts: IParseOptions): Part[] {
9191
const { alt, src } = n as HTMLImageElement;
9292
return pc.plainWithEmoji(`![${escape(alt)}](${src})`);
9393
}
9494

95-
function parseCodeBlock(n: Node, pc: PartCreator): Part[] {
95+
function parseCodeBlock(n: Node, pc: PartCreator, opts: IParseOptions): Part[] {
9696
let language = "";
9797
if (n.firstChild?.nodeName === "CODE") {
9898
for (const className of (n.firstChild as HTMLElement).classList) {
@@ -117,10 +117,10 @@ function parseCodeBlock(n: Node, pc: PartCreator): Part[] {
117117
return parts;
118118
}
119119

120-
function parseHeader(n: Node, pc: PartCreator): Part[] {
120+
function parseHeader(n: Node, pc: PartCreator, opts: IParseOptions): Part[] {
121121
const depth = parseInt(n.nodeName.slice(1), 10);
122122
const prefix = pc.plain("#".repeat(depth) + " ");
123-
return [prefix, ...parseChildren(n, pc)];
123+
return [prefix, ...parseChildren(n, pc, opts)];
124124
}
125125

126126
function checkIgnored(n) {
@@ -144,10 +144,10 @@ function prefixLines(parts: Part[], prefix: string, pc: PartCreator) {
144144
}
145145
}
146146

147-
function parseChildren(n: Node, pc: PartCreator, mkListItem?: (li: Node) => Part[]): Part[] {
147+
function parseChildren(n: Node, pc: PartCreator, opts: IParseOptions, mkListItem?: (li: Node) => Part[]): Part[] {
148148
let prev;
149149
return Array.from(n.childNodes).flatMap(c => {
150-
const parsed = parseNode(c, pc, mkListItem);
150+
const parsed = parseNode(c, pc, opts, mkListItem);
151151
if (parsed.length && prev && (checkBlockNode(prev) || checkBlockNode(c))) {
152152
if (isListChild(c)) {
153153
// Use tighter spacing within lists
@@ -161,12 +161,12 @@ function parseChildren(n: Node, pc: PartCreator, mkListItem?: (li: Node) => Part
161161
});
162162
}
163163

164-
function parseNode(n: Node, pc: PartCreator, mkListItem?: (li: Node) => Part[]): Part[] {
164+
function parseNode(n: Node, pc: PartCreator, opts: IParseOptions, mkListItem?: (li: Node) => Part[]): Part[] {
165165
if (checkIgnored(n)) return [];
166166

167167
switch (n.nodeType) {
168168
case Node.TEXT_NODE:
169-
return parseAtRoomMentions(n.nodeValue, pc);
169+
return parseAtRoomMentions(n.nodeValue, pc, opts);
170170
case Node.ELEMENT_NODE:
171171
switch (n.nodeName) {
172172
case "H1":
@@ -175,52 +175,52 @@ function parseNode(n: Node, pc: PartCreator, mkListItem?: (li: Node) => Part[]):
175175
case "H4":
176176
case "H5":
177177
case "H6":
178-
return parseHeader(n, pc);
178+
return parseHeader(n, pc, opts);
179179
case "A":
180-
return parseLink(n, pc);
180+
return parseLink(n, pc, opts);
181181
case "IMG":
182-
return parseImage(n, pc);
182+
return parseImage(n, pc, opts);
183183
case "BR":
184184
return [pc.newline()];
185185
case "HR":
186186
return [pc.plain("---")];
187187
case "EM":
188-
return [pc.plain("_"), ...parseChildren(n, pc), pc.plain("_")];
188+
return [pc.plain("_"), ...parseChildren(n, pc, opts), pc.plain("_")];
189189
case "STRONG":
190-
return [pc.plain("**"), ...parseChildren(n, pc), pc.plain("**")];
190+
return [pc.plain("**"), ...parseChildren(n, pc, opts), pc.plain("**")];
191191
case "DEL":
192-
return [pc.plain("<del>"), ...parseChildren(n, pc), pc.plain("</del>")];
192+
return [pc.plain("<del>"), ...parseChildren(n, pc, opts), pc.plain("</del>")];
193193
case "SUB":
194-
return [pc.plain("<sub>"), ...parseChildren(n, pc), pc.plain("</sub>")];
194+
return [pc.plain("<sub>"), ...parseChildren(n, pc, opts), pc.plain("</sub>")];
195195
case "SUP":
196-
return [pc.plain("<sup>"), ...parseChildren(n, pc), pc.plain("</sup>")];
196+
return [pc.plain("<sup>"), ...parseChildren(n, pc, opts), pc.plain("</sup>")];
197197
case "U":
198-
return [pc.plain("<u>"), ...parseChildren(n, pc), pc.plain("</u>")];
198+
return [pc.plain("<u>"), ...parseChildren(n, pc, opts), pc.plain("</u>")];
199199
case "PRE":
200-
return parseCodeBlock(n, pc);
200+
return parseCodeBlock(n, pc, opts);
201201
case "CODE": {
202202
// Escape backticks by using multiple backticks for the fence if necessary
203203
const fence = "`".repeat(longestBacktickSequence(n.textContent) + 1);
204204
return pc.plainWithEmoji(`${fence}${n.textContent}${fence}`);
205205
}
206206
case "BLOCKQUOTE": {
207-
const parts = parseChildren(n, pc);
207+
const parts = parseChildren(n, pc, opts);
208208
prefixLines(parts, "> ", pc);
209209
return parts;
210210
}
211211
case "LI":
212-
return mkListItem?.(n) ?? parseChildren(n, pc);
212+
return mkListItem?.(n) ?? parseChildren(n, pc, opts);
213213
case "UL": {
214-
const parts = parseChildren(n, pc, li => [pc.plain("- "), ...parseChildren(li, pc)]);
214+
const parts = parseChildren(n, pc, opts, li => [pc.plain("- "), ...parseChildren(li, pc, opts)]);
215215
if (isListChild(n)) {
216216
prefixLines(parts, " ", pc);
217217
}
218218
return parts;
219219
}
220220
case "OL": {
221221
let counter = (n as HTMLOListElement).start ?? 1;
222-
const parts = parseChildren(n, pc, li => {
223-
const parts = [pc.plain(`${counter}. `), ...parseChildren(li, pc)];
222+
const parts = parseChildren(n, pc, opts, li => {
223+
const parts = [pc.plain(`${counter}. `), ...parseChildren(li, pc, opts)];
224224
counter++;
225225
return parts;
226226
});
@@ -247,15 +247,20 @@ function parseNode(n: Node, pc: PartCreator, mkListItem?: (li: Node) => Part[]):
247247
}
248248
}
249249

250-
return parseChildren(n, pc);
250+
return parseChildren(n, pc, opts);
251251
}
252252

253-
function parseHtmlMessage(html: string, pc: PartCreator, isQuotedMessage: boolean): Part[] {
253+
interface IParseOptions {
254+
isQuotedMessage?: boolean;
255+
shouldEscape?: boolean;
256+
}
257+
258+
function parseHtmlMessage(html: string, pc: PartCreator, opts: IParseOptions): Part[] {
254259
// no nodes from parsing here should be inserted in the document,
255260
// as scripts in event handlers, etc would be executed then.
256261
// we're only taking text, so that is fine
257-
const parts = parseNode(new DOMParser().parseFromString(html, "text/html").body, pc);
258-
if (isQuotedMessage) {
262+
const parts = parseNode(new DOMParser().parseFromString(html, "text/html").body, pc, opts);
263+
if (opts.isQuotedMessage) {
259264
prefixLines(parts, "> ", pc);
260265
}
261266
return parts;
@@ -264,14 +269,14 @@ function parseHtmlMessage(html: string, pc: PartCreator, isQuotedMessage: boolea
264269
export function parsePlainTextMessage(
265270
body: string,
266271
pc: PartCreator,
267-
opts: { isQuotedMessage?: boolean, shouldEscape?: boolean },
272+
opts: IParseOptions,
268273
): Part[] {
269274
const lines = body.split(/\r\n|\r|\n/g); // split on any new-line combination not just \n, collapses \r\n
270275
return lines.reduce((parts, line, i) => {
271276
if (opts.isQuotedMessage) {
272277
parts.push(pc.plain("> "));
273278
}
274-
parts.push(...parseAtRoomMentions(line, pc, opts.shouldEscape));
279+
parts.push(...parseAtRoomMentions(line, pc, opts));
275280
const isLast = i === lines.length - 1;
276281
if (!isLast) {
277282
parts.push(pc.newline());
@@ -280,19 +285,19 @@ export function parsePlainTextMessage(
280285
}, [] as Part[]);
281286
}
282287

283-
export function parseEvent(event: MatrixEvent, pc: PartCreator, { isQuotedMessage = false } = {}) {
288+
export function parseEvent(event: MatrixEvent, pc: PartCreator, opts: IParseOptions = { shouldEscape: true }) {
284289
const content = event.getContent();
285290
let parts: Part[];
286291
const isEmote = content.msgtype === "m.emote";
287292
let isRainbow = false;
288293

289294
if (content.format === "org.matrix.custom.html") {
290-
parts = parseHtmlMessage(content.formatted_body || "", pc, isQuotedMessage);
295+
parts = parseHtmlMessage(content.formatted_body || "", pc, opts);
291296
if (content.body && content.formatted_body && textToHtmlRainbow(content.body) === content.formatted_body) {
292297
isRainbow = true;
293298
}
294299
} else {
295-
parts = parsePlainTextMessage(content.body || "", pc, { isQuotedMessage });
300+
parts = parsePlainTextMessage(content.body || "", pc, opts);
296301
}
297302

298303
if (isEmote && isRainbow) {

src/editor/serialize.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ limitations under the License.
1717

1818
import { AllHtmlEntities } from 'html-entities';
1919
import cheerio from 'cheerio';
20+
import escapeHtml from "escape-html";
2021

2122
import Markdown from '../Markdown';
2223
import { makeGenericPermalink } from "../utils/permalinks/Permalinks";
@@ -48,7 +49,19 @@ export function mdSerialize(model: EditorModel): string {
4849
}, "");
4950
}
5051

51-
export function htmlSerializeIfNeeded(model: EditorModel, { forceHTML = false } = {}): string {
52+
interface ISerializeOpts {
53+
forceHTML?: boolean;
54+
useMarkdown?: boolean;
55+
}
56+
57+
export function htmlSerializeIfNeeded(
58+
model: EditorModel,
59+
{ forceHTML = false, useMarkdown = true }: ISerializeOpts = {},
60+
): string {
61+
if (!useMarkdown) {
62+
return escapeHtml(textSerialize(model)).replace(/\n/g, '<br/>');
63+
}
64+
5265
let md = mdSerialize(model);
5366
// copy of raw input to remove unwanted math later
5467
const orig = md;

src/i18n/strings/en_EN.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -932,6 +932,8 @@
932932
"Use Ctrl + Enter to send a message": "Use Ctrl + Enter to send a message",
933933
"Surround selected text when typing special characters": "Surround selected text when typing special characters",
934934
"Automatically replace plain text Emoji": "Automatically replace plain text Emoji",
935+
"Enable Markdown": "Enable Markdown",
936+
"Start messages with <code>/plain</code> to send without markdown and <code>/md</code> to send with.": "Start messages with <code>/plain</code> to send without markdown and <code>/md</code> to send with.",
935937
"Mirror local video feed": "Mirror local video feed",
936938
"Match system theme": "Match system theme",
937939
"Use a system font": "Use a system font",

0 commit comments

Comments
 (0)