diff --git a/babel.config.js b/babel.config.js index d5a97d56ce..0a3a34a391 100644 --- a/babel.config.js +++ b/babel.config.js @@ -3,12 +3,15 @@ module.exports = { "presets": [ ["@babel/preset-env", { "targets": [ - "last 2 Chrome versions", "last 2 Firefox versions", "last 2 Safari versions" + "last 2 Chrome versions", + "last 2 Firefox versions", + "last 2 Safari versions", + "last 2 Edge versions", ], }], "@babel/preset-typescript", "@babel/preset-flow", - "@babel/preset-react" + "@babel/preset-react", ], "plugins": [ ["@babel/plugin-proposal-decorators", {legacy: true}], @@ -18,6 +21,6 @@ module.exports = { "@babel/plugin-proposal-object-rest-spread", "@babel/plugin-transform-flow-comments", "@babel/plugin-syntax-dynamic-import", - "@babel/plugin-transform-runtime" - ] + "@babel/plugin-transform-runtime", + ], }; diff --git a/res/css/_common.scss b/res/css/_common.scss index 6e9d252659..8ae1cc6641 100644 --- a/res/css/_common.scss +++ b/res/css/_common.scss @@ -606,6 +606,13 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus { } } +@define-mixin ProgressBarBgColour $colour { + background-color: $colour; + &::-webkit-progress-bar { + background-color: $colour; + } +} + @define-mixin ProgressBarBorderRadius $radius { border-radius: $radius; &::-moz-progress-bar { diff --git a/res/css/structures/_MainSplit.scss b/res/css/structures/_MainSplit.scss index ad1656efbb..8199121420 100644 --- a/res/css/structures/_MainSplit.scss +++ b/res/css/structures/_MainSplit.scss @@ -18,6 +18,7 @@ limitations under the License. display: flex; flex-direction: row; min-width: 0; + min-height: 0; height: 100%; } diff --git a/res/css/structures/_RoomView.scss b/res/css/structures/_RoomView.scss index 36bf96359b..26382b55e8 100644 --- a/res/css/structures/_RoomView.scss +++ b/res/css/structures/_RoomView.scss @@ -20,35 +20,54 @@ limitations under the License. flex-direction: column; } + +@keyframes mx_RoomView_fileDropTarget_animation { + from { + opacity: 0; + } + to { + opacity: 0.95; + } +} + .mx_RoomView_fileDropTarget { min-width: 0px; width: 100%; + height: 100%; + font-size: $font-18px; text-align: center; pointer-events: none; - padding-left: 12px; - padding-right: 12px; - margin-left: -12px; + background-color: $primary-bg-color; + opacity: 0.95; - border-top-left-radius: 10px; - border-top-right-radius: 10px; - - background-color: $droptarget-bg-color; - border: 2px #e1dddd solid; - border-bottom: none; position: absolute; - top: 52px; - bottom: 0px; z-index: 3000; + + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + + animation: mx_RoomView_fileDropTarget_animation; + animation-duration: 0.5s; } -.mx_RoomView_fileDropTargetLabel { - top: 50%; - width: 100%; - margin-top: -50px; - position: absolute; +@keyframes mx_RoomView_fileDropTarget_image_animation { + from { + width: 0px; + } + to { + width: 32px; + } +} + +.mx_RoomView_fileDropTarget_image { + animation: mx_RoomView_fileDropTarget_image_animation; + animation-duration: 0.5s; + margin-bottom: 16px; } .mx_RoomView_auxPanel { @@ -117,7 +136,6 @@ limitations under the License. } .mx_RoomView_body { - position: relative; //for .mx_RoomView_auxPanel_fullHeight display: flex; flex-direction: column; flex: 1; diff --git a/res/css/structures/_UploadBar.scss b/res/css/structures/_UploadBar.scss index d76c81668c..7c62516b47 100644 --- a/res/css/structures/_UploadBar.scss +++ b/res/css/structures/_UploadBar.scss @@ -1,5 +1,5 @@ /* -Copyright 2015, 2016 OpenMarket Ltd +Copyright 2015, 2016, 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. @@ -15,47 +15,45 @@ limitations under the License. */ .mx_UploadBar { + padding-left: 65px; // line up with the shield area in the composer position: relative; + + .mx_ProgressBar { + width: calc(100% - 40px); // cheating at a right margin + } } -.mx_UploadBar_uploadProgressOuter { - height: 5px; - margin-left: 63px; - margin-top: -1px; - padding-bottom: 5px; -} - -.mx_UploadBar_uploadProgressInner { - background-color: $accent-color; - height: 5px; -} - -.mx_UploadBar_uploadFilename { +.mx_UploadBar_filename { margin-top: 5px; - margin-left: 65px; - opacity: 0.5; - color: $primary-fg-color; -} - -.mx_UploadBar_uploadIcon { - float: left; - margin-top: 5px; - margin-left: 14px; -} - -.mx_UploadBar_uploadCancel { - float: right; - margin-top: 5px; - margin-right: 10px; + color: $muted-fg-color; position: relative; - opacity: 0.6; - cursor: pointer; - z-index: 1; + padding-left: 22px; // 18px for icon, 4px for padding + font-size: $font-15px; + vertical-align: middle; + + &::before { + content: ""; + height: 18px; + width: 18px; + position: absolute; + top: 0; + left: 0; + mask-repeat: no-repeat; + mask-position: center; + background-color: $muted-fg-color; + mask-image: url('$(res)/img/element-icons/upload.svg'); + } } -.mx_UploadBar_uploadBytes { - float: right; - margin-top: 5px; - margin-right: 30px; - color: $accent-color; +.mx_UploadBar_cancel { + position: absolute; + top: 0; + right: 0; + height: 16px; + width: 16px; + margin-right: 16px; // align over rightmost button in composer + mask-repeat: no-repeat; + mask-position: center; + background-color: $muted-fg-color; + mask-image: url('$(res)/img/icons-close.svg'); } diff --git a/res/css/views/dialogs/_HostSignupDialog.scss b/res/css/views/dialogs/_HostSignupDialog.scss index 1378ac9053..ac4bc41951 100644 --- a/res/css/views/dialogs/_HostSignupDialog.scss +++ b/res/css/views/dialogs/_HostSignupDialog.scss @@ -19,6 +19,11 @@ limitations under the License. max-width: 580px; height: 80vh; max-height: 600px; + // Ensure dialog borders are always white as the HostSignupDialog + // does not yet support dark mode or theming in general. + // In the future we might want to pass the theme to the called + // iframe, should some hosting provider have that need. + background-color: #ffffff; .mx_HostSignupDialog_info { text-align: center; diff --git a/res/css/views/elements/_ProgressBar.scss b/res/css/views/elements/_ProgressBar.scss index e49d85af04..770978e921 100644 --- a/res/css/views/elements/_ProgressBar.scss +++ b/res/css/views/elements/_ProgressBar.scss @@ -1,5 +1,5 @@ /* -Copyright 2020 The Matrix.org Foundation C.I.C. +Copyright 2020, 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. @@ -15,15 +15,15 @@ limitations under the License. */ progress.mx_ProgressBar { - height: 4px; + height: 6px; width: 60px; - border-radius: 10px; overflow: hidden; appearance: none; - border: 0; + border: none; - @mixin ProgressBarBorderRadius "10px"; - @mixin ProgressBarColour $accent-color; + @mixin ProgressBarBorderRadius "6px"; + @mixin ProgressBarColour $progressbar-fg-color; + @mixin ProgressBarBgColour $progressbar-bg-color; ::-webkit-progress-value { transition: width 1s; } diff --git a/res/css/views/messages/_MFileBody.scss b/res/css/views/messages/_MFileBody.scss index 6cbce68745..e219c0c5e4 100644 --- a/res/css/views/messages/_MFileBody.scss +++ b/res/css/views/messages/_MFileBody.scss @@ -45,3 +45,46 @@ limitations under the License. * big the content of the iframe is. */ height: 1.5em; } + +.mx_MFileBody_info { + background-color: $message-body-panel-bg-color; + border-radius: 4px; + width: 270px; + padding: 8px; + color: $message-body-panel-fg-color; + + .mx_MFileBody_info_icon { + background-color: $message-body-panel-icon-bg-color; + border-radius: 20px; + display: inline-block; + width: 32px; + height: 32px; + position: relative; + vertical-align: middle; + margin-right: 12px; + + &::before { + content: ''; + mask-repeat: no-repeat; + mask-position: center; + mask-size: cover; + mask-image: url('$(res)/img/element-icons/room/composer/attach.svg'); + background-color: $message-body-panel-fg-color; + width: 13px; + height: 15px; + + position: absolute; + top: 8px; + left: 9px; + } + } + + .mx_MFileBody_info_filename { + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + display: inline-block; + width: calc(100% - 32px - 12px); // 32px icon, 12px margin on the icon + vertical-align: middle; + } +} diff --git a/res/css/views/rooms/_AppsDrawer.scss b/res/css/views/rooms/_AppsDrawer.scss index 492ed95973..fd80836237 100644 --- a/res/css/views/rooms/_AppsDrawer.scss +++ b/res/css/views/rooms/_AppsDrawer.scss @@ -370,11 +370,6 @@ $MinWidth: 240px; display: none; } -/* Avoid apptile iframes capturing mouse event focus when resizing */ -.mx_AppsDrawer_resizing iframe { - pointer-events: none; -} - .mx_AppsDrawer_resizing .mx_AppTile_persistedWrapper { z-index: 1; } diff --git a/res/css/views/rooms/_AuxPanel.scss b/res/css/views/rooms/_AuxPanel.scss index 34ef5e01d4..17a6294bf0 100644 --- a/res/css/views/rooms/_AuxPanel.scss +++ b/res/css/views/rooms/_AuxPanel.scss @@ -17,7 +17,7 @@ limitations under the License. .m_RoomView_auxPanel_stateViews { padding: 5px; padding-left: 19px; - border-bottom: 1px solid #e5e5e5; + border-bottom: 1px solid $primary-hairline-color; } .m_RoomView_auxPanel_stateViews_span a { diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index 5841cf2853..028d9a7556 100644 --- a/res/css/views/rooms/_EventTile.scss +++ b/res/css/views/rooms/_EventTile.scss @@ -213,23 +213,36 @@ $left-gutter: 64px; color: $accent-fg-color; } -.mx_EventTile_encrypting { - color: $event-encrypting-color !important; -} - -.mx_EventTile_sending { - color: $event-sending-color; -} - -.mx_EventTile_sending .mx_UserPill, -.mx_EventTile_sending .mx_RoomPill { - opacity: 0.5; -} - .mx_EventTile_notSent { color: $event-notsent-color; } +.mx_EventTile_receiptSent, +.mx_EventTile_receiptSending { + // We don't use `position: relative` on the element because then it won't line + // up with the other read receipts + + &::before { + background-color: $tertiary-fg-color; + mask-repeat: no-repeat; + mask-position: center; + mask-size: 14px; + width: 14px; + height: 14px; + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + } +} +.mx_EventTile_receiptSent::before { + mask-image: url('$(res)/img/element-icons/circle-sent.svg'); +} +.mx_EventTile_receiptSending::before { + mask-image: url('$(res)/img/element-icons/circle-sending.svg'); +} + .mx_EventTile_contextual { opacity: 0.4; } diff --git a/res/css/views/rooms/_GroupLayout.scss b/res/css/views/rooms/_GroupLayout.scss index 903fabc8fd..818509785b 100644 --- a/res/css/views/rooms/_GroupLayout.scss +++ b/res/css/views/rooms/_GroupLayout.scss @@ -21,7 +21,7 @@ $left-gutter: 64px; .mx_EventTile { > .mx_SenderProfile { line-height: $font-20px; - padding-left: $left-gutter; + margin-left: $left-gutter; } > .mx_EventTile_line { diff --git a/res/css/views/rooms/_IRCLayout.scss b/res/css/views/rooms/_IRCLayout.scss index 792c2f1f58..21baa795e6 100644 --- a/res/css/views/rooms/_IRCLayout.scss +++ b/res/css/views/rooms/_IRCLayout.scss @@ -181,8 +181,7 @@ $irc-line-height: $font-18px; > span { display: flex; - > .mx_SenderProfile_name, - > .mx_SenderProfile_aux { + > .mx_SenderProfile_name { overflow: hidden; text-overflow: ellipsis; min-width: var(--name-width); @@ -212,8 +211,7 @@ $irc-line-height: $font-18px; background: transparent; > span { - > .mx_SenderProfile_name, - > .mx_SenderProfile_aux { + > .mx_SenderProfile_name { min-width: inherit; } } diff --git a/res/img/element-icons/circle-sending.svg b/res/img/element-icons/circle-sending.svg new file mode 100644 index 0000000000..2d15a0f716 --- /dev/null +++ b/res/img/element-icons/circle-sending.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/img/element-icons/circle-sent.svg b/res/img/element-icons/circle-sent.svg new file mode 100644 index 0000000000..04a00ceff7 --- /dev/null +++ b/res/img/element-icons/circle-sent.svg @@ -0,0 +1,4 @@ + + + + diff --git a/res/img/element-icons/upload.svg b/res/img/element-icons/upload.svg new file mode 100644 index 0000000000..71ad7ba1cf --- /dev/null +++ b/res/img/element-icons/upload.svg @@ -0,0 +1,4 @@ + + + + diff --git a/res/img/upload-big.svg b/res/img/upload-big.svg index 6099c2e976..9a6a265fdb 100644 --- a/res/img/upload-big.svg +++ b/res/img/upload-big.svg @@ -1,19 +1,3 @@ - - - - icons_upload_drop - Created with bin/sketchtool. - - - - - - - - - - - - - + + diff --git a/res/themes/dark/css/_dark.scss b/res/themes/dark/css/_dark.scss index 0de5e69782..7a751ad9c1 100644 --- a/res/themes/dark/css/_dark.scss +++ b/res/themes/dark/css/_dark.scss @@ -138,9 +138,6 @@ $panel-divider-color: transparent; $widget-menu-bar-bg-color: $header-panel-bg-color; $widget-body-bg-color: rgba(141, 151, 165, 0.2); -// event tile lifecycle -$event-sending-color: $text-secondary-color; - // event redaction $event-redacted-fg-color: #606060; $event-redacted-border-color: #000000; @@ -173,6 +170,9 @@ $button-link-bg-color: transparent; // Toggle switch $togglesw-off-color: $room-highlight-color; +$progressbar-fg-color: $accent-color; +$progressbar-bg-color: #21262c; + $visual-bell-bg-color: #800; $room-warning-bg-color: $header-panel-bg-color; @@ -203,6 +203,10 @@ $breadcrumb-placeholder-bg-color: #272c35; $user-tile-hover-bg-color: $header-panel-bg-color; +$message-body-panel-bg-color: #21262c82; +$message-body-panel-icon-bg-color: #8e99a4; +$message-body-panel-fg-color: $primary-fg-color; + // Appearance tab colors $appearance-tab-border-color: $room-highlight-color; diff --git a/res/themes/legacy-dark/css/_legacy-dark.scss b/res/themes/legacy-dark/css/_legacy-dark.scss index 8c5f20178b..764b8f302a 100644 --- a/res/themes/legacy-dark/css/_legacy-dark.scss +++ b/res/themes/legacy-dark/css/_legacy-dark.scss @@ -133,9 +133,6 @@ $panel-divider-color: $header-panel-border-color; $widget-menu-bar-bg-color: $header-panel-bg-color; $widget-body-bg-color: #1A1D23; -// event tile lifecycle -$event-sending-color: $text-secondary-color; - // event redaction $event-redacted-fg-color: #606060; $event-redacted-border-color: #000000; @@ -168,6 +165,9 @@ $button-link-bg-color: transparent; // Toggle switch $togglesw-off-color: $room-highlight-color; +$progressbar-fg-color: $accent-color; +$progressbar-bg-color: #21262c; + $visual-bell-bg-color: #800; $room-warning-bg-color: $header-panel-bg-color; @@ -198,6 +198,10 @@ $breadcrumb-placeholder-bg-color: #272c35; $user-tile-hover-bg-color: $header-panel-bg-color; +$message-body-panel-bg-color: #21262c82; +$message-body-panel-icon-bg-color: #8e99a4; +$message-body-panel-fg-color: $primary-fg-color; + // Appearance tab colors $appearance-tab-border-color: $room-highlight-color; diff --git a/res/themes/legacy-light/css/_legacy-light.scss b/res/themes/legacy-light/css/_legacy-light.scss index 3ba10a68ea..9ad154dd93 100644 --- a/res/themes/legacy-light/css/_legacy-light.scss +++ b/res/themes/legacy-light/css/_legacy-light.scss @@ -223,8 +223,6 @@ $widget-body-bg-color: #fff; $yellow-background: #fff8e3; // event tile lifecycle -$event-encrypting-color: #abddbc; -$event-sending-color: #ddd; $event-notsent-color: #f44; $event-highlight-fg-color: $warning-color; @@ -282,7 +280,8 @@ $togglesw-ball-color: #fff; $slider-selection-color: $accent-color; $slider-background-color: #c1c9d6; -$progressbar-color: #000; +$progressbar-fg-color: $accent-color; +$progressbar-bg-color: rgba(141, 151, 165, 0.2); $room-warning-bg-color: $yellow-background; @@ -322,6 +321,10 @@ $breadcrumb-placeholder-bg-color: #e8eef5; $user-tile-hover-bg-color: $header-panel-bg-color; +$message-body-panel-bg-color: #e3e8f082; +$message-body-panel-icon-bg-color: #ffffff; +$message-body-panel-fg-color: $muted-fg-color; + // FontSlider colors $appearance-tab-border-color: $input-darker-bg-color; diff --git a/res/themes/light/css/_light.scss b/res/themes/light/css/_light.scss index 76bf2ddc21..25fbd0201b 100644 --- a/res/themes/light/css/_light.scss +++ b/res/themes/light/css/_light.scss @@ -67,9 +67,6 @@ $groupFilterPanel-bg-color: rgba(232, 232, 232, 0.77); // used by RoomDirectory permissions $plinth-bg-color: $secondary-accent-color; -// used by RoomDropTarget -$droptarget-bg-color: rgba(255,255,255,0.5); - // used by AddressSelector $selected-color: $secondary-accent-color; @@ -223,8 +220,6 @@ $widget-body-bg-color: #FFF; $yellow-background: #fff8e3; // event tile lifecycle -$event-encrypting-color: #abddbc; -$event-sending-color: #ddd; $event-notsent-color: #f44; $event-highlight-fg-color: $warning-color; @@ -282,7 +277,8 @@ $togglesw-ball-color: #fff; $slider-selection-color: $accent-color; $slider-background-color: #c1c9d6; -$progressbar-color: #000; +$progressbar-fg-color: $accent-color; +$progressbar-bg-color: rgba(141, 151, 165, 0.2); $room-warning-bg-color: $yellow-background; @@ -323,6 +319,10 @@ $breadcrumb-placeholder-bg-color: #e8eef5; $user-tile-hover-bg-color: $header-panel-bg-color; +$message-body-panel-bg-color: #e3e8f082; +$message-body-panel-icon-bg-color: #ffffff; +$message-body-panel-fg-color: $muted-fg-color; + // FontSlider colors $appearance-tab-border-color: $input-darker-bg-color; diff --git a/src/CallHandler.tsx b/src/CallHandler.tsx index 42a38c7a54..d2564e637b 100644 --- a/src/CallHandler.tsx +++ b/src/CallHandler.tsx @@ -630,7 +630,7 @@ export default class CallHandler { logger.debug("Mapped real room " + roomId + " to room ID " + mappedRoomId); const timeUntilTurnCresExpire = MatrixClientPeg.get().getTurnServersExpiry() - Date.now(); - console.log("Current turn creds expire in " + timeUntilTurnCresExpire + " seconds"); + console.log("Current turn creds expire in " + timeUntilTurnCresExpire + " ms"); const call = createNewMatrixCall(MatrixClientPeg.get(), mappedRoomId); this.calls.set(roomId, call); @@ -706,6 +706,14 @@ export default class CallHandler { return; } + if (this.getCallForRoom(room.roomId)) { + Modal.createTrackedDialog('Call Handler', 'Existing Call with user', ErrorDialog, { + title: _t('Already in call'), + description: _t("You're already in a call with this person."), + }); + return; + } + const members = room.getJoinedMembers(); if (members.length <= 1) { Modal.createTrackedDialog('Call Handler', 'Cannot place call with self', ErrorDialog, { diff --git a/src/ContentMessages.tsx b/src/ContentMessages.tsx index bec36d49f6..95b45cce4a 100644 --- a/src/ContentMessages.tsx +++ b/src/ContentMessages.tsx @@ -32,6 +32,14 @@ import Spinner from "./components/views/elements/Spinner"; import "blueimp-canvas-to-blob"; import { Action } from "./dispatcher/actions"; import CountlyAnalytics from "./CountlyAnalytics"; +import { + UploadCanceledPayload, + UploadErrorPayload, + UploadFinishedPayload, + UploadProgressPayload, + UploadStartedPayload, +} from "./dispatcher/payloads/UploadPayload"; +import {IUpload} from "./models/IUpload"; const MAX_WIDTH = 800; const MAX_HEIGHT = 600; @@ -44,15 +52,6 @@ export class UploadCanceledError extends Error {} type ThumbnailableElement = HTMLImageElement | HTMLVideoElement; -interface IUpload { - fileName: string; - roomId: string; - total: number; - loaded: number; - promise: Promise; - canceled?: boolean; -} - interface IMediaConfig { "m.upload.size"?: number; } @@ -478,7 +477,7 @@ export default class ContentMessages { if (upload) { upload.canceled = true; MatrixClientPeg.get().cancelUpload(upload.promise); - dis.dispatch({action: 'upload_canceled', upload}); + dis.dispatch({action: Action.UploadCanceled, upload}); } } @@ -539,7 +538,7 @@ export default class ContentMessages { promise: prom, }; this.inprogress.push(upload); - dis.dispatch({action: 'upload_started'}); + dis.dispatch({action: Action.UploadStarted, upload}); // Focus the composer view dis.fire(Action.FocusComposer); @@ -547,7 +546,7 @@ export default class ContentMessages { function onProgress(ev) { upload.total = ev.total; upload.loaded = ev.loaded; - dis.dispatch({action: 'upload_progress', upload: upload}); + dis.dispatch({action: Action.UploadProgress, upload}); } let error; @@ -601,9 +600,9 @@ export default class ContentMessages { if (error && error.http_status === 413) { this.mediaConfig = null; } - dis.dispatch({action: 'upload_failed', upload, error}); + dis.dispatch({action: Action.UploadFailed, upload, error}); } else { - dis.dispatch({action: 'upload_finished', upload}); + dis.dispatch({action: Action.UploadFinished, upload}); dis.dispatch({action: 'message_sent'}); } }); diff --git a/src/Livestream.ts b/src/Livestream.ts new file mode 100644 index 0000000000..2389132762 --- /dev/null +++ b/src/Livestream.ts @@ -0,0 +1,55 @@ +/* +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 { ClientWidgetApi } from "matrix-widget-api"; +import { MatrixClientPeg } from "./MatrixClientPeg"; +import SdkConfig from "./SdkConfig"; +import { ElementWidgetActions } from "./stores/widgets/ElementWidgetActions"; + +export function getConfigLivestreamUrl() { + return SdkConfig.get()["audioStreamUrl"]; +} + +// Dummy rtmp URL used to signal that we want a special audio-only stream +const AUDIOSTREAM_DUMMY_URL = 'rtmp://audiostream.dummy/'; + +async function createLiveStream(roomId: string) { + const openIdToken = await MatrixClientPeg.get().getOpenIdToken(); + + const url = getConfigLivestreamUrl() + "/createStream"; + + const response = await window.fetch(url, { + method: 'POST', + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + room_id: roomId, + openid_token: openIdToken, + }), + }); + + const respBody = await response.json(); + return respBody['stream_id']; +} + +export async function startJitsiAudioLivestream(widgetMessaging: ClientWidgetApi, roomId: string) { + const streamId = await createLiveStream(roomId); + + await widgetMessaging.transport.send(ElementWidgetActions.StartLiveStream, { + rtmpStreamKey: AUDIOSTREAM_DUMMY_URL + streamId, + }); +} diff --git a/src/Presence.ts b/src/Presence.ts index 660bb0ac94..eb56c5714e 100644 --- a/src/Presence.ts +++ b/src/Presence.ts @@ -99,9 +99,9 @@ class Presence { try { await MatrixClientPeg.get().setPresence(this.state); - console.info("Presence: %s", newState); + console.info("Presence:", newState); } catch (err) { - console.error("Failed to set presence: %s", err); + console.error("Failed to set presence:", err); this.state = oldState; } } diff --git a/src/SlashCommands.tsx b/src/SlashCommands.tsx index 6b5f261374..aedcf7af8c 100644 --- a/src/SlashCommands.tsx +++ b/src/SlashCommands.tsx @@ -441,15 +441,14 @@ export const Commands = [ }), new Command({ command: 'invite', - args: '', + args: ' []', description: _td('Invites user with given id to current room'), runFn: function(roomId, args) { if (args) { - const matches = args.match(/^(\S+)$/); - if (matches) { + const [address, reason] = args.split(/\s+(.+)/); + if (address) { // We use a MultiInviter to re-use the invite logic, even though // we're only inviting one user. - const address = matches[1]; // If we need an identity server but don't have one, things // get a bit more complex here, but we try to show something // meaningful. @@ -490,7 +489,7 @@ export const Commands = [ } const inviter = new MultiInviter(roomId); return success(prom.then(() => { - return inviter.invite([address]); + return inviter.invite([address], reason); }).then(() => { if (inviter.getCompletionState(address) !== "invited") { throw new Error(inviter.getErrorText(address)); diff --git a/src/components/structures/LoggedInView.tsx b/src/components/structures/LoggedInView.tsx index 4e768bd9e5..3e6d56fd54 100644 --- a/src/components/structures/LoggedInView.tsx +++ b/src/components/structures/LoggedInView.tsx @@ -96,8 +96,10 @@ interface IProps { } interface IUsageLimit { + // "hs_disabled" is NOT a specced string, but is used in Synapse + // This is tracked over at https://github.com/matrix-org/synapse/issues/9237 // eslint-disable-next-line camelcase - limit_type: "monthly_active_user" | string; + limit_type: "monthly_active_user" | "hs_disabled" | string; // eslint-disable-next-line camelcase admin_contact?: string; } @@ -105,6 +107,8 @@ interface IUsageLimit { interface IState { syncErrorData?: { error: { + // This is not specced, but used in Synapse. See + // https://github.com/matrix-org/synapse/issues/9237#issuecomment-768238922 data: IUsageLimit; errcode: string; }; diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index 161227a139..9deda54bee 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -595,6 +595,19 @@ export default class MessagePanel extends React.Component { const readReceipts = this._readReceiptsByEvent[eventId]; + let isLastSuccessful = false; + const isSentState = s => !s || s === 'sent'; + const isSent = isSentState(mxEv.getAssociatedStatus()); + if (!nextEvent && isSent) { + isLastSuccessful = true; + } else if (nextEvent && isSent && !isSentState(nextEvent.getAssociatedStatus())) { + isLastSuccessful = true; + } + + // We only want to consider "last successful" if the event is sent by us, otherwise of course + // it's successful: we received it. + isLastSuccessful = isLastSuccessful && mxEv.getSender() === MatrixClientPeg.get().getUserId(); + // use txnId as key if available so that we don't remount during sending ret.push( contact your service administrator to continue using the service.", ), + 'hs_disabled': _td( + "Your message wasn't sent because this homeserver has been blocked by it's administrator. " + + "Please contact your service administrator to continue using the service.", + ), '': _td( "Your message wasn't sent because this homeserver has exceeded a resource limit. " + "Please contact your service administrator to continue using the service.", diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index 1961779d0e..90f6daf6cb 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -192,6 +192,7 @@ export interface IState { rejecting?: boolean; rejectError?: Error; hasPinnedWidgets?: boolean; + dragCounter: number; } export default class RoomView extends React.Component { @@ -242,6 +243,7 @@ export default class RoomView extends React.Component { canReply: false, layout: SettingsStore.getValue("layout"), matrixClientIsReady: this.context && this.context.isInitialSyncComplete(), + dragCounter: 0, }; this.dispatcherRef = dis.register(this.onAction); @@ -535,8 +537,8 @@ export default class RoomView extends React.Component { if (!roomView.ondrop) { roomView.addEventListener('drop', this.onDrop); roomView.addEventListener('dragover', this.onDragOver); - roomView.addEventListener('dragleave', this.onDragLeaveOrEnd); - roomView.addEventListener('dragend', this.onDragLeaveOrEnd); + roomView.addEventListener('dragenter', this.onDragEnter); + roomView.addEventListener('dragleave', this.onDragLeave); } } @@ -580,8 +582,8 @@ export default class RoomView extends React.Component { const roomView = this.roomView.current; roomView.removeEventListener('drop', this.onDrop); roomView.removeEventListener('dragover', this.onDragOver); - roomView.removeEventListener('dragleave', this.onDragLeaveOrEnd); - roomView.removeEventListener('dragend', this.onDragLeaveOrEnd); + roomView.removeEventListener('dragenter', this.onDragEnter); + roomView.removeEventListener('dragleave', this.onDragLeave); } dis.unregister(this.dispatcherRef); if (this.context) { @@ -709,9 +711,9 @@ export default class RoomView extends React.Component { [payload.file], this.state.room.roomId, this.context); break; case 'notifier_enabled': - case 'upload_started': - case 'upload_finished': - case 'upload_canceled': + case Action.UploadStarted: + case Action.UploadFinished: + case Action.UploadCanceled: this.forceUpdate(); break; case 'call_state': { @@ -1141,6 +1143,31 @@ export default class RoomView extends React.Component { this.updateTopUnreadMessagesBar(); }; + private onDragEnter = ev => { + ev.stopPropagation(); + ev.preventDefault(); + + this.setState({ + dragCounter: this.state.dragCounter + 1, + draggingFile: true, + }); + }; + + private onDragLeave = ev => { + ev.stopPropagation(); + ev.preventDefault(); + + this.setState({ + dragCounter: this.state.dragCounter - 1, + }); + + if (this.state.dragCounter === 0) { + this.setState({ + draggingFile: false, + }); + } + }; + private onDragOver = ev => { ev.stopPropagation(); ev.preventDefault(); @@ -1148,7 +1175,6 @@ export default class RoomView extends React.Component { ev.dataTransfer.dropEffect = 'none'; if (ev.dataTransfer.types.includes("Files") || ev.dataTransfer.types.includes("application/x-moz-file")) { - this.setState({ draggingFile: true }); ev.dataTransfer.dropEffect = 'copy'; } }; @@ -1159,14 +1185,12 @@ export default class RoomView extends React.Component { ContentMessages.sharedInstance().sendContentListToRoom( ev.dataTransfer.files, this.state.room.roomId, this.context, ); - this.setState({ draggingFile: false }); dis.fire(Action.FocusComposer); - }; - private onDragLeaveOrEnd = ev => { - ev.stopPropagation(); - ev.preventDefault(); - this.setState({ draggingFile: false }); + this.setState({ + draggingFile: false, + dragCounter: this.state.dragCounter - 1, + }); }; private injectSticker(url, info, text) { @@ -1768,6 +1792,19 @@ export default class RoomView extends React.Component { } } + let fileDropTarget = null; + if (this.state.draggingFile) { + fileDropTarget = ( + + + { _t("Drop file here to upload") } + + ); + } + // We have successfully loaded this room, and are not previewing. // Display the "normal" room view. @@ -1891,7 +1928,6 @@ export default class RoomView extends React.Component { room={this.state.room} fullHeight={false} userId={this.context.credentials.userId} - draggingFile={this.state.draggingFile} maxHeight={this.state.auxPanelMaxHeight} showApps={this.state.showApps} onResize={this.onResize} @@ -2060,6 +2096,7 @@ export default class RoomView extends React.Component { {auxPanel} + {fileDropTarget} {topUnreadMessagesBar} {jumpToBottom} {messagePanel} diff --git a/src/components/structures/UploadBar.js b/src/components/structures/UploadBar.js deleted file mode 100644 index 16cc4cb987..0000000000 --- a/src/components/structures/UploadBar.js +++ /dev/null @@ -1,109 +0,0 @@ -/* -Copyright 2015, 2016 OpenMarket Ltd -Copyright 2019 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 PropTypes from 'prop-types'; -import ContentMessages from '../../ContentMessages'; -import dis from "../../dispatcher/dispatcher"; -import filesize from "filesize"; -import { _t } from '../../languageHandler'; - -export default class UploadBar extends React.Component { - static propTypes = { - room: PropTypes.object, - }; - - componentDidMount() { - this.dispatcherRef = dis.register(this.onAction); - this.mounted = true; - } - - componentWillUnmount() { - this.mounted = false; - dis.unregister(this.dispatcherRef); - } - - onAction = payload => { - switch (payload.action) { - case 'upload_progress': - case 'upload_finished': - case 'upload_canceled': - case 'upload_failed': - if (this.mounted) this.forceUpdate(); - break; - } - }; - - render() { - const uploads = ContentMessages.sharedInstance().getCurrentUploads(); - - // for testing UI... - also fix up the ContentMessages.getCurrentUploads().length - // check in RoomView - // - // uploads = [{ - // roomId: this.props.room.roomId, - // loaded: 123493, - // total: 347534, - // fileName: "testing_fooble.jpg", - // }]; - - if (uploads.length == 0) { - return ; - } - - let upload; - for (let i = 0; i < uploads.length; ++i) { - if (uploads[i].roomId == this.props.room.roomId) { - upload = uploads[i]; - break; - } - } - if (!upload) { - return ; - } - - const innerProgressStyle = { - width: ((upload.loaded / (upload.total || 1)) * 100) + '%', - }; - let uploadedSize = filesize(upload.loaded); - const totalSize = filesize(upload.total); - if (uploadedSize.replace(/^.* /, '') === totalSize.replace(/^.* /, '')) { - uploadedSize = uploadedSize.replace(/ .*/, ''); - } - - // MUST use var name 'count' for pluralization to kick in - const uploadText = _t( - "Uploading %(filename)s and %(count)s others", {filename: upload.fileName, count: (uploads.length - 1)}, - ); - - return ( - - - - - - - - { uploadedSize } / { totalSize } - - { uploadText } - - ); - } -} diff --git a/src/components/structures/UploadBar.tsx b/src/components/structures/UploadBar.tsx new file mode 100644 index 0000000000..b9d157ee00 --- /dev/null +++ b/src/components/structures/UploadBar.tsx @@ -0,0 +1,100 @@ +/* +Copyright 2015, 2016, 2019, 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 { Room } from "matrix-js-sdk/src/models/room"; +import ContentMessages from '../../ContentMessages'; +import dis from "../../dispatcher/dispatcher"; +import filesize from "filesize"; +import { _t } from '../../languageHandler'; +import { ActionPayload } from "../../dispatcher/payloads"; +import { Action } from "../../dispatcher/actions"; +import ProgressBar from "../views/elements/ProgressBar"; +import AccessibleButton from "../views/elements/AccessibleButton"; +import { IUpload } from "../../models/IUpload"; + +interface IProps { + room: Room; +} + +interface IState { + currentUpload?: IUpload; + uploadsHere: IUpload[]; +} + +export default class UploadBar extends React.Component { + private dispatcherRef: string; + private mounted: boolean; + + constructor(props) { + super(props); + this.state = {uploadsHere: []}; + } + + componentDidMount() { + this.dispatcherRef = dis.register(this.onAction); + this.mounted = true; + } + + componentWillUnmount() { + this.mounted = false; + dis.unregister(this.dispatcherRef); + } + + private onAction = (payload: ActionPayload) => { + switch (payload.action) { + case Action.UploadStarted: + case Action.UploadProgress: + case Action.UploadFinished: + case Action.UploadCanceled: + case Action.UploadFailed: { + if (!this.mounted) return; + const uploads = ContentMessages.sharedInstance().getCurrentUploads(); + const uploadsHere = uploads.filter(u => u.roomId === this.props.room.roomId); + this.setState({currentUpload: uploadsHere[0], uploadsHere}); + break; + } + } + }; + + private onCancelClick = (ev) => { + ev.preventDefault(); + ContentMessages.sharedInstance().cancelUpload(this.state.currentUpload.promise); + }; + + render() { + if (!this.state.currentUpload) { + return null; + } + + // MUST use var name 'count' for pluralization to kick in + const uploadText = _t( + "Uploading %(filename)s and %(count)s others", { + filename: this.state.currentUpload.fileName, + count: this.state.uploadsHere.length - 1, + }, + ); + + const uploadSize = filesize(this.state.currentUpload.total); + return ( + + {uploadText} ({uploadSize}) + + + + ); + } +} diff --git a/src/components/structures/auth/Login.tsx b/src/components/structures/auth/Login.tsx index a217f1b4d9..96fc39a437 100644 --- a/src/components/structures/auth/Login.tsx +++ b/src/components/structures/auth/Login.tsx @@ -218,6 +218,9 @@ export default class LoginComponent extends React.PureComponent 'monthly_active_user': _td( "This homeserver has hit its Monthly Active User limit.", ), + 'hs_blocked': _td( + "This homeserver has been blocked by it's administrator.", + ), '': _td( "This homeserver has exceeded one of its resource limits.", ), diff --git a/src/components/structures/auth/Registration.tsx b/src/components/structures/auth/Registration.tsx index 095f3d3433..f9d338902c 100644 --- a/src/components/structures/auth/Registration.tsx +++ b/src/components/structures/auth/Registration.tsx @@ -276,6 +276,7 @@ export default class Registration extends React.Component { response.data.admin_contact, { 'monthly_active_user': _td("This homeserver has hit its Monthly Active User limit."), + 'hs_blocked': _td("This homeserver has been blocked by it's administrator."), '': _td("This homeserver has exceeded one of its resource limits."), }, ); diff --git a/src/components/views/context_menus/WidgetContextMenu.tsx b/src/components/views/context_menus/WidgetContextMenu.tsx index c1af86eae6..623fe04f2f 100644 --- a/src/components/views/context_menus/WidgetContextMenu.tsx +++ b/src/components/views/context_menus/WidgetContextMenu.tsx @@ -28,9 +28,11 @@ import dis from "../../../dispatcher/dispatcher"; import SettingsStore from "../../../settings/SettingsStore"; import Modal from "../../../Modal"; import QuestionDialog from "../dialogs/QuestionDialog"; +import ErrorDialog from "../dialogs/ErrorDialog"; import {WidgetType} from "../../../widgets/WidgetType"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; import { Container, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore"; +import { getConfigLivestreamUrl, startJitsiAudioLivestream } from "../../../Livestream"; interface IProps extends React.ComponentProps { app: IApp; @@ -54,6 +56,27 @@ const WidgetContextMenu: React.FC = ({ const widgetMessaging = WidgetMessagingStore.instance.getMessagingForId(app.id); const canModify = userWidget || WidgetUtils.canUserModifyWidgets(roomId); + let streamAudioStreamButton; + if (getConfigLivestreamUrl() && WidgetType.JITSI.matches(app.type)) { + const onStreamAudioClick = async () => { + try { + await startJitsiAudioLivestream(widgetMessaging, roomId); + } catch (err) { + console.error("Failed to start livestream", err); + // XXX: won't i18n well, but looks like widget api only support 'message'? + const message = err.message || _t("Unable to start audio streaming."); + Modal.createTrackedDialog('WidgetContext Menu', 'Livestream failed', ErrorDialog, { + title: _t('Failed to start livestream'), + description: message, + }); + } + onFinished(); + }; + streamAudioStreamButton = ; + } + let unpinButton; if (showUnpin) { const onUnpinClick = () => { @@ -163,6 +186,7 @@ const WidgetContextMenu: React.FC = ({ return + { streamAudioStreamButton } { editButton } { revokeButton } { deleteButton } @@ -175,4 +199,3 @@ const WidgetContextMenu: React.FC = ({ }; export default WidgetContextMenu; - diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js index 213351889f..2a72621ccc 100644 --- a/src/components/views/elements/AppTile.js +++ b/src/components/views/elements/AppTile.js @@ -325,9 +325,13 @@ export default class AppTile extends React.Component { // Additional iframe feature pemissions // (see - https://sites.google.com/a/chromium.org/dev/Home/chromium-security/deprecating-permissions-in-cross-origin-iframes and https://wicg.github.io/feature-policy/) - const iframeFeatures = "microphone; camera; encrypted-media; autoplay; display-capture;"; + const iframeFeatures = "microphone; camera; encrypted-media; autoplay; display-capture; clipboard-write;"; const appTileBodyClass = 'mx_AppTileBody' + (this.props.miniMode ? '_mini ' : ' '); + const appTileBodyStyles = {}; + if (this.props.pointerEvents) { + appTileBodyStyles['pointer-events'] = this.props.pointerEvents; + } const loadingElement = ( @@ -338,7 +342,7 @@ export default class AppTile extends React.Component { // only possible for room widgets, can assert this.props.room here const isEncrypted = MatrixClientPeg.get().isRoomEncrypted(this.props.room.roomId); appTileBody = ( - + + { loadingElement } ); } else { if (this.isMixedContent()) { appTileBody = ( - + ); } else { appTileBody = ( - + { this.state.loading && loadingElement } { + this.setState({ + hover: true, + }); + }; + + onMouseLeave = () => { + this.setState({ + hover: false, + }); + }; + doProfileLookup(userId, member) { MatrixClientPeg.get().getProfileInfo(userId).then((resp) => { if (this._unmounted) { @@ -226,7 +241,7 @@ class Pill extends React.Component { case Pill.TYPE_ROOM_MENTION: { const room = this.state.room; if (room) { - linkText = resource; + linkText = room.name || resource; if (this.props.shouldShowPillAvatar) { avatar = ; } @@ -256,15 +271,36 @@ class Pill extends React.Component { }); if (this.state.pillType) { + const {yOffset} = this.props; + + let tip; + if (this.state.hover && resource) { + tip = ; + } + return { this.props.inMessage ? - + { avatar } { linkText } + { tip } : - + { avatar } { linkText } + { tip } } ; } else { diff --git a/src/components/views/messages/EditHistoryMessage.js b/src/components/views/messages/EditHistoryMessage.js index 3bd9dfbd21..90eecc0ec2 100644 --- a/src/components/views/messages/EditHistoryMessage.js +++ b/src/components/views/messages/EditHistoryMessage.js @@ -156,6 +156,7 @@ export default class EditHistoryMessage extends React.PureComponent { const isSending = (['sending', 'queued', 'encrypting'].indexOf(this.state.sendStatus) !== -1); const classes = classNames({ "mx_EventTile": true, + // Note: we keep the `sending` state class for tests, not for our styles "mx_EventTile_sending": isSending, "mx_EventTile_notSent": this.state.sendStatus === 'not_sent', }); diff --git a/src/components/views/messages/MAudioBody.js b/src/components/views/messages/MAudioBody.js index 37f85a108f..587dee4513 100644 --- a/src/components/views/messages/MAudioBody.js +++ b/src/components/views/messages/MAudioBody.js @@ -105,7 +105,7 @@ export default class MAudioBody extends React.Component { return ( - + ); } diff --git a/src/components/views/messages/MFileBody.js b/src/components/views/messages/MFileBody.js index 770cd4fff3..676f0b7986 100644 --- a/src/components/views/messages/MFileBody.js +++ b/src/components/views/messages/MFileBody.js @@ -126,6 +126,12 @@ export default class MFileBody extends React.Component { onHeightChanged: PropTypes.func, /* the shape of the tile, used */ tileShape: PropTypes.string, + /* whether or not to show the default placeholder for the file. Defaults to true. */ + showGenericPlaceholder: PropTypes.bool, + }; + + static defaultProps = { + showGenericPlaceholder: true, }; constructor(props) { @@ -145,9 +151,10 @@ export default class MFileBody extends React.Component { * link text. * * @param {Object} content The "content" key of the matrix event. + * @param {boolean} withSize Whether to include size information. Default true. * @return {string} the human readable link text for the attachment. */ - presentableTextForFile(content) { + presentableTextForFile(content, withSize = true) { let linkText = _t("Attachment"); if (content.body && content.body.length > 0) { // The content body should be the name of the file including a @@ -155,7 +162,7 @@ export default class MFileBody extends React.Component { linkText = content.body; } - if (content.info && content.info.size) { + if (content.info && content.info.size && withSize) { // If we know the size of the file then add it as human readable // string to the end of the link text so that the user knows how // big a file they are downloading. @@ -218,6 +225,16 @@ export default class MFileBody extends React.Component { const fileSize = content.info ? content.info.size : null; const fileType = content.info ? content.info.mimetype : "application/octet-stream"; + let placeholder = null; + if (this.props.showGenericPlaceholder) { + placeholder = ( + + + {this.presentableTextForFile(content, false)} + + ); + } + if (isEncrypted) { if (this.state.decryptedBlob === null) { // Need to decrypt the attachment @@ -248,6 +265,7 @@ export default class MFileBody extends React.Component { // but it is not guaranteed between various browsers' settings. return ( + {placeholder} { _t("Decrypt %(text)s", { text: text }) } @@ -278,6 +296,7 @@ export default class MFileBody extends React.Component { // If the attachment is encrypted then put the link inside an iframe. return ( + {placeholder} { /* @@ -346,6 +365,7 @@ export default class MFileBody extends React.Component { if (this.props.tileShape === "file_grid") { return ( + {placeholder} { fileName } @@ -359,6 +379,7 @@ export default class MFileBody extends React.Component { } else { return ( + {placeholder} @@ -371,6 +392,7 @@ export default class MFileBody extends React.Component { } else { const extra = text ? (': ' + text) : ''; return + {placeholder} { _t("Invalid file%(extra)s", { extra: extra }) } ; } diff --git a/src/components/views/messages/MImageBody.js b/src/components/views/messages/MImageBody.js index a8cdc17abf..771d12accd 100644 --- a/src/components/views/messages/MImageBody.js +++ b/src/components/views/messages/MImageBody.js @@ -452,7 +452,7 @@ export default class MImageBody extends React.Component { // Overidden by MStickerBody getFileBody() { - return ; + return ; } render() { diff --git a/src/components/views/messages/MVideoBody.tsx b/src/components/views/messages/MVideoBody.tsx index 9628f11809..ce4a4eda76 100644 --- a/src/components/views/messages/MVideoBody.tsx +++ b/src/components/views/messages/MVideoBody.tsx @@ -243,7 +243,7 @@ export default class MVideoBody extends React.PureComponent { onPlay={this.videoOnPlay} > - + ); } diff --git a/src/components/views/messages/SenderProfile.js b/src/components/views/messages/SenderProfile.js index afe2d6d118..d2db05252c 100644 --- a/src/components/views/messages/SenderProfile.js +++ b/src/components/views/messages/SenderProfile.js @@ -18,14 +18,12 @@ import React from 'react'; import PropTypes from 'prop-types'; import Flair from '../elements/Flair.js'; import FlairStore from '../../../stores/FlairStore'; -import { _t } from '../../../languageHandler'; import {getUserNameColorClass} from '../../../utils/FormattingUtils'; import MatrixClientContext from "../../../contexts/MatrixClientContext"; export default class SenderProfile extends React.Component { static propTypes = { mxEvent: PropTypes.object.isRequired, // event whose sender we're showing - text: PropTypes.string, // Text to show. Defaults to sender name onClick: PropTypes.func, }; @@ -118,17 +116,10 @@ export default class SenderProfile extends React.Component { { flair } ; - const content = this.props.text ? - - - { _t(this.props.text, { senderName: () => nameElem }) } - - : nameFlair; - return ( - { content } + { nameFlair } ); diff --git a/src/components/views/room_settings/RoomProfileSettings.js b/src/components/views/room_settings/RoomProfileSettings.js index c651216e8c..65acc802dc 100644 --- a/src/components/views/room_settings/RoomProfileSettings.js +++ b/src/components/views/room_settings/RoomProfileSettings.js @@ -100,10 +100,11 @@ export default class RoomProfileSettings extends React.Component { const newState = {}; // TODO: What do we do about errors? - + const displayName = this.state.displayName.trim(); if (this.state.originalDisplayName !== this.state.displayName) { - await client.setRoomName(this.props.roomId, this.state.displayName); - newState.originalDisplayName = this.state.displayName; + await client.setRoomName(this.props.roomId, displayName); + newState.originalDisplayName = displayName; + newState.displayName = displayName; } if (this.state.avatarFile) { diff --git a/src/components/views/rooms/AppsDrawer.js b/src/components/views/rooms/AppsDrawer.js index ef30e4a8f5..aa7120bbe6 100644 --- a/src/components/views/rooms/AppsDrawer.js +++ b/src/components/views/rooms/AppsDrawer.js @@ -53,6 +53,8 @@ export default class AppsDrawer extends React.Component { this.state = { apps: this._getApps(), + resizingVertical: false, // true when changing the height of the apps drawer + resizingHorizontal: false, // true when chagning the distribution of the width between widgets }; this._resizeContainer = null; @@ -85,13 +87,16 @@ export default class AppsDrawer extends React.Component { } onIsResizing = (resizing) => { - this.setState({ resizing }); + // This one is the vertical, ie. change height of apps drawer + this.setState({ resizingVertical: resizing }); if (!resizing) { this._relaxResizer(); } }; _createResizer() { + // This is the horizontal one, changing the distribution of the width between the app tiles + // (ie. a vertical resize handle because, the handle itself is vertical...) const classNames = { handle: "mx_ResizeHandle", vertical: "mx_ResizeHandle_vertical", @@ -100,6 +105,7 @@ export default class AppsDrawer extends React.Component { const collapseConfig = { onResizeStart: () => { this._resizeContainer.classList.add("mx_AppsDrawer_resizing"); + this.setState({ resizingHorizontal: true }); }, onResizeStop: () => { this._resizeContainer.classList.remove("mx_AppsDrawer_resizing"); @@ -107,6 +113,7 @@ export default class AppsDrawer extends React.Component { this.props.room, Container.Top, this.state.apps.slice(1).map((_, i) => this.resizer.forHandleAt(i).size), ); + this.setState({ resizingHorizontal: false }); }, }; // pass a truthy container for now, we won't call attach until we update it @@ -162,6 +169,10 @@ export default class AppsDrawer extends React.Component { } }; + isResizing() { + return this.state.resizingVertical || this.state.resizingHorizontal; + } + onAction = (action) => { const hideWidgetKey = this.props.room.roomId + '_hide_widget_drawer'; switch (action.action) { @@ -209,6 +220,7 @@ export default class AppsDrawer extends React.Component { creatorUserId={app.creatorUserId} widgetPageTitle={WidgetUtils.getWidgetDataTitle(app)} waitForIframeLoad={app.waitForIframeLoad} + pointerEvents={this.isResizing() ? 'none' : undefined} />); }); diff --git a/src/components/views/rooms/AuxPanel.tsx b/src/components/views/rooms/AuxPanel.tsx index 4ce31be410..d193b98ec1 100644 --- a/src/components/views/rooms/AuxPanel.tsx +++ b/src/components/views/rooms/AuxPanel.tsx @@ -17,10 +17,8 @@ limitations under the License. import React from 'react'; import {MatrixClientPeg} from "../../../MatrixClientPeg"; import { Room } from 'matrix-js-sdk/src/models/room' -import * as sdk from '../../../index'; import dis from "../../../dispatcher/dispatcher"; import AppsDrawer from './AppsDrawer'; -import { _t } from '../../../languageHandler'; import classNames from 'classnames'; import RateLimitedFunc from '../../../ratelimitedfunc'; import SettingsStore from "../../../settings/SettingsStore"; @@ -36,9 +34,6 @@ interface IProps { userId: string, showApps: boolean, // Render apps - // set to true to show the file drop target - draggingFile: boolean, - // maxHeight attribute for the aux panel and the video // therein maxHeight: number, @@ -149,21 +144,6 @@ export default class AuxPanel extends React.Component { } render() { - const TintableSvg = sdk.getComponent("elements.TintableSvg"); - - let fileDropTarget = null; - if (this.props.draggingFile) { - fileDropTarget = ( - - - - - { _t("Drop file here to upload") } - - - ); - } - const callView = ( { { stateViews } { appsDrawer } - { fileDropTarget } { callView } { this.props.children } diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index 87fb190678..a705e92d9c 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -22,7 +22,7 @@ import React, {createRef} from 'react'; import PropTypes from 'prop-types'; import classNames from "classnames"; import {EventType} from "matrix-js-sdk/src/@types/event"; -import { _t, _td } from '../../../languageHandler'; +import { _t } from '../../../languageHandler'; import * as TextForEvent from "../../../TextForEvent"; import * as sdk from "../../../index"; import dis from '../../../dispatcher/dispatcher'; @@ -171,6 +171,9 @@ export default class EventTile extends React.Component { // targeting) lastInSection: PropTypes.bool, + // True if the event is the last successful (sent) event. + isLastSuccessful: PropTypes.bool, + /* true if this is search context (which has the effect of greying out * the text */ @@ -264,6 +267,81 @@ export default class EventTile extends React.Component { this._tile = createRef(); this._replyThread = createRef(); + + // Throughout the component we manage a read receipt listener to see if our tile still + // qualifies for a "sent" or "sending" state (based on their relevant conditions). We + // don't want to over-subscribe to the read receipt events being fired, so we use a flag + // to determine if we've already subscribed and use a combination of other flags to find + // out if we should even be subscribed at all. + this._isListeningForReceipts = false; + } + + /** + * When true, the tile qualifies for some sort of special read receipt. This could be a 'sending' + * or 'sent' receipt, for example. + * @returns {boolean} + * @private + */ + get _isEligibleForSpecialReceipt() { + // First, if there are other read receipts then just short-circuit this. + if (this.props.readReceipts && this.props.readReceipts.length > 0) return false; + if (!this.props.mxEvent) return false; + + // Sanity check (should never happen, but we shouldn't explode if it does) + const room = this.context.getRoom(this.props.mxEvent.getRoomId()); + if (!room) return false; + + // Quickly check to see if the event was sent by us. If it wasn't, it won't qualify for + // special read receipts. + const myUserId = MatrixClientPeg.get().getUserId(); + if (this.props.mxEvent.getSender() !== myUserId) return false; + + // Finally, determine if the type is relevant to the user. This notably excludes state + // events and pretty much anything that can't be sent by the composer as a message. For + // those we rely on local echo giving the impression of things changing, and expect them + // to be quick. + const simpleSendableEvents = [ + EventType.Sticker, + EventType.RoomMessage, + EventType.RoomMessageEncrypted, + ]; + if (!simpleSendableEvents.includes(this.props.mxEvent.getType())) return false; + + // Default case + return true; + } + + get _shouldShowSentReceipt() { + // If we're not even eligible, don't show the receipt. + if (!this._isEligibleForSpecialReceipt) return false; + + // We only show the 'sent' receipt on the last successful event. + if (!this.props.lastSuccessful) return false; + + // Check to make sure the sending state is appropriate. A null/undefined send status means + // that the message is 'sent', so we're just double checking that it's explicitly not sent. + if (this.props.eventSendStatus && this.props.eventSendStatus !== 'sent') return false; + + // If anyone has read the event besides us, we don't want to show a sent receipt. + const receipts = this.props.readReceipts || []; + const myUserId = MatrixClientPeg.get().getUserId(); + if (receipts.some(r => r.userId !== myUserId)) return false; + + // Finally, we should show a receipt. + return true; + } + + get _shouldShowSendingReceipt() { + // If we're not even eligible, don't show the receipt. + if (!this._isEligibleForSpecialReceipt) return false; + + // Check the event send status to see if we are pending. Null/undefined status means the + // message was sent, so check for that and 'sent' explicitly. + if (!this.props.eventSendStatus || this.props.eventSendStatus === 'sent') return false; + + // Default to showing - there's no other event properties/behaviours we care about at + // this point. + return true; } // TODO: [REACT-WARNING] Move into constructor @@ -281,6 +359,11 @@ export default class EventTile extends React.Component { if (this.props.showReactions) { this.props.mxEvent.on("Event.relationsCreated", this._onReactionsCreated); } + + if (this._shouldShowSentReceipt || this._shouldShowSendingReceipt) { + client.on("Room.receipt", this._onRoomReceipt); + this._isListeningForReceipts = true; + } } // TODO: [REACT-WARNING] Replace with appropriate lifecycle event @@ -305,12 +388,42 @@ export default class EventTile extends React.Component { const client = this.context; client.removeListener("deviceVerificationChanged", this.onDeviceVerificationChanged); client.removeListener("userTrustStatusChanged", this.onUserVerificationChanged); + client.removeListener("Room.receipt", this._onRoomReceipt); + this._isListeningForReceipts = false; this.props.mxEvent.removeListener("Event.decrypted", this._onDecrypted); if (this.props.showReactions) { this.props.mxEvent.removeListener("Event.relationsCreated", this._onReactionsCreated); } } + componentDidUpdate(prevProps, prevState, snapshot) { + // If we're not listening for receipts and expect to be, register a listener. + if (!this._isListeningForReceipts && (this._shouldShowSentReceipt || this._shouldShowSendingReceipt)) { + this.context.on("Room.receipt", this._onRoomReceipt); + this._isListeningForReceipts = true; + } + } + + _onRoomReceipt = (ev, room) => { + // ignore events for other rooms + const tileRoom = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId()); + if (room !== tileRoom) return; + + if (!this._shouldShowSentReceipt && !this._shouldShowSendingReceipt && !this._isListeningForReceipts) { + return; + } + + // We force update because we have no state or prop changes to queue up, instead relying on + // the getters we use here to determine what needs rendering. + this.forceUpdate(() => { + // Per elsewhere in this file, we can remove the listener once we will have no further purpose for it. + if (!this._shouldShowSentReceipt && !this._shouldShowSendingReceipt) { + this.context.removeListener("Room.receipt", this._onRoomReceipt); + this._isListeningForReceipts = false; + } + }); + }; + /** called when the event is decrypted after we show it. */ _onDecrypted = () => { @@ -454,6 +567,13 @@ export default class EventTile extends React.Component { }; getReadAvatars() { + if (this._shouldShowSentReceipt) { + return ; + } + if (this._shouldShowSendingReceipt) { + return ; + } + // return early if there are no read receipts if (!this.props.readReceipts || this.props.readReceipts.length === 0) { return (); @@ -692,7 +812,7 @@ export default class EventTile extends React.Component { mx_EventTile_isEditing: isEditing, mx_EventTile_info: isInfoMessage, mx_EventTile_12hr: this.props.isTwelveHour, - mx_EventTile_encrypting: this.props.eventSendStatus === 'encrypting', + // Note: we keep the `sending` state class for tests, not for our styles mx_EventTile_sending: !isEditing && isSending, mx_EventTile_notSent: this.props.eventSendStatus === 'not_sent', mx_EventTile_highlight: this.props.tileShape === 'notif' ? false : this.shouldHighlight(), @@ -768,15 +888,10 @@ export default class EventTile extends React.Component { } if (needsSenderProfile) { - let text = null; if (!this.props.tileShape || this.props.tileShape === 'reply' || this.props.tileShape === 'reply_preview') { - if (msgtype === 'm.image') text = _td('%(senderName)s sent an image'); - else if (msgtype === 'm.video') text = _td('%(senderName)s sent a video'); - else if (msgtype === 'm.file') text = _td('%(senderName)s uploaded a file'); sender = ; + enableFlair={this.props.enableFlair} />; } else { sender = ; } diff --git a/src/components/views/settings/ProfileSettings.js b/src/components/views/settings/ProfileSettings.js index 4dc69884c3..89d7cf6c2b 100644 --- a/src/components/views/settings/ProfileSettings.js +++ b/src/components/views/settings/ProfileSettings.js @@ -81,10 +81,12 @@ export default class ProfileSettings extends React.Component { const client = MatrixClientPeg.get(); const newState = {}; + const displayName = this.state.displayName.trim(); try { if (this.state.originalDisplayName !== this.state.displayName) { - await client.setDisplayName(this.state.displayName); - newState.originalDisplayName = this.state.displayName; + await client.setDisplayName(displayName); + newState.originalDisplayName = displayName; + newState.displayName = displayName; } if (this.state.avatarFile) { diff --git a/src/contexts/RoomContext.ts b/src/contexts/RoomContext.ts index b00dc148e4..30ff74b071 100644 --- a/src/contexts/RoomContext.ts +++ b/src/contexts/RoomContext.ts @@ -43,6 +43,7 @@ const RoomContext = createContext({ canReply: false, layout: Layout.Group, matrixClientIsReady: false, + dragCounter: 0, }); RoomContext.displayName = "RoomContext"; export default RoomContext; diff --git a/src/dispatcher/actions.ts b/src/dispatcher/actions.ts index 12bf4c57a3..cd32c3743f 100644 --- a/src/dispatcher/actions.ts +++ b/src/dispatcher/actions.ts @@ -113,4 +113,29 @@ export enum Action { * XXX: Ditto */ VirtualRoomSupportUpdated = "virtual_room_support_updated", + + /** + * Fired when an upload has started. Should be used with UploadStartedPayload. + */ + UploadStarted = "upload_started", + + /** + * Fired when an upload makes progress. Should be used with UploadProgressPayload. + */ + UploadProgress = "upload_progress", + + /** + * Fired when an upload is completed. Should be used with UploadFinishedPayload. + */ + UploadFinished = "upload_finished", + + /** + * Fired when an upload fails. Should be used with UploadErrorPayload. + */ + UploadFailed = "upload_failed", + + /** + * Fired when an upload is cancelled by the user. Should be used with UploadCanceledPayload. + */ + UploadCanceled = "upload_canceled", } diff --git a/src/dispatcher/payloads/UploadPayload.ts b/src/dispatcher/payloads/UploadPayload.ts new file mode 100644 index 0000000000..40c2710dd6 --- /dev/null +++ b/src/dispatcher/payloads/UploadPayload.ts @@ -0,0 +1,51 @@ +/* +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 { ActionPayload } from "../payloads"; +import { Action } from "../actions"; +import {IUpload} from "../../models/IUpload"; + +interface UploadPayload extends ActionPayload { + /** + * The upload with fields representing the new upload state. + */ + upload: IUpload; +} + +export interface UploadStartedPayload extends UploadPayload { + action: Action.UploadStarted; +} + +export interface UploadProgressPayload extends UploadPayload { + action: Action.UploadProgress; +} + +export interface UploadErrorPayload extends UploadPayload { + action: Action.UploadFailed; + + /** + * An error to describe what went wrong with the upload. + */ + error: Error; +} + +export interface UploadFinishedPayload extends UploadPayload { + action: Action.UploadFinished; +} + +export interface UploadCanceledPayload extends UploadPayload { + action: Action.UploadCanceled; +} diff --git a/src/editor/parts.ts b/src/editor/parts.ts index 8a7ccfcb7b..67f6a2c0c5 100644 --- a/src/editor/parts.ts +++ b/src/editor/parts.ts @@ -329,8 +329,8 @@ class NewlinePart extends BasePart implements IBasePart { } class RoomPillPart extends PillPart { - constructor(displayAlias, private room: Room) { - super(displayAlias, displayAlias); + constructor(resourceId: string, label: string, private room: Room) { + super(resourceId, label); } setAvatar(node: HTMLElement) { @@ -357,6 +357,10 @@ class RoomPillPart extends PillPart { } class AtRoomPillPart extends RoomPillPart { + constructor(text: string, room: Room) { + super(text, text, room); + } + get type(): IPillPart["type"] { return Type.AtRoomPill; } @@ -521,7 +525,7 @@ export class PartCreator { r.getAltAliases().includes(alias); }); } - return new RoomPillPart(alias, room); + return new RoomPillPart(alias, room ? room.name : alias, room); } atRoomPill(text: string) { diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index c3752d7942..833a8c7838 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -58,6 +58,8 @@ "You cannot place VoIP calls in this browser.": "You cannot place VoIP calls in this browser.", "Too Many Calls": "Too Many Calls", "You've reached the maximum number of simultaneous calls.": "You've reached the maximum number of simultaneous calls.", + "Already in call": "Already in call", + "You're already in a call with this person.": "You're already in a call with this person.", "You cannot place a call with yourself.": "You cannot place a call with yourself.", "Call in Progress": "Call in Progress", "A call is currently being placed!": "A call is currently being placed!", @@ -657,6 +659,7 @@ "Unexpected error resolving identity server configuration": "Unexpected error resolving identity server configuration", "The message you are trying to send is too large.": "The message you are trying to send is too large.", "This homeserver has hit its Monthly Active User limit.": "This homeserver has hit its Monthly Active User limit.", + "This homeserver has been blocked by its administrator.": "This homeserver has been blocked by its administrator.", "This homeserver has exceeded one of its resource limits.": "This homeserver has exceeded one of its resource limits.", "Please contact your service administrator to continue using the service.": "Please contact your service administrator to continue using the service.", "Unable to connect to Homeserver. Retrying...": "Unable to connect to Homeserver. Retrying...", @@ -737,6 +740,7 @@ "Element Web is experimental on mobile. For a better experience and the latest features, use our free native app.": "Element Web is experimental on mobile. For a better experience and the latest features, use our free native app.", "Use app": "Use app", "Your homeserver has exceeded its user limit.": "Your homeserver has exceeded its user limit.", + "This homeserver has been blocked by it's administrator.": "This homeserver has been blocked by it's administrator.", "Your homeserver has exceeded one of its resource limits.": "Your homeserver has exceeded one of its resource limits.", "Contact your server admin.": "Contact your server admin.", "Warning": "Warning", @@ -1417,8 +1421,6 @@ "Remove %(phone)s?": "Remove %(phone)s?", "A text message has been sent to +%(msisdn)s. Please enter the verification code it contains.": "A text message has been sent to +%(msisdn)s. Please enter the verification code it contains.", "Phone Number": "Phone Number", - "Drop File Here": "Drop File Here", - "Drop file here to upload": "Drop file here to upload", "This user has not verified all of their sessions.": "This user has not verified all of their sessions.", "You have not verified this user.": "You have not verified this user.", "You have verified this user. This user has verified all of their sessions.": "You have verified this user. This user has verified all of their sessions.", @@ -1428,9 +1430,6 @@ "Edit message": "Edit message", "Mod": "Mod", "This event could not be displayed": "This event could not be displayed", - "%(senderName)s sent an image": "%(senderName)s sent an image", - "%(senderName)s sent a video": "%(senderName)s sent a video", - "%(senderName)s uploaded a file": "%(senderName)s uploaded a file", "Your key share request has been sent - please check your other sessions for key share requests.": "Your key share request has been sent - please check your other sessions for key share requests.", "Key share requests are sent to your other sessions automatically. If you rejected or dismissed the key share request on your other sessions, click here to request the keys for this session again.": "Key share requests are sent to your other sessions automatically. If you rejected or dismissed the key share request on your other sessions, click here to request the keys for this session again.", "If your other sessions do not have the key for this message you will not be able to decrypt them.": "If your other sessions do not have the key for this message you will not be able to decrypt them.", @@ -2407,6 +2406,9 @@ "Set status": "Set status", "Set a new status...": "Set a new status...", "View Community": "View Community", + "Unable to start audio streaming.": "Unable to start audio streaming.", + "Failed to start livestream": "Failed to start livestream", + "Start audio stream": "Start audio stream", "Take a picture": "Take a picture", "Delete Widget": "Delete Widget", "Deleting a widget removes it for all users in this room. Are you sure you want to delete this widget?": "Deleting a widget removes it for all users in this room. Are you sure you want to delete this widget?", @@ -2572,6 +2574,7 @@ "Filter rooms and people": "Filter rooms and people", "You can't send any messages until you review and agree to our terms and conditions.": "You can't send any messages until you review and agree to our terms and conditions.", "Your message wasn't sent because this homeserver has hit its Monthly Active User Limit. Please contact your service administrator to continue using the service.": "Your message wasn't sent because this homeserver has hit its Monthly Active User Limit. Please contact your service administrator to continue using the service.", + "Your message wasn't sent because this homeserver has been blocked by it's administrator. Please contact your service administrator to continue using the service.": "Your message wasn't sent because this homeserver has been blocked by it's administrator. Please contact your service administrator to continue using the service.", "Your message wasn't sent because this homeserver has exceeded a resource limit. Please contact your service administrator to continue using the service.": "Your message wasn't sent because this homeserver has exceeded a resource limit. Please contact your service administrator to continue using the service.", "%(count)s of your messages have not been sent.|other": "Some of your messages have not been sent.", "%(count)s of your messages have not been sent.|one": "Your message was not sent.", @@ -2586,6 +2589,7 @@ "No more results": "No more results", "Room": "Room", "Failed to reject invite": "Failed to reject invite", + "Drop file here to upload": "Drop file here to upload", "You have %(count)s unread notifications in a prior version of this room.|other": "You have %(count)s unread notifications in a prior version of this room.", "You have %(count)s unread notifications in a prior version of this room.|one": "You have %(count)s unread notification in a prior version of this room.", "Unnamed Space": "Unnamed Space", diff --git a/src/models/IUpload.ts b/src/models/IUpload.ts new file mode 100644 index 0000000000..5b376e9330 --- /dev/null +++ b/src/models/IUpload.ts @@ -0,0 +1,24 @@ +/* +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. +*/ + +export interface IUpload { + fileName: string; + roomId: string; + total: number; + loaded: number; + promise: Promise; + canceled?: boolean; +} diff --git a/src/stores/widgets/ElementWidgetActions.ts b/src/stores/widgets/ElementWidgetActions.ts index de48746a74..cd591a6fb4 100644 --- a/src/stores/widgets/ElementWidgetActions.ts +++ b/src/stores/widgets/ElementWidgetActions.ts @@ -19,6 +19,7 @@ import { IWidgetApiRequest } from "matrix-widget-api"; export enum ElementWidgetActions { ClientReady = "im.vector.ready", HangupCall = "im.vector.hangup", + StartLiveStream = "im.vector.start_live_stream", OpenIntegrationManager = "integration_manager_open", /** diff --git a/src/toasts/ServerLimitToast.tsx b/src/toasts/ServerLimitToast.tsx index 81b5492402..36de6f04e0 100644 --- a/src/toasts/ServerLimitToast.tsx +++ b/src/toasts/ServerLimitToast.tsx @@ -26,6 +26,7 @@ const TOAST_KEY = "serverlimit"; export const showToast = (limitType: string, onHideToast: () => void, adminContact?: string, syncError?: boolean) => { const errorText = messageForResourceLimitError(limitType, adminContact, { 'monthly_active_user': _td("Your homeserver has exceeded its user limit."), + 'hs_blocked': _td("This homeserver has been blocked by it's administrator."), '': _td("Your homeserver has exceeded one of its resource limits."), }); const contactText = messageForResourceLimitError(limitType, adminContact, { diff --git a/src/utils/ErrorUtils.js b/src/utils/ErrorUtils.js index f0a4d7c49e..2c6acd5503 100644 --- a/src/utils/ErrorUtils.js +++ b/src/utils/ErrorUtils.js @@ -62,6 +62,7 @@ export function messageForSyncError(err) { err.data.admin_contact, { 'monthly_active_user': _td("This homeserver has hit its Monthly Active User limit."), + 'hs_blocked': _td("This homeserver has been blocked by its administrator."), '': _td("This homeserver has exceeded one of its resource limits."), }, ); diff --git a/src/utils/MultiInviter.js b/src/utils/MultiInviter.js index 7d1c900360..63d3942b37 100644 --- a/src/utils/MultiInviter.js +++ b/src/utils/MultiInviter.js @@ -53,13 +53,15 @@ export default class MultiInviter { * instance of the class. * * @param {array} addrs Array of addresses to invite + * @param {string} reason Reason for inviting (optional) * @returns {Promise} Resolved when all invitations in the queue are complete */ - invite(addrs) { + invite(addrs, reason) { if (this.addrs.length > 0) { throw new Error("Already inviting/invited"); } this.addrs.push(...addrs); + this.reason = reason; for (const addr of this.addrs) { if (getAddressType(addr) === null) { @@ -123,7 +125,7 @@ export default class MultiInviter { } } - return MatrixClientPeg.get().invite(roomId, addr); + return MatrixClientPeg.get().invite(roomId, addr, undefined, this.reason); } else { throw new Error('Unsupported address'); } diff --git a/test/components/views/messages/TextualBody-test.js b/test/components/views/messages/TextualBody-test.js index d5b80b6756..a596825c09 100644 --- a/test/components/views/messages/TextualBody-test.js +++ b/test/components/views/messages/TextualBody-test.js @@ -208,7 +208,7 @@ describe("", () => { const content = wrapper.find(".mx_EventTile_body"); expect(content.html()).toBe('' + 'Hey ' + - '' + + '' + 'Member' + ''); @@ -267,8 +267,8 @@ describe("", () => { expect(content.html()).toBe( '' + 'A ' + '!ZxbRYPQXDXKGmDnJNg:example.com with vias',