diff --git a/res/css/_components.scss b/res/css/_components.scss index a6e1f81583..006bac09c9 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -71,6 +71,7 @@ @import "./views/dialogs/_EditCommunityPrototypeDialog.scss"; @import "./views/dialogs/_FeedbackDialog.scss"; @import "./views/dialogs/_GroupAddressPicker.scss"; +@import "./views/dialogs/_HostSignupDialog.scss"; @import "./views/dialogs/_IncomingSasDialog.scss"; @import "./views/dialogs/_InviteDialog.scss"; @import "./views/dialogs/_KeyboardShortcutsDialog.scss"; @@ -106,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/structures/_UserMenu.scss b/res/css/structures/_UserMenu.scss index 0673ebb058..f2577938e5 100644 --- a/res/css/structures/_UserMenu.scss +++ b/res/css/structures/_UserMenu.scss @@ -272,6 +272,9 @@ limitations under the License. .mx_UserMenu_iconHome::before { mask-image: url('$(res)/img/element-icons/roomlist/home.svg'); } + .mx_UserMenu_iconHosting::before { + mask-image: url('$(res)/img/element-icons/brands/element.svg'); + } .mx_UserMenu_iconBell::before { mask-image: url('$(res)/img/element-icons/notifications.svg'); diff --git a/res/css/views/dialogs/_HostSignupDialog.scss b/res/css/views/dialogs/_HostSignupDialog.scss new file mode 100644 index 0000000000..1378ac9053 --- /dev/null +++ b/res/css/views/dialogs/_HostSignupDialog.scss @@ -0,0 +1,138 @@ +/* +Copyright 2021 The Matrix.org Foundation C.I.C. + +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_HostSignupDialog { + width: 90vw; + max-width: 580px; + height: 80vh; + max-height: 600px; + + .mx_HostSignupDialog_info { + text-align: center; + + .mx_HostSignupDialog_content_top { + margin-bottom: 24px; + } + + .mx_HostSignupDialog_paragraphs { + text-align: left; + padding-left: 25%; + padding-right: 25%; + } + + .mx_HostSignupDialog_buttons { + margin-bottom: 24px; + display: flex; + justify-content: center; + + button { + padding: 12px; + margin: 0 16px; + } + } + + .mx_HostSignupDialog_footer { + display: flex; + justify-content: center; + align-items: baseline; + + img { + padding-right: 5px; + } + } + } + + iframe { + width: 100%; + height: 100%; + border: none; + background-color: #fff; + min-height: 540px; + } +} + +.mx_HostSignupDialog_text_dark { + color: $primary-fg-color; +} + +.mx_HostSignupDialog_text_light { + color: $secondary-fg-color; +} + +.mx_HostSignup_maximize_button { + mask: url('$(res)/img/feather-customised/maximise.svg'); + mask-repeat: no-repeat; + mask-position: center; + mask-size: cover; + width: 14px; + height: 14px; + background-color: $dialog-close-fg-color; + cursor: pointer; + position: absolute; + top: 10px; + right: 10px; +} + +.mx_HostSignup_minimize_button { + mask: url('$(res)/img/feather-customised/minimise.svg'); + mask-repeat: no-repeat; + mask-position: center; + mask-size: cover; + width: 14px; + height: 14px; + background-color: $dialog-close-fg-color; + cursor: pointer; + position: absolute; + top: 10px; + right: 25px; +} + +.mx_HostSignup_persisted { + width: 90vw; + max-width: 580px; + height: 80vh; + max-height: 600px; + top: 0; + left: 0; + position: fixed; + display: none; +} + +.mx_HostSignupDialog_minimized { + position: fixed; + bottom: 80px; + right: 26px; + width: 314px; + height: 217px; + overflow: hidden; + + &.mx_Dialog { + padding: 12px; + } + + .mx_Dialog_title { + text-align: left !important; + padding-left: 20px; + font-size: $font-15px; + } + + iframe { + width: 100%; + height: 100%; + border: none; + background-color: #fff; + } +} 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..42df3211de 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,21 +526,42 @@ $left-gutter: 64px; } // Inserted adjacent to
 blocks, (See TextualBody)
-.mx_EventTile_copyButton {
+.mx_EventTile_button {
     position: absolute;
     display: inline-block;
     visibility: hidden;
     cursor: pointer;
     top: 6px;
-    right: 6px;
+    right: 12px;
     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/element-icons/brands/element.svg b/res/img/element-icons/brands/element.svg
new file mode 100644
index 0000000000..6861de0955
--- /dev/null
+++ b/res/img/element-icons/brands/element.svg
@@ -0,0 +1,6 @@
+
+
+
+
+
+
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/dark/css/_dark.scss b/res/themes/dark/css/_dark.scss
index 08fe2e9f57..612b8e03bd 100644
--- a/res/themes/dark/css/_dark.scss
+++ b/res/themes/dark/css/_dark.scss
@@ -258,6 +258,7 @@ $composer-shadow-color: rgba(0, 0, 0, 0.28);
 // markdown overrides:
 .mx_EventTile_content .markdown-body pre:hover {
     border-color: #808080 !important; // inverted due to rules below
+    scrollbar-color: rgba(0, 0, 0, 0.2) transparent; // copied from light theme due to inversion below
 }
 .mx_EventTile_content .markdown-body {
     pre, code {
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..41a5941092 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';
@@ -426,6 +427,13 @@ export default class CallHandler {
             `our Party ID: ${call.ourPartyId}, hangup party: ${call.hangupParty}, ` +
             `hangup reason: ${call.hangupReason}`,
         );
+        if (!stats) {
+            logger.debug(
+                "Call statistics are undefined. The call has " +
+                "probably failed before a peerConn was established",
+            );
+            return;
+        }
         logger.debug("Local candidates:");
         for (const cand of stats.filter(item => item.type === 'local-candidate')) {
             const address = cand.address || cand.ip; // firefox uses 'address', chrome uses 'ip'
@@ -572,9 +580,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 +624,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 +635,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 +650,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/HostSignupAction.tsx b/src/components/structures/HostSignupAction.tsx
new file mode 100644
index 0000000000..39185acd05
--- /dev/null
+++ b/src/components/structures/HostSignupAction.tsx
@@ -0,0 +1,45 @@
+/*
+Copyright 2021 The Matrix.org Foundation C.I.C.
+
+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 {
+    IconizedContextMenuOption,
+    IconizedContextMenuOptionList,
+} from "../views/context_menus/IconizedContextMenu";
+import { _t } from "../../languageHandler";
+import { HostSignupStore } from "../../stores/HostSignupStore";
+
+interface IProps {}
+
+interface IState {}
+
+export default class HostSignupAction extends React.PureComponent {
+    private openDialog = async () => {
+        await HostSignupStore.instance.setHostSignupActive(true);
+    }
+
+    public render(): React.ReactNode {
+        return (
+            
+                
+            
+        );
+    }
+}
diff --git a/src/components/structures/LoggedInView.tsx b/src/components/structures/LoggedInView.tsx
index 0e52ff4c1c..c76cd7cee7 100644
--- a/src/components/structures/LoggedInView.tsx
+++ b/src/components/structures/LoggedInView.tsx
@@ -54,6 +54,7 @@ import { ToggleRightPanelPayload } from "../../dispatcher/payloads/ToggleRightPa
 import { IThreepidInvite } from "../../stores/ThreepidInviteStore";
 import Modal from "../../Modal";
 import { ICollapseConfig } from "../../resizer/distributors/collapse";
+import HostSignupContainer from '../views/host_signup/HostSignupContainer';
 
 // We need to fetch each pinned message individually (if we don't already have it)
 // so each pinned message may trigger a request. Limit the number per room for sanity.
@@ -648,6 +649,7 @@ class LoggedInView extends React.Component {
                 
                 
                 
+                
             
         );
     }
diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx
index accdd16b4e..5045e44182 100644
--- a/src/components/structures/MatrixChat.tsx
+++ b/src/components/structures/MatrixChat.tsx
@@ -1375,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();
@@ -1385,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',
             });
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 = { const prototypeCommunityName = CommunityPrototypeStore.instance.getSelectedCommunityName(); let topSection; - const signupLink = getHostingLink("user-context-menu"); + const hostSignupConfig: IHostSignupConfig = SdkConfig.get().hostSignup; if (MatrixClientPeg.get().isGuest()) { topSection = (
@@ -292,24 +293,19 @@ export default class UserMenu extends React.Component { })}
) - } else if (signupLink) { - topSection = ( -
- {_t( - "Upgrade to your own domain", {}, - { - a: sub => ( - {sub} - ), - }, - )} -
- ); + } else if (hostSignupConfig) { + if (hostSignupConfig && hostSignupConfig.url) { + // If hostSignup.domains is set to a non-empty array, only show + // dialog if the user is on the domain or a subdomain. + const hostSignupDomains = hostSignupConfig.domains || []; + const mxDomain = MatrixClientPeg.get().getDomain(); + const validDomains = hostSignupDomains.filter(d => (d === mxDomain || mxDomain.endsWith(`.${d}`))); + if (!hostSignupDomains || validDomains.length > 0) { + topSection =
+ +
; + } + } } let homeButton = null; diff --git a/src/components/views/dialogs/HostSignupDialog.tsx b/src/components/views/dialogs/HostSignupDialog.tsx new file mode 100644 index 0000000000..45a03b7cf0 --- /dev/null +++ b/src/components/views/dialogs/HostSignupDialog.tsx @@ -0,0 +1,291 @@ +/* +Copyright 2021 The Matrix.org Foundation C.I.C. + +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 AccessibleButton from "../elements/AccessibleButton"; +import Modal from "../../../Modal"; +import PersistedElement from "../elements/PersistedElement"; +import QuestionDialog from './QuestionDialog'; +import SdkConfig from "../../../SdkConfig"; +import classNames from "classnames"; +import { _t } from "../../../languageHandler"; +import { MatrixClientPeg } from "../../../MatrixClientPeg"; +import { HostSignupStore } from "../../../stores/HostSignupStore"; +import { OwnProfileStore } from "../../../stores/OwnProfileStore"; +import { + IHostSignupConfig, + IPostmessage, + IPostmessageResponseData, + PostmessageAction, +} from "./HostSignupDialogTypes"; + +const HOST_SIGNUP_KEY = "host_signup"; + +interface IProps {} + +interface IState { + completed: boolean; + error: string; + minimized: boolean; +} + +export default class HostSignupDialog extends React.PureComponent { + private iframeRef: React.RefObject = React.createRef(); + private readonly config: IHostSignupConfig; + + constructor(props: IProps) { + super(props); + + this.state = { + completed: false, + error: null, + minimized: false, + }; + + this.config = SdkConfig.get().hostSignup; + } + + private messageHandler = async (message: IPostmessage) => { + if (!this.config.url.startsWith(message.origin)) { + return; + } + switch (message.data.action) { + case PostmessageAction.HostSignupAccountDetailsRequest: + this.onAccountDetailsRequest(); + break; + case PostmessageAction.Maximize: + this.setState({ + minimized: false, + }); + break; + case PostmessageAction.Minimize: + this.setState({ + minimized: true, + }); + break; + case PostmessageAction.SetupComplete: + this.setState({ + completed: true, + }); + break; + case PostmessageAction.CloseDialog: + return this.closeDialog(); + } + } + + private maximizeDialog = () => { + this.setState({ + minimized: false, + }); + // Send this action to the iframe so it can act accordingly + this.sendMessage({ + action: PostmessageAction.Maximize, + }); + } + + private minimizeDialog = () => { + this.setState({ + minimized: true, + }); + // Send this action to the iframe so it can act accordingly + this.sendMessage({ + action: PostmessageAction.Minimize, + }); + } + + private closeDialog = async () => { + window.removeEventListener("message", this.messageHandler); + // Ensure we destroy the host signup persisted element + PersistedElement.destroyElement("host_signup"); + // Finally clear the flag in + return HostSignupStore.instance.setHostSignupActive(false); + } + + private onCloseClick = async () => { + if (this.state.completed) { + // We're done, close + return this.closeDialog(); + } else { + Modal.createDialog( + QuestionDialog, + { + title: _t("Confirm abort of host creation"), + description: _t( + "Are you sure you wish to abort creation of the host? The process cannot be continued.", + ), + button: _t("Abort"), + onFinished: result => { + if (result) { + return this.closeDialog(); + } + }, + }, + ); + } + } + + private sendMessage = (message: IPostmessageResponseData) => { + this.iframeRef.current.contentWindow.postMessage(message, this.config.url); + } + + private async sendAccountDetails() { + const openIdToken = await MatrixClientPeg.get().getOpenIdToken(); + if (!openIdToken || !openIdToken.access_token) { + console.warn("Failed to connect to homeserver for OpenID token.") + this.setState({ + completed: true, + error: _t("Failed to connect to your homeserver. Please close this dialog and try again."), + }); + return; + } + this.sendMessage({ + action: PostmessageAction.HostSignupAccountDetails, + account: { + accessToken: await MatrixClientPeg.get().getAccessToken(), + name: OwnProfileStore.instance.displayName, + openIdToken: openIdToken.access_token, + serverName: await MatrixClientPeg.get().getDomain(), + userLocalpart: await MatrixClientPeg.get().getUserIdLocalpart(), + termsAccepted: true, + }, + }); + } + + private onAccountDetailsDialogFinished = async (result) => { + if (result) { + return this.sendAccountDetails(); + } + return this.closeDialog(); + } + + private onAccountDetailsRequest = () => { + const textComponent = ( + <> +

+ {_t("Continuing temporarily allows the %(hostSignupBrand)s setup process to access your " + + "account to fetch verified email addresses. This data is not stored.", { + hostSignupBrand: this.config.brand, + })} +

+

+ {_t("Learn more in our , and .", + {}, + { + cookiePolicyLink: () => ( + + {_t("Cookie Policy")} + + ), + privacyPolicyLink: () => ( + + {_t("Privacy Policy")} + + ), + termsOfServiceLink: () => ( + + {_t("Terms of Service")} + + ), + }, + )} +

+ + ); + Modal.createDialog( + QuestionDialog, + { + title: _t("You should know"), + description: textComponent, + button: _t("Continue"), + onFinished: this.onAccountDetailsDialogFinished, + }, + ); + } + + public componentDidMount() { + window.addEventListener("message", this.messageHandler); + } + + public componentWillUnmount() { + if (HostSignupStore.instance.isHostSignupActive) { + // Run the close dialog actions if we're still active, otherwise good to go + return this.closeDialog(); + } + } + + public render(): React.ReactNode { + return ( +
+ +
+
+ {this.state.minimized && +
+
+ {_t("%(hostSignupBrand)s Setup", { + hostSignupBrand: this.config.brand, + })} +
+ +
+ } + {!this.state.minimized && +
+ + +
+ } + {this.state.error && +
+ {this.state.error} +
+ } + {!this.state.error && +