diff --git a/res/css/structures/_LeftPanelWidget.scss b/res/css/structures/_LeftPanelWidget.scss index 4df651d7b6..6e2d99bb37 100644 --- a/res/css/structures/_LeftPanelWidget.scss +++ b/res/css/structures/_LeftPanelWidget.scss @@ -134,7 +134,7 @@ limitations under the License. mask-position: center; mask-size: contain; mask-repeat: no-repeat; - mask-image: url('$(res)/img/feather-customised/widget/maximise.svg'); + mask-image: url('$(res)/img/feather-customised/maximise.svg'); background: $muted-fg-color; } } diff --git a/res/css/views/messages/_ViewSourceEvent.scss b/res/css/views/messages/_ViewSourceEvent.scss index 076932ee97..66825030e0 100644 --- a/res/css/views/messages/_ViewSourceEvent.scss +++ b/res/css/views/messages/_ViewSourceEvent.scss @@ -35,13 +35,13 @@ limitations under the License. mask-size: auto 12px; visibility: hidden; background-color: $accent-color; - mask-image: url('$(res)/img/feather-customised/widget/maximise.svg'); + mask-image: url('$(res)/img/feather-customised/maximise.svg'); } &.mx_ViewSourceEvent_expanded .mx_ViewSourceEvent_toggle { mask-position: 0 bottom; margin-bottom: 7px; - mask-image: url('$(res)/img/feather-customised/widget/minimise.svg'); + mask-image: url('$(res)/img/feather-customised/minimise.svg'); } &:hover .mx_ViewSourceEvent_toggle { diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index 42d91b3eb5..da94f914f7 100644 --- a/res/css/views/rooms/_EventTile.scss +++ b/res/css/views/rooms/_EventTile.scss @@ -491,7 +491,6 @@ $left-gutter: 64px; // https://github.com/vector-im/vector-web/issues/754 overflow-x: overlay; overflow-y: visible; - max-height: 30vh; } code { @@ -500,6 +499,22 @@ $left-gutter: 64px; } } +.mx_EventTile_lineNumbers { + float: left; + margin: 0 0.5em 0 -1.5em; + color: gray; +} + +.mx_EventTile_lineNumber { + text-align: right; + display: block; + padding-left: 1em; +} + +.mx_EventTile_collapsedCodeBlock { + max-height: 30vh; +} + .mx_EventTile:hover .mx_EventTile_body pre, .mx_EventTile.focus-visible:focus-within .mx_EventTile_body pre { border: 1px solid #e5e5e5; // deliberate constant as we're behind an invert filter @@ -511,7 +526,7 @@ $left-gutter: 64px; } // Inserted adjacent to
blocks, (See TextualBody)
-.mx_EventTile_copyButton {
+.mx_EventTile_button {
position: absolute;
display: inline-block;
visibility: hidden;
@@ -520,12 +535,33 @@ $left-gutter: 64px;
right: 6px;
width: 19px;
height: 19px;
- mask-image: url($copy-button-url);
background-color: $message-action-bar-fg-color;
}
+.mx_EventTile_buttonBottom {
+ top: 31px;
+}
+.mx_EventTile_copyButton {
+ mask-image: url($copy-button-url);
+}
+.mx_EventTile_collapseButton {
+ mask-size: 75%;
+ mask-position: center;
+ mask-repeat: no-repeat;
+ mask-image: url($collapse-button-url);
+}
+.mx_EventTile_expandButton {
+ mask-size: 75%;
+ mask-position: center;
+ mask-repeat: no-repeat;
+ mask-image: url($expand-button-url);
+}
.mx_EventTile_body .mx_EventTile_pre_container:focus-within .mx_EventTile_copyButton,
-.mx_EventTile_body .mx_EventTile_pre_container:hover .mx_EventTile_copyButton {
+.mx_EventTile_body .mx_EventTile_pre_container:hover .mx_EventTile_copyButton,
+.mx_EventTile_body .mx_EventTile_pre_container:focus-within .mx_EventTile_collapseButton,
+.mx_EventTile_body .mx_EventTile_pre_container:hover .mx_EventTile_collapseButton,
+.mx_EventTile_body .mx_EventTile_pre_container:focus-within .mx_EventTile_expandButton,
+.mx_EventTile_body .mx_EventTile_pre_container:hover .mx_EventTile_expandButton {
visibility: visible;
}
diff --git a/res/img/feather-customised/widget/maximise.svg b/res/img/feather-customised/maximise.svg
similarity index 100%
rename from res/img/feather-customised/widget/maximise.svg
rename to res/img/feather-customised/maximise.svg
diff --git a/res/img/feather-customised/widget/minimise.svg b/res/img/feather-customised/minimise.svg
similarity index 100%
rename from res/img/feather-customised/widget/minimise.svg
rename to res/img/feather-customised/minimise.svg
diff --git a/res/themes/legacy-light/css/_legacy-light.scss b/res/themes/legacy-light/css/_legacy-light.scss
index 085d6d7f10..a740ba155c 100644
--- a/res/themes/legacy-light/css/_legacy-light.scss
+++ b/res/themes/legacy-light/css/_legacy-light.scss
@@ -237,7 +237,8 @@ $event-redacted-border-color: #cccccc;
$event-timestamp-color: #acacac;
$copy-button-url: "$(res)/img/feather-customised/clipboard.svg";
-
+$collapse-button-url: "$(res)/img/feather-customised/minimise.svg";
+$expand-button-url: "$(res)/img/feather-customised/maximise.svg";
// e2e
$e2e-verified-color: #76cfa5; // N.B. *NOT* the same as $accent-color
diff --git a/res/themes/light/css/_light.scss b/res/themes/light/css/_light.scss
index 4cfeeae05e..1c89d83c01 100644
--- a/res/themes/light/css/_light.scss
+++ b/res/themes/light/css/_light.scss
@@ -237,6 +237,8 @@ $event-redacted-border-color: #cccccc;
$event-timestamp-color: #acacac;
$copy-button-url: "$(res)/img/feather-customised/clipboard.svg";
+$collapse-button-url: "$(res)/img/feather-customised/minimise.svg";
+$expand-button-url: "$(res)/img/feather-customised/maximise.svg";
// e2e
$e2e-verified-color: #76cfa5; // N.B. *NOT* the same as $accent-color
diff --git a/src/components/views/messages/TextualBody.js b/src/components/views/messages/TextualBody.js
index bf0884a2f3..71f7572a25 100644
--- a/src/components/views/messages/TextualBody.js
+++ b/src/components/views/messages/TextualBody.js
@@ -81,6 +81,7 @@ export default class TextualBody extends React.Component {
}
_applyFormatting() {
+ const showLineNumbers = SettingsStore.getValue("showCodeLineNumbers");
this.activateSpoilers([this._content.current]);
// pillifyLinks BEFORE linkifyElement because plain room/user URLs in the composer
@@ -91,29 +92,136 @@ export default class TextualBody extends React.Component {
this.calculateUrlPreview();
if (this.props.mxEvent.getContent().format === "org.matrix.custom.html") {
- const blocks = ReactDOM.findDOMNode(this).getElementsByTagName("code");
- if (blocks.length > 0) {
- // Do this asynchronously: parsing code takes time and we don't
- // need to block the DOM update on it.
- setTimeout(() => {
- if (this._unmounted) return;
- for (let i = 0; i < blocks.length; i++) {
- if (SettingsStore.getValue("enableSyntaxHighlightLanguageDetection")) {
- highlight.highlightBlock(blocks[i]);
- } else {
- // Only syntax highlight if there's a class starting with language-
- const classes = blocks[i].className.split(/\s+/).filter(function(cl) {
- return cl.startsWith('language-') && !cl.startsWith('language-_');
- });
-
- if (classes.length != 0) {
- highlight.highlightBlock(blocks[i]);
- }
- }
+ // Handle expansion and add buttons
+ const pres = ReactDOM.findDOMNode(this).getElementsByTagName("pre");
+ if (pres.length > 0) {
+ for (let i = 0; i < pres.length; i++) {
+ // Wrap a div around so that the copy button can be correctly positioned
+ // when the overflows and is scrolled horizontally.
+ const div = this._wrapInDiv(pres[i]);
+ this._handleCodeBlockExpansion(pres[i]);
+ this._addCodeExpansionButton(div, pres[i]);
+ this._addCodeCopyButton(div);
+ if (showLineNumbers) {
+ this._addLineNumbers(pres[i]);
}
- }, 10);
+ }
+ }
+ // Highlight code
+ const codes = ReactDOM.findDOMNode(this).getElementsByTagName("code");
+ if (codes.length > 0) {
+ for (let i = 0; i < codes.length; i++) {
+ // Do this asynchronously: parsing code takes time and we don't
+ // need to block the DOM update on it.
+ setTimeout(() => {
+ if (this._unmounted) return;
+ for (let i = 0; i < pres.length; i++) {
+ this._highlightCode(codes[i]);
+ }
+ }, 10);
+ }
+ }
+ }
+ }
+
+ _addCodeExpansionButton(div, pre) {
+ // Calculate how many percent does the pre element take up.
+ // If it's less than 30% we don't add the expansion button.
+ const percentageOfViewport = pre.offsetHeight / window.innerHeight * 100;
+ if (percentageOfViewport < 30) return;
+
+ const button = document.createElement("span");
+ button.className = "mx_EventTile_button ";
+ if (pre.className == "mx_EventTile_collapsedCodeBlock") {
+ button.className += "mx_EventTile_expandButton";
+ } else {
+ button.className += "mx_EventTile_collapseButton";
+ }
+
+ button.onclick = async () => {
+ button.className = "mx_EventTile_button ";
+ if (pre.className == "mx_EventTile_collapsedCodeBlock") {
+ pre.className = "";
+ button.className += "mx_EventTile_collapseButton";
+ } else {
+ pre.className = "mx_EventTile_collapsedCodeBlock";
+ button.className += "mx_EventTile_expandButton";
+ }
+
+ // By expanding/collapsing we changed
+ // the height, therefore we call this
+ this.props.onHeightChanged();
+ };
+
+ div.appendChild(button);
+ }
+
+ _addCodeCopyButton(div) {
+ const button = document.createElement("span");
+ button.className = "mx_EventTile_button mx_EventTile_copyButton ";
+
+ // Check if expansion button exists. If so
+ // we put the copy button to the bottom
+ const expansionButtonExists = div.getElementsByClassName("mx_EventTile_button");
+ if (expansionButtonExists.length > 0) button.className += "mx_EventTile_buttonBottom";
+
+ button.onclick = async () => {
+ const copyCode = button.parentNode.getElementsByTagName("code")[0];
+ const successful = await copyPlaintext(copyCode.textContent);
+
+ const buttonRect = button.getBoundingClientRect();
+ const GenericTextContextMenu = sdk.getComponent('context_menus.GenericTextContextMenu');
+ const {close} = ContextMenu.createMenu(GenericTextContextMenu, {
+ ...toRightOf(buttonRect, 2),
+ message: successful ? _t('Copied!') : _t('Failed to copy'),
+ });
+ button.onmouseleave = close;
+ };
+
+ div.appendChild(button);
+ }
+
+ _wrapInDiv(pre) {
+ const div = document.createElement("div");
+ div.className = "mx_EventTile_pre_container";
+
+ // Insert containing div in place of block
+ pre.parentNode.replaceChild(div, pre);
+ // Append block and copy button to container
+ div.appendChild(pre);
+
+ return div;
+ }
+
+ _handleCodeBlockExpansion(pre) {
+ if (!SettingsStore.getValue("expandCodeByDefault")) {
+ pre.className = "mx_EventTile_collapsedCodeBlock";
+ }
+ }
+
+ _addLineNumbers(pre) {
+ pre.innerHTML = '' + pre.innerHTML + '';
+ const lineNumbers = pre.getElementsByClassName("mx_EventTile_lineNumbers")[0];
+ // Calculate number of lines in pre
+ const number = pre.innerHTML.split(/\n/).length;
+ // Iterate through lines starting with 1 (number of the first line is 1)
+ for (let i = 1; i < number; i++) {
+ lineNumbers.innerHTML += '' + i + '';
+ }
+ }
+
+ _highlightCode(code) {
+ if (SettingsStore.getValue("enableSyntaxHighlightLanguageDetection")) {
+ highlight.highlightBlock(code);
+ } else {
+ // Only syntax highlight if there's a class starting with language-
+ const classes = code.className.split(/\s+/).filter(function(cl) {
+ return cl.startsWith('language-') && !cl.startsWith('language-_');
+ });
+
+ if (classes.length != 0) {
+ highlight.highlightBlock(code);
}
- this._addCodeCopyButton();
}
}
@@ -254,38 +362,6 @@ export default class TextualBody extends React.Component {
}
}
- _addCodeCopyButton() {
- // Add 'copy' buttons to pre blocks
- Array.from(ReactDOM.findDOMNode(this).querySelectorAll('.mx_EventTile_body pre')).forEach((p) => {
- const button = document.createElement("span");
- button.className = "mx_EventTile_copyButton";
- button.onclick = async () => {
- const copyCode = button.parentNode.getElementsByTagName("pre")[0];
- const successful = await copyPlaintext(copyCode.textContent);
-
- const buttonRect = button.getBoundingClientRect();
- const GenericTextContextMenu = sdk.getComponent('context_menus.GenericTextContextMenu');
- const {close} = ContextMenu.createMenu(GenericTextContextMenu, {
- ...toRightOf(buttonRect, 2),
- message: successful ? _t('Copied!') : _t('Failed to copy'),
- });
- button.onmouseleave = close;
- };
-
- // Wrap a div around so that the copy button can be correctly positioned
- // when the overflows and is scrolled horizontally.
- const div = document.createElement("div");
- div.className = "mx_EventTile_pre_container";
-
- // Insert containing div in place of block
- p.parentNode.replaceChild(div, p);
-
- // Append block and copy button to container
- div.appendChild(p);
- div.appendChild(button);
- });
- }
-
onCancelClick = event => {
this.setState({ widgetHidden: true });
// FIXME: persist this somewhere smarter than local storage
diff --git a/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.js b/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.js
index 91df7cb2eb..04fcea39dc 100644
--- a/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.js
+++ b/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.js
@@ -47,6 +47,8 @@ export default class PreferencesUserSettingsTab extends React.Component {
'alwaysShowTimestamps',
'showRedactions',
'enableSyntaxHighlightLanguageDetection',
+ 'expandCodeByDefault',
+ 'showCodeLineNumbers',
'showJoinLeaves',
'showAvatarChanges',
'showDisplaynameChanges',
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index 06aeec75fd..7986184d97 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -806,6 +806,8 @@
"Always show message timestamps": "Always show message timestamps",
"Autoplay GIFs and videos": "Autoplay GIFs and videos",
"Enable automatic language detection for syntax highlighting": "Enable automatic language detection for syntax highlighting",
+ "Expand code blocks by default": "Expand code blocks by default",
+ "Show line numbers in code blocks": "Show line numbers in code blocks",
"Show avatars in user and room mentions": "Show avatars in user and room mentions",
"Enable big emoji in chat": "Enable big emoji in chat",
"Send typing notifications": "Send typing notifications",
diff --git a/src/settings/Settings.ts b/src/settings/Settings.ts
index 9e29df94b6..385b892478 100644
--- a/src/settings/Settings.ts
+++ b/src/settings/Settings.ts
@@ -305,6 +305,16 @@ export const SETTINGS: {[setting: string]: ISetting} = {
displayName: _td('Enable automatic language detection for syntax highlighting'),
default: false,
},
+ "expandCodeByDefault": {
+ supportedLevels: LEVELS_ACCOUNT_SETTINGS,
+ displayName: _td('Expand code blocks by default'),
+ default: false,
+ },
+ "showCodeLineNumbers": {
+ supportedLevels: LEVELS_ACCOUNT_SETTINGS,
+ displayName: _td('Show line numbers in code blocks'),
+ default: true,
+ },
"Pill.shouldShowPillAvatar": {
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
displayName: _td('Show avatars in user and room mentions'),