diff --git a/res/css/_components.scss b/res/css/_components.scss index 2a34cd1734..006bac09c9 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -107,6 +107,7 @@ @import "./views/elements/_AddressTile.scss"; @import "./views/elements/_DesktopBuildsNotice.scss"; @import "./views/elements/_DirectorySearchBox.scss"; +@import "./views/elements/_DesktopCapturerSourcePicker.scss"; @import "./views/elements/_Dropdown.scss"; @import "./views/elements/_EditableItemList.scss"; @import "./views/elements/_ErrorBoundary.scss"; 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/structures/_RoomDirectory.scss b/res/css/structures/_RoomDirectory.scss index 29e6fecd34..89cb21b7a6 100644 --- a/res/css/structures/_RoomDirectory.scss +++ b/res/css/structures/_RoomDirectory.scss @@ -64,28 +64,23 @@ limitations under the License. } .mx_RoomDirectory_table { - font-size: $font-12px; color: $primary-fg-color; - width: 100%; + display: grid; + font-size: $font-12px; + grid-template-columns: max-content auto max-content max-content max-content; + row-gap: 24px; text-align: left; - table-layout: fixed; + width: 100%; } .mx_RoomDirectory_roomAvatar { - width: 32px; - padding-right: 14px; - vertical-align: top; -} - -.mx_RoomDirectory_roomDescription { - padding-bottom: 16px; + padding: 2px 14px 0 0; } .mx_RoomDirectory_roomMemberCount { + align-self: center; color: $light-fg-color; - width: 60px; - padding: 0 10px; - text-align: center; + padding: 3px 10px 0; &::before { background-color: $light-fg-color; @@ -105,8 +100,7 @@ limitations under the License. } .mx_RoomDirectory_join, .mx_RoomDirectory_preview { - width: 80px; - text-align: center; + align-self: center; white-space: nowrap; } diff --git a/res/css/views/elements/_DesktopCapturerSourcePicker.scss b/res/css/views/elements/_DesktopCapturerSourcePicker.scss new file mode 100644 index 0000000000..69dde5925e --- /dev/null +++ b/res/css/views/elements/_DesktopCapturerSourcePicker.scss @@ -0,0 +1,72 @@ +/* +Copyright 2021 Šimon Brandner + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +.mx_desktopCapturerSourcePicker { + overflow: hidden; +} + +.mx_desktopCapturerSourcePicker_tabLabels { + display: flex; + padding: 0 0 8px 0; +} + +.mx_desktopCapturerSourcePicker_tabLabel, +.mx_desktopCapturerSourcePicker_tabLabel_selected { + width: 100%; + text-align: center; + border-radius: 8px; + padding: 8px 0; + font-size: $font-13px; +} + +.mx_desktopCapturerSourcePicker_tabLabel_selected { + background-color: $tab-label-active-bg-color; + color: $tab-label-active-fg-color; +} + +.mx_desktopCapturerSourcePicker_panel { + display: flex; + flex-wrap: wrap; + justify-content: center; + align-items: flex-start; + height: 500px; + overflow: overlay; +} + +.mx_desktopCapturerSourcePicker_stream_button { + display: flex; + flex-direction: column; + margin: 8px; + border-radius: 4px; +} + +.mx_desktopCapturerSourcePicker_stream_button:hover, +.mx_desktopCapturerSourcePicker_stream_button:focus { + background: $roomtile-selected-bg-color; +} + +.mx_desktopCapturerSourcePicker_stream_thumbnail { + margin: 4px; + width: 312px; +} + +.mx_desktopCapturerSourcePicker_stream_name { + margin: 0 4px; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + width: 312px; +} 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/css/views/settings/_Notifications.scss b/res/css/views/settings/_Notifications.scss
index e6d09b9a2a..77a7bc5b68 100644
--- a/res/css/views/settings/_Notifications.scss
+++ b/res/css/views/settings/_Notifications.scss
@@ -64,6 +64,7 @@ limitations under the License.
 
 .mx_UserNotifSettings_notifTable {
     display: table;
+    position: relative;
 }
 
 .mx_UserNotifSettings_notifTable .mx_Spinner {
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/CallHandler.tsx b/src/CallHandler.tsx
index 9dee7594ea..a6d3534fa1 100644
--- a/src/CallHandler.tsx
+++ b/src/CallHandler.tsx
@@ -82,6 +82,7 @@ import CountlyAnalytics from "./CountlyAnalytics";
 import {UIFeature} from "./settings/UIFeature";
 import { CallError } from "matrix-js-sdk/src/webrtc/call";
 import { logger } from 'matrix-js-sdk/src/logger';
+import DesktopCapturerSourcePicker from "./components/views/elements/DesktopCapturerSourcePicker"
 import { Action } from './dispatcher/actions';
 import { roomForVirtualRoom, getOrCreateVirtualRoomForRoom } from './VoipUserMapper';
 import { addManagedHybridWidget, isManagedHybridWidgetEnabled } from './widgets/ManagedHybrid';
@@ -572,9 +573,17 @@ export default class CallHandler {
                 });
                 return;
             }
-            call.placeScreenSharingCall(remoteElement, localElement);
+
+            call.placeScreenSharingCall(
+                remoteElement,
+                localElement,
+                async () : Promise => {
+                    const {finished} = Modal.createDialog(DesktopCapturerSourcePicker);
+                    const [source] = await finished;
+                    return source;
+                });
         } else {
-            console.error("Unknown conf call type: %s", type);
+            console.error("Unknown conf call type: " + type);
         }
     }
 
@@ -608,7 +617,7 @@ export default class CallHandler {
 
                     const room = MatrixClientPeg.get().getRoom(payload.room_id);
                     if (!room) {
-                        console.error("Room %s does not exist.", payload.room_id);
+                        console.error(`Room ${payload.room_id} does not exist.`);
                         return;
                     }
 
@@ -619,7 +628,7 @@ export default class CallHandler {
                         });
                         return;
                     } else if (members.length === 2) {
-                        console.info("Place %s call in %s", payload.type, payload.room_id);
+                        console.info(`Place ${payload.type} call in ${payload.room_id}`);
 
                         this.placeCall(payload.room_id, payload.type, payload.local_element, payload.remote_element);
                     } else { // > 2
@@ -634,17 +643,17 @@ export default class CallHandler {
                 }
                 break;
             case 'place_conference_call':
-                console.info("Place conference call in %s", payload.room_id);
+                console.info("Place conference call in " + payload.room_id);
                 Analytics.trackEvent('voip', 'placeConferenceCall');
                 CountlyAnalytics.instance.trackStartCall(payload.room_id, payload.type === PlaceCallType.Video, true);
                 this.startCallApp(payload.room_id, payload.type);
                 break;
             case 'end_conference':
-                console.info("Terminating conference call in %s", payload.room_id);
+                console.info("Terminating conference call in " + payload.room_id);
                 this.terminateCallApp(payload.room_id);
                 break;
             case 'hangup_conference':
-                console.info("Leaving conference call in %s", payload.room_id);
+                console.info("Leaving conference call in "+ payload.room_id);
                 this.hangupCallApp(payload.room_id);
                 break;
             case 'incoming_call':
diff --git a/src/RoomNotifs.js b/src/RoomNotifs.js
index a86c521ac4..600655f635 100644
--- a/src/RoomNotifs.js
+++ b/src/RoomNotifs.js
@@ -202,12 +202,13 @@ function setRoomNotifsStateUnmuted(roomId, newState) {
 }
 
 function findOverrideMuteRule(roomId) {
-    if (!MatrixClientPeg.get().pushRules ||
-        !MatrixClientPeg.get().pushRules['global'] ||
-        !MatrixClientPeg.get().pushRules['global'].override) {
+    const cli = MatrixClientPeg.get();
+    if (!cli.pushRules ||
+        !cli.pushRules['global'] ||
+        !cli.pushRules['global'].override) {
         return null;
     }
-    for (const rule of MatrixClientPeg.get().pushRules['global'].override) {
+    for (const rule of cli.pushRules['global'].override) {
         if (isRuleForRoom(roomId, rule)) {
             if (isMuteRule(rule) && rule.enabled) {
                 return rule;
diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx
index 8ed4b6cd11..5045e44182 100644
--- a/src/components/structures/MatrixChat.tsx
+++ b/src/components/structures/MatrixChat.tsx
@@ -755,6 +755,8 @@ export default class MatrixChat extends React.PureComponent {
                 break;
             case 'on_logged_in':
                 if (
+                    // Skip this handling for token login as that always calls onLoggedIn itself
+                    !this.tokenLogin &&
                     !Lifecycle.isSoftLogout() &&
                     this.state.view !== Views.LOGIN &&
                     this.state.view !== Views.REGISTER &&
@@ -1373,6 +1375,9 @@ export default class MatrixChat extends React.PureComponent {
         cli.on('Session.logged_out', function(errObj) {
             if (Lifecycle.isLoggingOut()) return;
 
+            // A modal might have been open when we were logged out by the server
+            Modal.closeCurrentModal('Session.logged_out');
+
             if (errObj.httpStatus === 401 && errObj.data && errObj.data['soft_logout']) {
                 console.warn("Soft logout issued by server - avoiding data deletion");
                 Lifecycle.softLogout();
@@ -1383,6 +1388,7 @@ export default class MatrixChat extends React.PureComponent {
                 title: _t('Signed Out'),
                 description: _t('For security, this session has been signed out. Please sign in again.'),
             });
+
             dis.dispatch({
                 action: 'logout',
             });
@@ -1652,10 +1658,16 @@ export default class MatrixChat extends React.PureComponent {
             // TODO: Handle encoded room/event IDs: https://github.com/vector-im/element-web/issues/9149
 
             let threepidInvite: IThreepidInvite;
+            // if we landed here from a 3PID invite, persist it
             if (params.signurl && params.email) {
                 threepidInvite = ThreepidInviteStore.instance
                     .storeInvite(roomString, params as IThreepidInviteWireFormat);
             }
+            // otherwise check that this room doesn't already have a known invite
+            if (!threepidInvite) {
+                const invites = ThreepidInviteStore.instance.getInvites();
+                threepidInvite = invites.find(invite => invite.roomId === roomString);
+            }
 
             // on our URLs there might be a ?via=matrix.org or similar to help
             // joins to the room succeed. We'll pass these through as an array
diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js
index b8dae41ef8..371623642b 100644
--- a/src/components/structures/MessagePanel.js
+++ b/src/components/structures/MessagePanel.js
@@ -229,7 +229,7 @@ export default class MessagePanel extends React.Component {
 
     onAction = (payload) => {
         switch (payload.action) {
-            case "message_sent":
+            case "scroll_to_bottom":
                 this.scrollToBottom();
                 break;
         }
diff --git a/src/components/structures/RoomDirectory.js b/src/components/structures/RoomDirectory.js
index 9ddacaf829..7387e1aac0 100644
--- a/src/components/structures/RoomDirectory.js
+++ b/src/components/structures/RoomDirectory.js
@@ -477,7 +477,7 @@ export default class RoomDirectory extends React.Component {
         dis.dispatch(payload);
     }
 
-    getRow(room) {
+    createRoomCells(room) {
         const client = MatrixClientPeg.get();
         const clientRoom = client.getRoom(room.room_id);
         const hasJoinedRoom = clientRoom && clientRoom.getMyMembership() === "join";
@@ -523,31 +523,56 @@ export default class RoomDirectory extends React.Component {
                                 MatrixClientPeg.get().getHomeserverUrl(),
                                 room.avatar_url, 32, 32, "crop",
                             );
-        return (
-             this.onRoomClicked(room, ev)}
                 // cancel onMouseDown otherwise shift-clicking highlights text
                 onMouseDown={(ev) => {ev.preventDefault();}}
+                className="mx_RoomDirectory_roomAvatar"
             >
-                
-                    
-                
-                
-                    
{ name }
  -
{ ev.stopPropagation(); } } - dangerouslySetInnerHTML={{ __html: topic }} /> -
{ get_display_alias_for_room(room) }
- - - { room.num_joined_members } - - {previewButton} - {joinOrViewButton} - - ); + +
, +
this.onRoomClicked(room, ev)} + // cancel onMouseDown otherwise shift-clicking highlights text + onMouseDown={(ev) => {ev.preventDefault();}} + className="mx_RoomDirectory_roomDescription" + > +
{ name }
  +
{ ev.stopPropagation(); } } + dangerouslySetInnerHTML={{ __html: topic }} + /> +
{ get_display_alias_for_room(room) }
+
, +
this.onRoomClicked(room, ev)} + // cancel onMouseDown otherwise shift-clicking highlights text + onMouseDown={(ev) => {ev.preventDefault();}} + className="mx_RoomDirectory_roomMemberCount" + > + { room.num_joined_members } +
, +
this.onRoomClicked(room, ev)} + // cancel onMouseDown otherwise shift-clicking highlights text + onMouseDown={(ev) => {ev.preventDefault();}} + className="mx_RoomDirectory_preview" + > + {previewButton} +
, +
this.onRoomClicked(room, ev)} + // cancel onMouseDown otherwise shift-clicking highlights text + onMouseDown={(ev) => {ev.preventDefault();}} + className="mx_RoomDirectory_join" + > + {joinOrViewButton} +
, + ]; } collectScrollPanel = (element) => { @@ -606,7 +631,8 @@ export default class RoomDirectory extends React.Component { } else if (this.state.protocolsLoading) { content = ; } else { - const rows = (this.state.publicRooms || []).map(room => this.getRow(room)); + const cells = (this.state.publicRooms || []) + .reduce((cells, room) => cells.concat(this.createRoomCells(room)), [],); // we still show the scrollpanel, at least for now, because // otherwise we don't fetch more because we don't get a fill // request from the scrollpanel because there isn't one @@ -617,14 +643,12 @@ export default class RoomDirectory extends React.Component { } let scrollpanel_content; - if (rows.length === 0 && !this.state.loading) { + if (cells.length === 0 && !this.state.loading) { scrollpanel_content = { _t('No rooms to show') }; } else { - scrollpanel_content = - - { rows } - -
; + scrollpanel_content =
+ { cells } +
; } const ScrollPanel = sdk.getComponent("structures.ScrollPanel"); content = + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from 'react'; +import { _t } from '../../../languageHandler'; +import BaseDialog from "..//dialogs/BaseDialog" +import AccessibleButton from './AccessibleButton'; +import {getDesktopCapturerSources} from "matrix-js-sdk/src/webrtc/call"; + +export interface DesktopCapturerSource { + id: string; + name: string; + thumbnailURL; +} + +export enum Tabs { + Screens = "screens", + Windows = "windows", +} + +export interface DesktopCapturerSourceIProps { + source: DesktopCapturerSource; + onSelect(source: DesktopCapturerSource): void; +} + +export class ExistingSource extends React.Component { + constructor(props) { + super(props); + } + + onClick = (ev) => { + this.props.onSelect(this.props.source); + } + + render() { + return ( + + + {this.props.source.name} + + ); + } +} + +export interface DesktopCapturerSourcePickerIState { + selectedTab: Tabs; + sources: Array; +} +export interface DesktopCapturerSourcePickerIProps { + onFinished(source: DesktopCapturerSource): void; +} + +export default class DesktopCapturerSourcePicker extends React.Component< + DesktopCapturerSourcePickerIProps, + DesktopCapturerSourcePickerIState + > { + interval; + + constructor(props) { + super(props); + + this.state = { + selectedTab: Tabs.Screens, + sources: [], + }; + } + + componentDidMount() { + // We update the sources every 500ms to get newer thumbnails + this.interval = setInterval(async () => { + this.setState({ + sources: await getDesktopCapturerSources(), + }); + }, 500); + } + + componentWillUnmount() { + clearInterval(this.interval); + } + + onSelect = (source) => { + this.props.onFinished(source); + } + + onScreensClick = (ev) => { + this.setState({selectedTab: Tabs.Screens}); + } + + onWindowsClick = (ev) => { + this.setState({selectedTab: Tabs.Windows}); + } + + onCloseClick = (ev) => { + this.props.onFinished(null); + } + + render() { + let sources; + if (this.state.selectedTab === Tabs.Screens) { + sources = this.state.sources + .filter((source) => { + return source.id.startsWith("screen"); + }) + .map((source) => { + return ; + }); + } else { + sources = this.state.sources + .filter((source) => { + return source.id.startsWith("window"); + }) + .map((source) => { + return ; + }); + } + + const buttonStyle = "mx_desktopCapturerSourcePicker_tabLabel"; + const screensButtonStyle = buttonStyle + ((this.state.selectedTab === Tabs.Screens) ? "_selected" : ""); + const windowsButtonStyle = buttonStyle + ((this.state.selectedTab === Tabs.Windows) ? "_selected" : ""); + + return ( + +
+ + {_t("Screens")} + + + {_t("Windows")} + +
+
+ { sources } +
+
+ ); + } +} 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/room_settings/RoomProfileSettings.js b/src/components/views/room_settings/RoomProfileSettings.js
index c76c0823e4..c651216e8c 100644
--- a/src/components/views/room_settings/RoomProfileSettings.js
+++ b/src/components/views/room_settings/RoomProfileSettings.js
@@ -69,19 +69,24 @@ export default class RoomProfileSettings extends React.Component {
         // clear file upload field so same file can be selected
         this._avatarUpload.current.value = "";
         this.setState({
-            avatarUrl: undefined,
-            avatarFile: undefined,
+            avatarUrl: null,
+            avatarFile: null,
             enableProfileSave: true,
         });
     };
 
-    _clearProfile = async (e) => {
+    _cancelProfileChanges = async (e) => {
         e.stopPropagation();
         e.preventDefault();
 
         if (!this.state.enableProfileSave) return;
-        this._removeAvatar();
-        this.setState({enableProfileSave: false, displayName: this.state.originalDisplayName});
+        this.setState({
+            enableProfileSave: false,
+            displayName: this.state.originalDisplayName,
+            topic: this.state.originalTopic,
+            avatarUrl: this.state.originalAvatarUrl,
+            avatarFile: null,
+        });
     };
 
     _saveProfile = async (e) => {
@@ -108,7 +113,7 @@ export default class RoomProfileSettings extends React.Component {
             newState.originalAvatarUrl = newState.avatarUrl;
             newState.avatarFile = null;
         } else if (this.state.originalAvatarUrl !== this.state.avatarUrl) {
-            await client.sendStateEvent(this.props.roomId, 'm.room.avatar', {url: undefined}, '');
+            await client.sendStateEvent(this.props.roomId, 'm.room.avatar', {}, '');
         }
 
         if (this.state.originalTopic !== this.state.topic) {
@@ -164,11 +169,15 @@ export default class RoomProfileSettings extends React.Component {
         const AvatarSetting = sdk.getComponent('settings.AvatarSetting');
 
         let profileSettingsButtons;
-        if (this.state.canSetTopic && this.state.canSetName) {
+        if (
+            this.state.canSetName ||
+            this.state.canSetTopic ||
+            this.state.canSetAvatar
+        ) {
             profileSettingsButtons = (
                 
diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js index da430e622a..6a867386f7 100644 --- a/src/components/views/rooms/MessageComposer.js +++ b/src/components/views/rooms/MessageComposer.js @@ -426,7 +426,8 @@ export default class MessageComposer extends React.Component { , ); - if (SettingsStore.getValue(UIFeature.Widgets)) { + if (SettingsStore.getValue(UIFeature.Widgets) && + SettingsStore.getValue("MessageComposerInput.showStickersButton")) { controls.push(); } diff --git a/src/components/views/rooms/RoomBreadcrumbs.tsx b/src/components/views/rooms/RoomBreadcrumbs.tsx index 7725ce456e..ff60ab7779 100644 --- a/src/components/views/rooms/RoomBreadcrumbs.tsx +++ b/src/components/views/rooms/RoomBreadcrumbs.tsx @@ -111,7 +111,7 @@ export default class RoomBreadcrumbs extends React.PureComponent appear={true} in={this.state.doAnimation} timeout={640} classNames='mx_RoomBreadcrumbs' > - + {tiles.slice(this.state.skipFirst ? 1 : 0)} diff --git a/src/components/views/rooms/SendMessageComposer.js b/src/components/views/rooms/SendMessageComposer.js index 7b516e1f52..62c474e417 100644 --- a/src/components/views/rooms/SendMessageComposer.js +++ b/src/components/views/rooms/SendMessageComposer.js @@ -403,6 +403,7 @@ export default class SendMessageComposer extends React.Component { this._editorRef.clearUndoHistory(); this._editorRef.focus(); this._clearStoredEditorState(); + dis.dispatch({action: "scroll_to_bottom"}); } componentWillUnmount() { diff --git a/src/components/views/settings/ProfileSettings.js b/src/components/views/settings/ProfileSettings.js index c11a2e3a5e..4dc69884c3 100644 --- a/src/components/views/settings/ProfileSettings.js +++ b/src/components/views/settings/ProfileSettings.js @@ -52,19 +52,23 @@ export default class ProfileSettings extends React.Component { // clear file upload field so same file can be selected this._avatarUpload.current.value = ""; this.setState({ - avatarUrl: undefined, - avatarFile: undefined, + avatarUrl: null, + avatarFile: null, enableProfileSave: true, }); }; - _clearProfile = async (e) => { + _cancelProfileChanges = async (e) => { e.stopPropagation(); e.preventDefault(); if (!this.state.enableProfileSave) return; - this._removeAvatar(); - this.setState({enableProfileSave: false, displayName: this.state.originalDisplayName}); + this.setState({ + enableProfileSave: false, + displayName: this.state.originalDisplayName, + avatarUrl: this.state.originalAvatarUrl, + avatarFile: null, + }); }; _saveProfile = async (e) => { @@ -186,7 +190,7 @@ export default class ProfileSettings extends React.Component {
diff --git a/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.js b/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.js index a32549b326..04fcea39dc 100644 --- a/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.js @@ -34,6 +34,7 @@ export default class PreferencesUserSettingsTab extends React.Component { 'MessageComposerInput.suggestEmoji', 'sendTypingNotifications', 'MessageComposerInput.ctrlEnterToSend', + 'MessageComposerInput.showStickersButton', ]; static TIMELINE_SETTINGS = [ @@ -46,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 347f12f9fa..9a01af7d40 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -795,6 +795,7 @@ "Font size": "Font size", "Use custom size": "Use custom size", "Enable Emoji suggestions while typing": "Enable Emoji suggestions while typing", + "Show stickers button": "Show stickers button", "Use a more compact ‘Modern’ layout": "Use a more compact ‘Modern’ layout", "Show a placeholder for removed messages": "Show a placeholder for removed messages", "Show join/leave messages (invites/kicks/bans unaffected)": "Show join/leave messages (invites/kicks/bans unaffected)", @@ -805,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", @@ -1462,6 +1465,7 @@ "Seen by %(displayName)s (%(userName)s) at %(dateTime)s": "Seen by %(displayName)s (%(userName)s) at %(dateTime)s", "Replying": "Replying", "Room %(name)s": "Room %(name)s", + "Recently visited rooms": "Recently visited rooms", "No recently visited rooms": "No recently visited rooms", "No rooms to show": "No rooms to show", "Unnamed room": "Unnamed room", @@ -1851,6 +1855,9 @@ "Use the Desktop app to search encrypted messages": "Use the Desktop app to search encrypted messages", "This version of %(brand)s does not support viewing some encrypted files": "This version of %(brand)s does not support viewing some encrypted files", "This version of %(brand)s does not support searching encrypted messages": "This version of %(brand)s does not support searching encrypted messages", + "Share your screen": "Share your screen", + "Screens": "Screens", + "Windows": "Windows", "Join": "Join", "No results": "No results", "Please create a new issue on GitHub so that we can investigate this bug.": "Please create a new issue on GitHub so that we can investigate this bug.", diff --git a/src/settings/Settings.ts b/src/settings/Settings.ts index 6415723f56..385b892478 100644 --- a/src/settings/Settings.ts +++ b/src/settings/Settings.ts @@ -240,6 +240,11 @@ export const SETTINGS: {[setting: string]: ISetting} = { default: true, invertedSettingName: 'MessageComposerInput.dontSuggestEmoji', }, + "MessageComposerInput.showStickersButton": { + supportedLevels: LEVELS_ACCOUNT_SETTINGS, + displayName: _td('Show stickers button'), + default: true, + }, // TODO: Wire up appropriately to UI (FTUE notifications) "Notifications.alwaysShowBadgeCounts": { supportedLevels: LEVELS_ROOM_OR_ACCOUNT, @@ -300,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'),