diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 3f017329926..a393db3324e 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -200,6 +200,8 @@ "noteToSelf": "Note to Self", "hideMenuBarTitle": "Hide Menu Bar", "hideMenuBarDescription": "Toggle system menu bar visibility", + "messageFormattingTitle": "Message Formatting (Experimental)", + "messageFormattingDescription": "Render messages using Markdown formatting", "startConversation": "Start New Conversation", "invalidNumberError": "Invalid Session ID or ONS Name", "failedResolveOns": "Failed to resolve ONS name", diff --git a/package.json b/package.json index 13c55ee32ef..b76e1e197f1 100644 --- a/package.json +++ b/package.json @@ -112,6 +112,15 @@ "linkify-it": "3.0.2", "lodash": "^4.17.20", "long": "^4.0.0", + "markdown-it": "^13.0.1", + "markdown-it-abbr": "^1.0.4", + "markdown-it-container": "^3.0.0", + "markdown-it-footnote": "^3.0.3", + "markdown-it-highlightjs": "^4.0.1", + "markdown-it-ins": "^3.0.1", + "markdown-it-mark": "^3.0.1", + "markdown-it-sub": "^1.0.0", + "markdown-it-sup": "^1.0.0", "mic-recorder-to-mp3": "^2.2.2", "minimist": "^1.2.6", "moment": "^2.29.4", diff --git a/stylesheets/_session.scss b/stylesheets/_session.scss index fe23d4724ab..462804b2597 100644 --- a/stylesheets/_session.scss +++ b/stylesheets/_session.scss @@ -1247,7 +1247,31 @@ input { } .module-message__text { + white-space: pre-wrap; + + p { + display: inline-block; + margin-top: 0.4em; + margin-bottom: 0.25em; + } + + ul, ol, blockquote { + margin-bottom: -0.5em; + white-space: normal; + + p { + display: block !important; + } + } + + pre { + margin-bottom: -0.5em; + } + + * { + user-select: text; + } } .speedButton { diff --git a/stylesheets/manifest.scss b/stylesheets/manifest.scss index d41bd1ab009..90b385fa219 100644 --- a/stylesheets/manifest.scss +++ b/stylesheets/manifest.scss @@ -6,6 +6,7 @@ @import '../node_modules/sanitize.css/sanitize.css'; @import '../node_modules/sanitize.css/forms.css'; @import '../node_modules/sanitize.css/typography.css'; +@import '../node_modules/highlight.js/styles/stackoverflow-dark.css'; // Global Settings, Variables, and Mixins @import 'session_constants'; diff --git a/ts/components/conversation/message/message-content/MessageBody.tsx b/ts/components/conversation/message/message-content/MessageBody.tsx index 4a76370e693..b8eda9ec8f7 100644 --- a/ts/components/conversation/message/message-content/MessageBody.tsx +++ b/ts/components/conversation/message/message-content/MessageBody.tsx @@ -1,7 +1,7 @@ import React, { useCallback } from 'react'; import { useDispatch } from 'react-redux'; import LinkifyIt from 'linkify-it'; - +import MarkdownIt from 'markdown-it'; import { RenderTextCallbackType } from '../../../../types/Util'; import { getEmojiSizeClass, SizeClassType } from '../../../../util/emoji'; import { AddMentions } from '../../AddMentions'; @@ -12,6 +12,42 @@ import { showLinkVisitWarningDialog } from '../../../dialog/SessionConfirm'; const linkify = LinkifyIt(); +const markdown = MarkdownIt('default', { + html: false, + linkify: true, + typographer: true, + // This seems not to work: + breaks: false + } +) + // tslint:disable:no-var-requires no-require-imports + .use(require('markdown-it-abbr')) + .use(require('markdown-it-sub')) + .use(require('markdown-it-sup')) + .use(require('markdown-it-ins')) + .use(require('markdown-it-mark')) + .use(require('markdown-it-container'), 'spoiler', { + validate: (params: string) => { + return params.trim().match(/^spoiler\s+(.*)$/); + }, + + render: (tokens: Array, idx: number) => { + const m = tokens[idx].info.trim().match(/^spoiler\s+(.*)$/); + + if (tokens[idx].nesting === 1) { + // opening tag + return `
${markdown.utils.escapeHtml(m[1])}\n`; + } else { + // closing tag + return '
\n'; + } + } + }) + .use(require('markdown-it-footnote')) + .use(require('markdown-it-highlightjs'), { inline: true } + // tslint:enable:no-var-requires no-require-imports +); + type Props = { text: string; /** If set, all emoji will be the same size. Otherwise, just one emoji will be large. */ @@ -104,8 +140,13 @@ export const MessageBody = (props: Props) => { ); } - if (text && text.startsWith('```') && text.endsWith('```') && text.length > 6) { - return
{text.substring(4, text.length - 3)}
; + if (window.getSettingValue('message-formatting')) { + /* tslint:disable:react-no-dangerous-html */ + return ( +
${markdown.render(text).trim()}`}} + /> + ); } return JsxSelectable( diff --git a/ts/components/settings/section/CategoryAppearance.tsx b/ts/components/settings/section/CategoryAppearance.tsx index 09c47c51c1c..e7a33ed5181 100644 --- a/ts/components/settings/section/CategoryAppearance.tsx +++ b/ts/components/settings/section/CategoryAppearance.tsx @@ -57,6 +57,11 @@ export const SettingsCategoryAppearance = (props: { hasPassword: boolean | null ? true : window.getSettingValue(SettingsKey.settingsMenuBar); + const isMessageFormattingActive = + window.getSettingValue(SettingsKey.settingsMessageFormatting) === undefined + ? false + : window.getSettingValue(SettingsKey.settingsMessageFormatting); + const isSpellCheckActive = window.getSettingValue(SettingsKey.settingsSpellCheck) === undefined ? true @@ -78,6 +83,15 @@ export const SettingsCategoryAppearance = (props: { hasPassword: boolean | null active={isHideMenuBarActive} /> )} + { + window.toggleMessageFormatting(); + forceUpdate(); + }} + title={window.i18n('messageFormattingTitle')} + description={window.i18n('messageFormattingDescription')} + active={isMessageFormattingActive} + /> { window.toggleSpellCheck(); diff --git a/ts/data/settings-key.ts b/ts/data/settings-key.ts index e0c1135e1bb..a307770474e 100644 --- a/ts/data/settings-key.ts +++ b/ts/data/settings-key.ts @@ -8,6 +8,8 @@ const settingsLinkPreview = 'link-preview-setting'; const settingsStartInTray = 'start-in-tray-setting'; const settingsOpengroupPruning = 'prune-setting'; +const settingsMessageFormatting = 'message-formatting'; + export const SettingsKey = { settingsReadReceipt, settingsTypingIndicator, @@ -17,4 +19,5 @@ export const SettingsKey = { settingsLinkPreview, settingsStartInTray, settingsOpengroupPruning, + settingsMessageFormatting }; diff --git a/ts/mains/main_renderer.tsx b/ts/mains/main_renderer.tsx index 27501e386f4..fa4edee6e2a 100644 --- a/ts/mains/main_renderer.tsx +++ b/ts/mains/main_renderer.tsx @@ -138,6 +138,11 @@ Storage.onready(async () => { window.setMenuBarVisibility(!value); }, + getMessageFormatting: () => Storage.get('message-formatting', true), + setMessageFormatting: async (value: boolean) => { + await Storage.put('message-formatting', value); + }, + getSpellCheck: () => Storage.get('spell-check', true), setSpellCheck: async (value: boolean) => { await Storage.put('spell-check', value); @@ -303,6 +308,12 @@ async function start() { window.Events.setHideMenuBar(!current); }; + window.toggleMessageFormatting = () => { + const currentValue = window.getSettingValue('message-formatting'); + const newValue = currentValue !== undefined ? !currentValue : true; + window.Events.setMessageFormatting(newValue); + }; + window.toggleSpellCheck = () => { const currentValue = window.getSettingValue('spell-check'); // if undefined, it means 'default' so true. but we have to toggle it, so false diff --git a/ts/markdown-it.d.ts b/ts/markdown-it.d.ts new file mode 100644 index 00000000000..ae4a4b412a1 --- /dev/null +++ b/ts/markdown-it.d.ts @@ -0,0 +1 @@ +declare module 'markdown-it'; diff --git a/ts/types/LocalizerKeys.ts b/ts/types/LocalizerKeys.ts index 766227d0014..ca9c790e1a8 100644 --- a/ts/types/LocalizerKeys.ts +++ b/ts/types/LocalizerKeys.ts @@ -52,6 +52,7 @@ export type LocalizerKeys = | 'fileSizeWarning' | 'openGroupURL' | 'hideMenuBarDescription' + | 'messageFormattingDescription' | 'pickClosedGroupMember' | 'ByUsingThisService...' | 'startConversation' @@ -255,6 +256,7 @@ export type LocalizerKeys = | 'editMenuDeleteContact' | 'hideMenuBarTitle' | 'reactionPopupOne' + | 'messageFormattingTitle' | 'imageCaptionIconAlt' | 'sendRecoveryPhraseTitle' | 'multipleJoinedTheGroup' diff --git a/ts/util/accountManager.ts b/ts/util/accountManager.ts index 2e0a31c186f..f5714f2bcb6 100644 --- a/ts/util/accountManager.ts +++ b/ts/util/accountManager.ts @@ -172,6 +172,9 @@ async function createAccount(identityKeyPair: SessionKeyPair) { await Storage.put(SettingsKey.settingsOpengroupPruning, true); await window.setOpengroupPruning(true); + // Disable message formatting by default. + await Storage.put(SettingsKey.settingsMessageFormatting, false); + await setLocalPubKey(pubKeyString); } diff --git a/ts/window.d.ts b/ts/window.d.ts index c0774264884..3e2451369b2 100644 --- a/ts/window.d.ts +++ b/ts/window.d.ts @@ -55,6 +55,7 @@ declare global { toggleCallMediaPermissionsTo: (enabled: boolean) => Promise; getCallMediaPermissions: () => boolean; toggleMenuBar: () => void; + toggleMessageFormatting: () => void; toggleSpellCheck: any; setTheme: (newTheme: string) => any; isDev?: () => boolean; diff --git a/yarn.lock b/yarn.lock index b7142792b69..8c590056e2d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3877,7 +3877,7 @@ entities@^2.0.0: resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55" integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A== -entities@^3.0.1: +entities@^3.0.1, entities@~3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/entities/-/entities-3.0.1.tgz#2b887ca62585e96db3903482d336c1006c3001d4" integrity sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q== @@ -5154,6 +5154,11 @@ he@1.2.0: resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== +highlight.js@^11.5.1: + version "11.6.0" + resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-11.6.0.tgz#a50e9da05763f1bb0c1322c8f4f755242cff3f5a" + integrity sha512-ig1eqDzJaB0pqEvlPVIpSSyMaO92bH1N2rJpLMN/nX396wTpDA4Eq0uK+7I/2XG17pFaaKE0kjV/XPeGt7Evjw== + hmac-drbg@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" @@ -6047,6 +6052,13 @@ linkify-it@3.0.2: dependencies: uc.micro "^1.0.1" +linkify-it@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-4.0.1.tgz#01f1d5e508190d06669982ba31a7d9f56a5751ec" + integrity sha512-C7bfi1UZmoj8+PQx22XyeXCuBlokoyWQL5pWSP+EI6nzRylyThouddufc2c1NDIcP9k5agmN9fLpA7VNJfIiqw== + dependencies: + uc.micro "^1.0.1" + livereload-js@^2.3.0: version "2.4.0" resolved "https://registry.yarnpkg.com/livereload-js/-/livereload-js-2.4.0.tgz#447c31cf1ea9ab52fc20db615c5ddf678f78009c" @@ -6231,6 +6243,59 @@ map-cache@^0.2.0: resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf" integrity sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8= +markdown-it-abbr@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/markdown-it-abbr/-/markdown-it-abbr-1.0.4.tgz#d66b5364521cbb3dd8aa59dadfba2fb6865c8fd8" + integrity sha512-ZeA4Z4SaBbYysZap5iZcxKmlPL6bYA8grqhzJIHB1ikn7njnzaP8uwbtuXc4YXD5LicI4/2Xmc0VwmSiFV04gg== + +markdown-it-container@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/markdown-it-container/-/markdown-it-container-3.0.0.tgz#1d19b06040a020f9a827577bb7dbf67aa5de9a5b" + integrity sha512-y6oKTq4BB9OQuY/KLfk/O3ysFhB3IMYoIWhGJEidXt1NQFocFK2sA2t0NYZAMyMShAGL6x5OPIbrmXPIqaN9rw== + +markdown-it-footnote@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/markdown-it-footnote/-/markdown-it-footnote-3.0.3.tgz#e0e4c0d67390a4c5f0c75f73be605c7c190ca4d8" + integrity sha512-YZMSuCGVZAjzKMn+xqIco9d1cLGxbELHZ9do/TSYVzraooV8ypsppKNmUJ0fVH5ljkCInQAtFpm8Rb3eXSrt5w== + +markdown-it-highlightjs@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/markdown-it-highlightjs/-/markdown-it-highlightjs-4.0.1.tgz#6b8eb6a3b971ed592db1ff160cfa5ce9f2e44232" + integrity sha512-EPXwFEN6P5nqR3G4KjT20r20xbGYKMMA/360hhSYFmeoGXTE6hsLtJAiB/8ID8slVH4CWHHEL7GX0YenyIstVQ== + dependencies: + highlight.js "^11.5.1" + +markdown-it-ins@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/markdown-it-ins/-/markdown-it-ins-3.0.1.tgz#c09356b917cf1dbf73add0b275d67ab8c73d4b4d" + integrity sha512-32SSfZqSzqyAmmQ4SHvhxbFqSzPDqsZgMHDwxqPzp+v+t8RsmqsBZRG+RfRQskJko9PfKC2/oxyOs4Yg/CfiRw== + +markdown-it-mark@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/markdown-it-mark/-/markdown-it-mark-3.0.1.tgz#51257db58787d78aaf46dc13418d99a9f3f0ebd3" + integrity sha512-HyxjAu6BRsdt6Xcv6TKVQnkz/E70TdGXEFHRYBGLncRE9lBFwDNLVtFojKxjJWgJ+5XxUwLaHXy+2sGBbDn+4A== + +markdown-it-sub@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/markdown-it-sub/-/markdown-it-sub-1.0.0.tgz#375fd6026eae7ddcb012497f6411195ea1e3afe8" + integrity sha512-z2Rm/LzEE1wzwTSDrI+FlPEveAAbgdAdPhdWarq/ZGJrGW/uCQbKAnhoCsE4hAbc3SEym26+W2z/VQB0cQiA9Q== + +markdown-it-sup@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/markdown-it-sup/-/markdown-it-sup-1.0.0.tgz#cb9c9ff91a5255ac08f3fd3d63286e15df0a1fc3" + integrity sha512-E32m0nV9iyhRR7CrhnzL5msqic7rL1juWre6TQNxsnApg7Uf+F97JOKxUijg5YwXz86lZ0mqfOnutoryyNdntQ== + +markdown-it@^13.0.1: + version "13.0.1" + resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-13.0.1.tgz#c6ecc431cacf1a5da531423fc6a42807814af430" + integrity sha512-lTlxriVoy2criHP0JKRhO2VDG9c2ypWCsT237eDiLqi09rmbKoUetyGHq2uOIRoRS//kfoJckS0eUzzkDR+k2Q== + dependencies: + argparse "^2.0.1" + entities "~3.0.1" + linkify-it "^4.0.1" + mdurl "^1.0.1" + uc.micro "^1.0.5" + matcher@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/matcher/-/matcher-3.0.0.tgz#bd9060f4c5b70aa8041ccc6f80368760994f30ca" @@ -6252,6 +6317,11 @@ mdn-data@2.0.14: resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.14.tgz#7113fc4281917d63ce29b43446f701e68c25ba50" integrity sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow== +mdurl@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e" + integrity sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g== + mem@^5.0.0: version "5.1.1" resolved "https://registry.yarnpkg.com/mem/-/mem-5.1.1.tgz#7059b67bf9ac2c924c9f1cff7155a064394adfb3" @@ -8856,7 +8926,7 @@ typescript@^4.6.3: resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.6.4.tgz#caa78bbc3a59e6a5c510d35703f6a09877ce45e9" integrity sha512-9ia/jWHIEbo49HfjrLGfKbZSuWo9iTMwXO+Ca3pRsSpbsMbc7/IU8NKdCZVRRBafVPGnoJeFL76ZOAA84I9fEg== -uc.micro@^1.0.1: +uc.micro@^1.0.1, uc.micro@^1.0.5: version "1.0.6" resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.6.tgz#9c411a802a409a91fc6cf74081baba34b24499ac" integrity sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==