diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index c9d11f02c8..fb237a5845 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,3 +1,3 @@ - + - + diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.md similarity index 100% rename from CONTRIBUTING.rst rename to CONTRIBUTING.md diff --git a/__mocks__/workerMock.js b/__mocks__/workerMock.js new file mode 100644 index 0000000000..6ee585673e --- /dev/null +++ b/__mocks__/workerMock.js @@ -0,0 +1 @@ +module.exports = jest.fn(); diff --git a/package.json b/package.json index 27c4f39a09..e80ed8dd5a 100644 --- a/package.json +++ b/package.json @@ -187,7 +187,8 @@ "\\$webapp/i18n/languages.json": "/__mocks__/languages.json", "decoderWorker\\.min\\.js": "/__mocks__/empty.js", "decoderWorker\\.min\\.wasm": "/__mocks__/empty.js", - "waveWorker\\.min\\.js": "/__mocks__/empty.js" + "waveWorker\\.min\\.js": "/__mocks__/empty.js", + "workers/(.+)\\.worker\\.ts": "/__mocks__/workerMock.js" }, "transformIgnorePatterns": [ "/node_modules/(?!matrix-js-sdk).+$" diff --git a/res/css/_components.scss b/res/css/_components.scss index 8f80f1bf97..bb22446258 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -120,6 +120,7 @@ @import "./views/elements/_AddressTile.scss"; @import "./views/elements/_DesktopBuildsNotice.scss"; @import "./views/elements/_DesktopCapturerSourcePicker.scss"; +@import "./views/elements/_DialPadBackspaceButton.scss"; @import "./views/elements/_DirectorySearchBox.scss"; @import "./views/elements/_Dropdown.scss"; @import "./views/elements/_EditableItemList.scss"; diff --git a/res/css/structures/_TabbedView.scss b/res/css/structures/_TabbedView.scss index 39a8ebed32..833450a25b 100644 --- a/res/css/structures/_TabbedView.scss +++ b/res/css/structures/_TabbedView.scss @@ -1,6 +1,7 @@ /* Copyright 2017 Travis Ralston Copyright 2019 New Vector Ltd +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. @@ -20,7 +21,6 @@ limitations under the License. padding: 0 0 0 16px; display: flex; flex-direction: column; - position: absolute; top: 0; bottom: 0; left: 0; @@ -28,11 +28,93 @@ limitations under the License. margin-top: 8px; } +.mx_TabbedView_tabsOnLeft { + flex-direction: column; + position: absolute; + + .mx_TabbedView_tabLabels { + width: 170px; + max-width: 170px; + position: fixed; + } + + .mx_TabbedView_tabPanel { + margin-left: 240px; // 170px sidebar + 70px padding + flex-direction: column; + } + + .mx_TabbedView_tabLabel_active { + background-color: $tab-label-active-bg-color; + color: $tab-label-active-fg-color; + } + + .mx_TabbedView_tabLabel_active .mx_TabbedView_maskedIcon::before { + background-color: $tab-label-active-icon-bg-color; + } + + .mx_TabbedView_maskedIcon { + width: 16px; + height: 16px; + margin-left: 8px; + margin-right: 16px; + } + + .mx_TabbedView_maskedIcon::before { + mask-size: 16px; + width: 16px; + height: 16px; + } +} + +.mx_TabbedView_tabsOnTop { + flex-direction: column; + + .mx_TabbedView_tabLabels { + display: flex; + margin-bottom: 8px; + } + + .mx_TabbedView_tabLabel { + padding-left: 0px; + padding-right: 52px; + + .mx_TabbedView_tabLabel_text { + font-size: 15px; + color: $tertiary-fg-color; + } + } + + .mx_TabbedView_tabPanel { + flex-direction: row; + } + + .mx_TabbedView_tabLabel_active { + color: $accent-color; + .mx_TabbedView_tabLabel_text { + color: $accent-color; + } + } + + .mx_TabbedView_tabLabel_active .mx_TabbedView_maskedIcon::before { + background-color: $accent-color; + } + + .mx_TabbedView_maskedIcon { + width: 22px; + height: 22px; + margin-left: 0px; + margin-right: 8px; + } + + .mx_TabbedView_maskedIcon::before { + mask-size: 22px; + width: inherit; + height: inherit; + } +} + .mx_TabbedView_tabLabels { - width: 170px; - max-width: 170px; color: $tab-label-fg-color; - position: fixed; } .mx_TabbedView_tabLabel { @@ -46,43 +128,25 @@ limitations under the License. position: relative; } -.mx_TabbedView_tabLabel_active { - background-color: $tab-label-active-bg-color; - color: $tab-label-active-fg-color; -} - .mx_TabbedView_maskedIcon { - margin-left: 8px; - margin-right: 16px; - width: 16px; - height: 16px; display: inline-block; } .mx_TabbedView_maskedIcon::before { display: inline-block; - background-color: $tab-label-icon-bg-color; + background-color: $icon-button-color; mask-repeat: no-repeat; - mask-size: 16px; - width: 16px; - height: 16px; mask-position: center; content: ''; } -.mx_TabbedView_tabLabel_active .mx_TabbedView_maskedIcon::before { - background-color: $tab-label-active-icon-bg-color; -} - .mx_TabbedView_tabLabel_text { vertical-align: middle; } .mx_TabbedView_tabPanel { - margin-left: 240px; // 170px sidebar + 70px padding flex-grow: 1; display: flex; - flex-direction: column; min-height: 0; // firefox } diff --git a/res/css/views/dialogs/_InviteDialog.scss b/res/css/views/dialogs/_InviteDialog.scss index c01b43c1c4..9fc4b7a15c 100644 --- a/res/css/views/dialogs/_InviteDialog.scss +++ b/res/css/views/dialogs/_InviteDialog.scss @@ -14,6 +14,10 @@ See the License for the specific language governing permissions and limitations under the License. */ +.mx_InviteDialog_transferWrapper .mx_Dialog { + padding-bottom: 16px; +} + .mx_InviteDialog_addressBar { display: flex; flex-direction: row; @@ -286,16 +290,41 @@ limitations under the License. } } -.mx_InviteDialog { +.mx_InviteDialog_other { // Prevent the dialog from jumping around randomly when elements change. height: 600px; padding-left: 20px; // the design wants some padding on the left - display: flex; + + .mx_InviteDialog_userSections { + height: calc(100% - 115px); // mx_InviteDialog's height minus some for the upper and lower elements + } +} + +.mx_InviteDialog_content { + height: calc(100% - 36px); // full height minus the size of the header + overflow: hidden; +} + +.mx_InviteDialog_transfer { + width: 496px; + height: 466px; flex-direction: column; .mx_InviteDialog_content { - overflow: hidden; - height: 100%; + flex-direction: column; + + .mx_TabbedView { + height: calc(100% - 60px); + } + overflow: visible; + } + + .mx_InviteDialog_addressBar { + margin-top: 8px; + } + + input[type="checkbox"] { + margin-right: 8px; } } @@ -303,7 +332,6 @@ limitations under the License. margin-top: 4px; overflow-y: auto; padding: 0 45px 4px 0; - height: calc(100% - 115px); // mx_InviteDialog's height minus some for the upper and lower elements } .mx_InviteDialog_hasFooter .mx_InviteDialog_userSections { @@ -318,6 +346,74 @@ limitations under the License. padding: 0; } +.mx_InviteDialog_dialPad .mx_InviteDialog_dialPadField { + border-top: 0; + border-left: 0; + border-right: 0; + border-radius: 0; + margin-top: 0; + border-color: $quaternary-fg-color; + + input { + font-size: 18px; + font-weight: 600; + padding-top: 0; + } +} + +.mx_InviteDialog_dialPad .mx_InviteDialog_dialPadField:focus-within { + border-color: $accent-color; +} + +.mx_InviteDialog_dialPadField .mx_Field_postfix { + /* Remove border separator between postfix and field content */ + border-left: none; +} + +.mx_InviteDialog_dialPad { + width: 224px; + margin-top: 16px; + margin-left: auto; + margin-right: auto; +} + +.mx_InviteDialog_dialPad .mx_DialPad { + row-gap: 16px; + column-gap: 48px; + + margin-left: auto; + margin-right: auto; +} + +.mx_InviteDialog_transferConsultConnect { + padding-top: 16px; + /* This wants a drop shadow the full width of the dialog, so relative-position it + * and make it wider, then compensate with padding + */ + position: relative; + width: 496px; + left: -24px; + padding-left: 24px; + padding-right: 24px; + border-top: 1px solid $message-body-panel-bg-color; + + display: flex; + flex-direction: row; + align-items: center; +} + +.mx_InviteDialog_transferConsultConnect_pushRight { + margin-left: auto; +} + +.mx_InviteDialog_userDirectoryIcon::before { + mask-image: url('$(res)/img/voip/tab-userdirectory.svg'); +} + +.mx_InviteDialog_dialPadIcon::before { + mask-image: url('$(res)/img/voip/tab-dialpad.svg'); +} + .mx_InviteDialog_multiInviterError { > h4 { font-size: $font-15px; diff --git a/res/css/views/elements/_DialPadBackspaceButton.scss b/res/css/views/elements/_DialPadBackspaceButton.scss new file mode 100644 index 0000000000..40e4af7025 --- /dev/null +++ b/res/css/views/elements/_DialPadBackspaceButton.scss @@ -0,0 +1,40 @@ +/* +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_DialPadBackspaceButton { + position: relative; + height: 28px; + width: 28px; + + &::before { + /* force this element to appear on the DOM */ + content: ""; + + background-color: #8D97A5; + width: inherit; + height: inherit; + top: 0px; + left: 0px; + position: absolute; + display: inline-block; + vertical-align: middle; + + mask-image: url('$(res)/img/element-icons/call/delete.svg'); + mask-position: 8px; + mask-size: 20px; + mask-repeat: no-repeat; + } +} diff --git a/res/css/views/voip/_DialPad.scss b/res/css/views/voip/_DialPad.scss index 483b131bfe..eefd2e9ba5 100644 --- a/res/css/views/voip/_DialPad.scss +++ b/res/css/views/voip/_DialPad.scss @@ -16,11 +16,21 @@ limitations under the License. .mx_DialPad { display: grid; + row-gap: 16px; + column-gap: 0px; + margin-top: 24px; + margin-left: auto; + margin-right: auto; + + /* squeeze the dial pad buttons together horizontally */ grid-template-columns: repeat(3, 1fr); - gap: 16px; } .mx_DialPad_button { + display: flex; + flex-direction: column; + justify-content: center; + width: 40px; height: 40px; background-color: $dialpad-button-bg-color; @@ -29,10 +39,19 @@ limitations under the License. font-weight: 600; text-align: center; vertical-align: middle; - line-height: 40px; + margin-left: auto; + margin-right: auto; } -.mx_DialPad_deleteButton, .mx_DialPad_dialButton { +.mx_DialPad_button .mx_DialPad_buttonSubText { + font-size: 8px; +} + +.mx_DialPad_dialButton { + /* Always show the dial button in the center grid column */ + grid-column: 2; + background-color: $accent-color; + &::before { content: ''; display: inline-block; @@ -42,21 +61,7 @@ limitations under the License. mask-repeat: no-repeat; mask-size: 20px; mask-position: center; - background-color: $primary-bg-color; - } -} - -.mx_DialPad_deleteButton { - background-color: $notice-primary-color; - &::before { - mask-image: url('$(res)/img/element-icons/call/delete.svg'); - mask-position: 9px; // delete icon is right-heavy so have to be slightly to the left to look centered - } -} - -.mx_DialPad_dialButton { - background-color: $accent-color; - &::before { + background-color: #FFF; // on all themes mask-image: url('$(res)/img/element-icons/call/voice-call.svg'); } } diff --git a/res/css/views/voip/_DialPadContextMenu.scss b/res/css/views/voip/_DialPadContextMenu.scss index 31327113cf..0019994e72 100644 --- a/res/css/views/voip/_DialPadContextMenu.scss +++ b/res/css/views/voip/_DialPadContextMenu.scss @@ -14,10 +14,40 @@ See the License for the specific language governing permissions and limitations under the License. */ +.mx_DialPadContextMenu_dialPad .mx_DialPad { + row-gap: 16px; + column-gap: 32px; +} + +.mx_DialPadContextMenuWrapper { + padding: 15px; +} + .mx_DialPadContextMenu_header { - margin-top: 12px; - margin-left: 12px; - margin-right: 12px; + border: none; + margin-top: 32px; + margin-left: 20px; + margin-right: 20px; + + /* a separator between the input line and the dial buttons */ + border-bottom: 1px solid $quaternary-fg-color; + transition: border-bottom 0.25s; +} + +.mx_DialPadContextMenu_cancel { + float: right; + mask: url('$(res)/img/feather-customised/cancel.svg'); + mask-repeat: no-repeat; + mask-position: center; + mask-size: cover; + width: 14px; + height: 14px; + background-color: $dialog-close-fg-color; + cursor: pointer; +} + +.mx_DialPadContextMenu_header:focus-within { + border-bottom: 1px solid $accent-color; } .mx_DialPadContextMenu_title { @@ -30,7 +60,6 @@ limitations under the License. height: 1.5em; font-size: 18px; font-weight: 600; - max-width: 150px; border: none; margin: 0px; } @@ -38,7 +67,7 @@ limitations under the License. font-size: 18px; font-weight: 600; overflow: hidden; - max-width: 150px; + max-width: 185px; text-align: left; direction: rtl; padding: 8px 0px; @@ -48,13 +77,3 @@ limitations under the License. .mx_DialPadContextMenu_dialPad { margin: 16px; } - -.mx_DialPadContextMenu_horizSep { - position: relative; - &::before { - content: ''; - position: absolute; - width: 100%; - border-bottom: 1px solid $input-darker-bg-color; - } -} diff --git a/res/css/views/voip/_DialPadModal.scss b/res/css/views/voip/_DialPadModal.scss index f9d7673a38..b8042f77ae 100644 --- a/res/css/views/voip/_DialPadModal.scss +++ b/res/css/views/voip/_DialPadModal.scss @@ -19,14 +19,23 @@ limitations under the License. } .mx_DialPadModal { - width: 192px; - height: 368px; + width: 292px; + height: 370px; + padding: 16px 0px 0px 0px; } .mx_DialPadModal_header { - margin-top: 12px; - margin-left: 12px; - margin-right: 12px; + margin-top: 32px; + margin-left: 40px; + margin-right: 40px; + + /* a separator between the input line and the dial buttons */ + border-bottom: 1px solid $quaternary-fg-color; + transition: border-bottom 0.25s; +} + +.mx_DialPadModal_header:focus-within { + border-bottom: 1px solid $accent-color; } .mx_DialPadModal_title { @@ -45,11 +54,18 @@ limitations under the License. height: 14px; background-color: $dialog-close-fg-color; cursor: pointer; + margin-right: 16px; } .mx_DialPadModal_field { border: none; margin: 0px; + height: 30px; +} + +.mx_DialPadModal_field .mx_Field_postfix { + /* Remove border separator between postfix and field content */ + border-left: none; } .mx_DialPadModal_field input { @@ -62,13 +78,3 @@ limitations under the License. margin-right: 16px; margin-top: 16px; } - -.mx_DialPadModal_horizSep { - position: relative; - &::before { - content: ''; - position: absolute; - width: 100%; - border-bottom: 1px solid $input-darker-bg-color; - } -} diff --git a/res/img/voip/tab-dialpad.svg b/res/img/voip/tab-dialpad.svg new file mode 100644 index 0000000000..b7add0addb --- /dev/null +++ b/res/img/voip/tab-dialpad.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/img/voip/tab-userdirectory.svg b/res/img/voip/tab-userdirectory.svg new file mode 100644 index 0000000000..792ded7be4 --- /dev/null +++ b/res/img/voip/tab-userdirectory.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/res/themes/dark/css/_dark.scss b/res/themes/dark/css/_dark.scss index 57cbc7efa9..74b33fbd02 100644 --- a/res/themes/dark/css/_dark.scss +++ b/res/themes/dark/css/_dark.scss @@ -118,7 +118,7 @@ $voipcall-plinth-color: #394049; // ******************** $theme-button-bg-color: #e3e8f0; -$dialpad-button-bg-color: #6F7882; +$dialpad-button-bg-color: #394049; $roomlist-button-bg-color: rgba(141, 151, 165, 0.2); // Buttons include the filter box, explore button, and sublist buttons $roomlist-filter-active-bg-color: $bg-color; diff --git a/src/AddThreepid.js b/src/AddThreepid.js index eb822c6d75..ab291128a7 100644 --- a/src/AddThreepid.js +++ b/src/AddThreepid.js @@ -248,7 +248,7 @@ export default class AddThreepid { /** * Takes a phone number verification code as entered by the user and validates - * it with the ID server, then if successful, adds the phone number. + * it with the identity server, then if successful, adds the phone number. * @param {string} msisdnToken phone number verification code as entered by the user * @return {Promise} Resolves if the phone number was added. Rejects with an object * with a "message" property which contains a human-readable message detailing why diff --git a/src/BlurhashEncoder.ts b/src/BlurhashEncoder.ts new file mode 100644 index 0000000000..2aee370fe9 --- /dev/null +++ b/src/BlurhashEncoder.ts @@ -0,0 +1,60 @@ +/* +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 { defer, IDeferred } from "matrix-js-sdk/src/utils"; + +// @ts-ignore - `.ts` is needed here to make TS happy +import BlurhashWorker from "./workers/blurhash.worker.ts"; + +interface IBlurhashWorkerResponse { + seq: number; + blurhash: string; +} + +export class BlurhashEncoder { + private static internalInstance = new BlurhashEncoder(); + + public static get instance(): BlurhashEncoder { + return BlurhashEncoder.internalInstance; + } + + private readonly worker: Worker; + private seq = 0; + private pendingDeferredMap = new Map>(); + + constructor() { + this.worker = new BlurhashWorker(); + this.worker.onmessage = this.onMessage; + } + + private onMessage = (ev: MessageEvent) => { + const { seq, blurhash } = ev.data; + const deferred = this.pendingDeferredMap.get(seq); + if (deferred) { + this.pendingDeferredMap.delete(seq); + deferred.resolve(blurhash); + } + }; + + public getBlurhash(imageData: ImageData): Promise { + const seq = this.seq++; + const deferred = defer(); + this.pendingDeferredMap.set(seq, deferred); + this.worker.postMessage({ seq, imageData }); + return deferred.promise; + } +} + diff --git a/src/CallHandler.tsx b/src/CallHandler.tsx index a0adee6b8d..f90854ee64 100644 --- a/src/CallHandler.tsx +++ b/src/CallHandler.tsx @@ -394,7 +394,7 @@ export default class CallHandler extends EventEmitter { } private setCallListeners(call: MatrixCall) { - let mappedRoomId = CallHandler.sharedInstance().roomIdForCall(call); + let mappedRoomId = this.roomIdForCall(call); call.on(CallEvent.Error, (err: CallError) => { if (!this.matchesCallForThisRoom(call)) return; @@ -871,6 +871,12 @@ export default class CallHandler extends EventEmitter { case Action.DialNumber: this.dialNumber(payload.number); break; + case Action.TransferCallToMatrixID: + this.startTransferToMatrixID(payload.call, payload.destination, payload.consultFirst); + break; + case Action.TransferCallToPhoneNumber: + this.startTransferToPhoneNumber(payload.call, payload.destination, payload.consultFirst); + break; } }; @@ -905,6 +911,48 @@ export default class CallHandler extends EventEmitter { }); } + private async startTransferToPhoneNumber(call: MatrixCall, destination: string, consultFirst: boolean) { + const results = await this.pstnLookup(destination); + if (!results || results.length === 0 || !results[0].userid) { + Modal.createTrackedDialog('', '', ErrorDialog, { + title: _t("Unable to transfer call"), + description: _t("There was an error looking up the phone number"), + }); + return; + } + + await this.startTransferToMatrixID(call, results[0].userid, consultFirst); + } + + private async startTransferToMatrixID(call: MatrixCall, destination: string, consultFirst: boolean) { + if (consultFirst) { + const dmRoomId = await ensureDMExists(MatrixClientPeg.get(), destination); + + dis.dispatch({ + action: 'place_call', + type: call.type, + room_id: dmRoomId, + transferee: call, + }); + dis.dispatch({ + action: 'view_room', + room_id: dmRoomId, + should_peek: false, + joining: false, + }); + } else { + try { + await call.transfer(destination); + } catch (e) { + console.log("Failed to transfer call", e); + Modal.createTrackedDialog('Failed to transfer call', '', ErrorDialog, { + title: _t('Transfer Failed'), + description: _t('Failed to transfer call'), + }); + } + } + } + setActiveCallRoomId(activeCallRoomId: string) { logger.info("Setting call in room " + activeCallRoomId + " active"); diff --git a/src/ContentMessages.tsx b/src/ContentMessages.tsx index b752886b8a..0c65a7bd35 100644 --- a/src/ContentMessages.tsx +++ b/src/ContentMessages.tsx @@ -17,7 +17,6 @@ limitations under the License. */ import React from "react"; -import { encode } from "blurhash"; import { MatrixClient } from "matrix-js-sdk/src/client"; import dis from './dispatcher/dispatcher'; @@ -28,7 +27,6 @@ import RoomViewStore from './stores/RoomViewStore'; import encrypt from "browser-encrypt-attachment"; import extractPngChunks from "png-chunks-extract"; import Spinner from "./components/views/elements/Spinner"; - import { Action } from "./dispatcher/actions"; import CountlyAnalytics from "./CountlyAnalytics"; import { @@ -40,6 +38,7 @@ import { } from "./dispatcher/payloads/UploadPayload"; import { IUpload } from "./models/IUpload"; import { IAbortablePromise, IImageInfo } from "matrix-js-sdk/src/@types/partials"; +import { BlurhashEncoder } from "./BlurhashEncoder"; const MAX_WIDTH = 800; const MAX_HEIGHT = 600; @@ -103,55 +102,62 @@ interface IThumbnail { * @return {Promise} A promise that resolves with an object with an info key * and a thumbnail key. */ -function createThumbnail( +async function createThumbnail( element: ThumbnailableElement, inputWidth: number, inputHeight: number, mimeType: string, ): Promise { - return new Promise((resolve) => { - let targetWidth = inputWidth; - let targetHeight = inputHeight; - if (targetHeight > MAX_HEIGHT) { - targetWidth = Math.floor(targetWidth * (MAX_HEIGHT / targetHeight)); - targetHeight = MAX_HEIGHT; - } - if (targetWidth > MAX_WIDTH) { - targetHeight = Math.floor(targetHeight * (MAX_WIDTH / targetWidth)); - targetWidth = MAX_WIDTH; - } + let targetWidth = inputWidth; + let targetHeight = inputHeight; + if (targetHeight > MAX_HEIGHT) { + targetWidth = Math.floor(targetWidth * (MAX_HEIGHT / targetHeight)); + targetHeight = MAX_HEIGHT; + } + if (targetWidth > MAX_WIDTH) { + targetHeight = Math.floor(targetHeight * (MAX_WIDTH / targetWidth)); + targetWidth = MAX_WIDTH; + } - const canvas = document.createElement("canvas"); + let canvas: HTMLCanvasElement | OffscreenCanvas; + if (window.OffscreenCanvas) { + canvas = new window.OffscreenCanvas(targetWidth, targetHeight); + } else { + canvas = document.createElement("canvas"); canvas.width = targetWidth; canvas.height = targetHeight; - const context = canvas.getContext("2d"); - context.drawImage(element, 0, 0, targetWidth, targetHeight); - const imageData = context.getImageData(0, 0, targetWidth, targetHeight); - const blurhash = encode( - imageData.data, - imageData.width, - imageData.height, - // use 4 components on the longer dimension, if square then both - imageData.width >= imageData.height ? 4 : 3, - imageData.height >= imageData.width ? 4 : 3, - ); - canvas.toBlob(function(thumbnail) { - resolve({ - info: { - thumbnail_info: { - w: targetWidth, - h: targetHeight, - mimetype: thumbnail.type, - size: thumbnail.size, - }, - w: inputWidth, - h: inputHeight, - [BLURHASH_FIELD]: blurhash, - }, - thumbnail, - }); - }, mimeType); - }); + } + + const context = canvas.getContext("2d"); + context.drawImage(element, 0, 0, targetWidth, targetHeight); + + let thumbnailPromise: Promise; + + if (window.OffscreenCanvas) { + thumbnailPromise = (canvas as OffscreenCanvas).convertToBlob({ type: mimeType }); + } else { + thumbnailPromise = new Promise(resolve => (canvas as HTMLCanvasElement).toBlob(resolve, mimeType)); + } + + const imageData = context.getImageData(0, 0, targetWidth, targetHeight); + // thumbnailPromise and blurhash promise are being awaited concurrently + const blurhash = await BlurhashEncoder.instance.getBlurhash(imageData); + const thumbnail = await thumbnailPromise; + + return { + info: { + thumbnail_info: { + w: targetWidth, + h: targetHeight, + mimetype: thumbnail.type, + size: thumbnail.size, + }, + w: inputWidth, + h: inputHeight, + [BLURHASH_FIELD]: blurhash, + }, + thumbnail, + }; } /** diff --git a/src/IdentityAuthClient.js b/src/IdentityAuthClient.js index 31a5021317..447c5edd30 100644 --- a/src/IdentityAuthClient.js +++ b/src/IdentityAuthClient.js @@ -127,7 +127,7 @@ export default class IdentityAuthClient { await this._matrixClient.getIdentityAccount(token); } catch (e) { if (e.errcode === "M_TERMS_NOT_SIGNED") { - console.log("Identity Server requires new terms to be agreed to"); + console.log("Identity server requires new terms to be agreed to"); await startTermsFlow([new Service( SERVICE_TYPES.IS, identityServerUrl, diff --git a/src/components/structures/InteractiveAuth.js b/src/components/structures/InteractiveAuth.js index 9ff830f66a..61ae1882df 100644 --- a/src/components/structures/InteractiveAuth.js +++ b/src/components/structures/InteractiveAuth.js @@ -54,7 +54,7 @@ export default class InteractiveAuthComponent extends React.Component { // * emailSid {string} If email auth was performed, the sid of // the auth session. // * clientSecret {string} The client secret used in auth - // sessions with the ID server. + // sessions with the identity server. onAuthFinished: PropTypes.func.isRequired, // Inputs provided by the user to the auth process diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index 4cb1049546..b3d869cd91 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -561,7 +561,7 @@ export default class MatrixChat extends React.PureComponent { switch (payload.action) { case 'MatrixActions.accountData': // XXX: This is a collection of several hacks to solve a minor problem. We want to - // update our local state when the ID server changes, but don't want to put that in + // update our local state when the identity server changes, but don't want to put that in // the js-sdk as we'd be then dictating how all consumers need to behave. However, // this component is already bloated and we probably don't want this tiny logic in // here, but there's no better place in the react-sdk for it. Additionally, we're diff --git a/src/components/structures/TabbedView.tsx b/src/components/structures/TabbedView.tsx index dcfde94811..19694cd769 100644 --- a/src/components/structures/TabbedView.tsx +++ b/src/components/structures/TabbedView.tsx @@ -20,6 +20,7 @@ import * as React from "react"; import { _t } from '../../languageHandler'; import AutoHideScrollbar from './AutoHideScrollbar'; import { replaceableComponent } from "../../utils/replaceableComponent"; +import classNames from "classnames"; import AccessibleButton from "../views/elements/AccessibleButton"; /** @@ -37,9 +38,16 @@ export class Tab { } } +export enum TabLocation { + LEFT = 'left', + TOP = 'top', +} + interface IProps { tabs: Tab[]; initialTabId?: string; + tabLocation: TabLocation; + onChange?: (tabId: string) => void; } interface IState { @@ -62,6 +70,10 @@ export default class TabbedView extends React.Component { }; } + static defaultProps = { + tabLocation: TabLocation.LEFT, + }; + private _getActiveTabIndex() { if (!this.state || !this.state.activeTabIndex) return 0; return this.state.activeTabIndex; @@ -75,6 +87,7 @@ export default class TabbedView extends React.Component { private _setActiveTab(tab: Tab) { const idx = this.props.tabs.indexOf(tab); if (idx !== -1) { + if (this.props.onChange) this.props.onChange(tab.id); this.setState({ activeTabIndex: idx }); } else { console.error("Could not find tab " + tab.label + " in tabs"); @@ -119,8 +132,14 @@ export default class TabbedView extends React.Component { const labels = this.props.tabs.map(tab => this._renderTabLabel(tab)); const panel = this._renderTabPanel(this.props.tabs[this._getActiveTabIndex()]); + const tabbedViewClasses = classNames({ + 'mx_TabbedView': true, + 'mx_TabbedView_tabsOnLeft': this.props.tabLocation == TabLocation.LEFT, + 'mx_TabbedView_tabsOnTop': this.props.tabLocation == TabLocation.TOP, + }); + return ( -
+
{labels}
diff --git a/src/components/views/auth/InteractiveAuthEntryComponents.tsx b/src/components/views/auth/InteractiveAuthEntryComponents.tsx index 4b1ecec740..d9af2c2b77 100644 --- a/src/components/views/auth/InteractiveAuthEntryComponents.tsx +++ b/src/components/views/auth/InteractiveAuthEntryComponents.tsx @@ -41,7 +41,7 @@ import CaptchaForm from "./CaptchaForm"; * one HS whilst beign a guest on another). * loginType: the login type of the auth stage being attempted * authSessionId: session id from the server - * clientSecret: The client secret in use for ID server auth sessions + * clientSecret: The client secret in use for identity server auth sessions * stageParams: params from the server for the stage being attempted * errorText: error message from a previous attempt to authenticate * submitAuthDict: a function which will be called with the new auth dict @@ -54,8 +54,8 @@ import CaptchaForm from "./CaptchaForm"; * Defined keys for stages are: * m.login.email.identity: * * emailSid: string representing the sid of the active - * verification session from the ID server, or - * null if no session is active. + * verification session from the identity server, + * or null if no session is active. * fail: a function which should be called with an error object if an * error occurred during the auth stage. This will cause the auth * session to be failed and the process to go back to the start. diff --git a/src/components/views/context_menus/CallContextMenu.tsx b/src/components/views/context_menus/CallContextMenu.tsx index 428e18ed30..76e1670669 100644 --- a/src/components/views/context_menus/CallContextMenu.tsx +++ b/src/components/views/context_menus/CallContextMenu.tsx @@ -53,7 +53,7 @@ export default class CallContextMenu extends React.Component { onTransferClick = () => { Modal.createTrackedDialog( 'Transfer Call', '', InviteDialog, { kind: KIND_CALL_TRANSFER, call: this.props.call }, - /*className=*/null, /*isPriority=*/false, /*isStatic=*/true, + /*className=*/"mx_InviteDialog_transferWrapper", /*isPriority=*/false, /*isStatic=*/true, ); this.props.onFinished(); }; diff --git a/src/components/views/context_menus/DialpadContextMenu.tsx b/src/components/views/context_menus/DialpadContextMenu.tsx index 28a73ba8d4..39dfd50795 100644 --- a/src/components/views/context_menus/DialpadContextMenu.tsx +++ b/src/components/views/context_menus/DialpadContextMenu.tsx @@ -15,11 +15,11 @@ limitations under the License. */ import React from 'react'; -import { _t } from '../../../languageHandler'; +import AccessibleButton from "../elements/AccessibleButton"; import { ContextMenu, IProps as IContextMenuProps } from '../../structures/ContextMenu'; import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call'; import Field from "../elements/Field"; -import Dialpad from '../voip/DialPad'; +import DialPad from '../voip/DialPad'; import { replaceableComponent } from "../../../utils/replaceableComponent"; interface IProps extends IContextMenuProps { @@ -45,24 +45,29 @@ export default class DialpadContextMenu extends React.Component this.setState({ value: this.state.value + digit }); }; + onCancelClick = () => { + this.props.onFinished(); + }; + onChange = (ev) => { this.setState({ value: ev.target.value }); }; render() { return -
+
- {_t("Dial pad")} + +
+
+ +
+
+
- -
-
-
-
; } diff --git a/src/components/views/dialogs/IntegrationsImpossibleDialog.js b/src/components/views/dialogs/IntegrationsImpossibleDialog.js index 2cf9daa7ea..30b6904f27 100644 --- a/src/components/views/dialogs/IntegrationsImpossibleDialog.js +++ b/src/components/views/dialogs/IntegrationsImpossibleDialog.js @@ -46,7 +46,7 @@ export default class IntegrationsImpossibleDialog extends React.Component {

{_t( - "Your %(brand)s doesn't allow you to use an Integration Manager to do this. " + + "Your %(brand)s doesn't allow you to use an integration manager to do this. " + "Please contact an admin.", { brand }, )} diff --git a/src/components/views/dialogs/InviteDialog.tsx b/src/components/views/dialogs/InviteDialog.tsx index 2aa14449df..58bab511bf 100644 --- a/src/components/views/dialogs/InviteDialog.tsx +++ b/src/components/views/dialogs/InviteDialog.tsx @@ -32,7 +32,6 @@ import Modal from "../../../Modal"; import { humanizeTime } from "../../../utils/humanize"; import createRoom, { canEncryptToAllUsers, - ensureDMExists, findDMForUser, privateShouldBeEncrypted, } from "../../../createRoom"; @@ -64,9 +63,14 @@ import { copyPlaintext, selectText } from "../../../utils/strings"; import * as ContextMenu from "../../structures/ContextMenu"; import { toRightOf } from "../../structures/ContextMenu"; import GenericTextContextMenu from "../context_menus/GenericTextContextMenu"; +import { TransferCallPayload } from '../../../dispatcher/payloads/TransferCallPayload'; +import Field from '../elements/Field'; +import TabbedView, { Tab, TabLocation } from '../../structures/TabbedView'; +import Dialpad from '../voip/DialPad'; import QuestionDialog from "./QuestionDialog"; import Spinner from "../elements/Spinner"; import BaseDialog from "./BaseDialog"; +import DialPadBackspaceButton from "../elements/DialPadBackspaceButton"; import SpaceStore from "../../../stores/SpaceStore"; // we have a number of types defined from the Matrix spec which can't reasonably be altered here. @@ -80,11 +84,19 @@ interface IRecentUser { export const KIND_DM = "dm"; export const KIND_INVITE = "invite"; +// NB. This dialog needs the 'mx_InviteDialog_transferWrapper' wrapper class to have the correct +// padding on the bottom (because all modals have 24px padding on all sides), so this needs to +// be passed when creating the modal export const KIND_CALL_TRANSFER = "call_transfer"; const INITIAL_ROOMS_SHOWN = 3; // Number of rooms to show at first const INCREMENT_ROOMS_SHOWN = 5; // Number of rooms to add when 'show more' is clicked +enum TabId { + UserDirectory = 'users', + DialPad = 'dialpad', +} + // This is the interface that is expected by various components in the Invite Dialog and RoomInvite. // It is a bit awkward because it also matches the RoomMember class from the js-sdk with some extra support // for 3PIDs/email addresses. @@ -357,6 +369,8 @@ interface IInviteDialogState { canUseIdentityServer: boolean; tryingIdentityServer: boolean; consultFirst: boolean; + dialPadValue: string; + currentTabId: TabId; // These two flags are used for the 'Go' button to communicate what is going on. busy: boolean; @@ -408,6 +422,8 @@ export default class InviteDialog extends React.PureComponent { - this.convertFilter(); - const targets = this.convertFilter(); - const targetIds = targets.map(t => t.userId); - if (targetIds.length > 1) { - this.setState({ - errorText: _t("A call can only be transferred to a single user."), - }); - } - - if (this.state.consultFirst) { - const dmRoomId = await ensureDMExists(MatrixClientPeg.get(), targetIds[0]); - - dis.dispatch({ - action: 'place_call', - type: this.props.call.type, - room_id: dmRoomId, - transferee: this.props.call, - }); - dis.dispatch({ - action: 'view_room', - room_id: dmRoomId, - should_peek: false, - joining: false, - }); - this.props.onFinished(); - } else { - this.setState({ busy: true }); - try { - await this.props.call.transfer(targetIds[0]); - this.setState({ busy: false }); - this.props.onFinished(); - } catch (e) { + if (this.state.currentTabId == TabId.UserDirectory) { + this.convertFilter(); + const targets = this.convertFilter(); + const targetIds = targets.map(t => t.userId); + if (targetIds.length > 1) { this.setState({ - busy: false, - errorText: _t("Failed to transfer call"), + errorText: _t("A call can only be transferred to a single user."), }); + return; } + + dis.dispatch({ + action: Action.TransferCallToMatrixID, + call: this.props.call, + destination: targetIds[0], + consultFirst: this.state.consultFirst, + } as TransferCallPayload); + } else { + dis.dispatch({ + action: Action.TransferCallToPhoneNumber, + call: this.props.call, + destination: this.state.dialPadValue, + consultFirst: this.state.consultFirst, + } as TransferCallPayload); } + this.props.onFinished(); }; private onKeyDown = (e) => { @@ -828,6 +832,10 @@ export default class InviteDialog extends React.PureComponent { + this.props.onFinished([]); + }; + private updateSuggestions = async (term) => { MatrixClientPeg.get().searchUserDirectory({ term }).then(async r => { if (term !== this.state.filterText) { @@ -963,11 +971,14 @@ export default class InviteDialog extends React.PureComponent { if (!this.state.busy) { let filterText = this.state.filterText; - const targets = this.state.targets.map(t => t); // cheap clone for mutation + let targets = this.state.targets.map(t => t); // cheap clone for mutation const idx = targets.indexOf(member); if (idx >= 0) { targets.splice(idx, 1); } else { + if (this.props.kind === KIND_CALL_TRANSFER && targets.length > 0) { + targets = []; + } targets.push(member); filterText = ""; // clear the filter when the user accepts a suggestion } @@ -1190,6 +1201,11 @@ export default class InviteDialog extends React.PureComponent ( )); @@ -1202,8 +1218,9 @@ export default class InviteDialog extends React.PureComponent 0)} autoComplete="off" + placeholder={hasPlaceholder ? _t("Search") : null} /> ); return ( @@ -1250,6 +1267,28 @@ export default class InviteDialog extends React.PureComponent { + ev.preventDefault(); + this.transferCall(); + }; + + private onDialChange = ev => { + this.setState({ dialPadValue: ev.currentTarget.value }); + }; + + private onDigitPress = digit => { + this.setState({ dialPadValue: this.state.dialPadValue + digit }); + }; + + private onDeletePress = () => { + if (this.state.dialPadValue.length === 0) return; + this.setState({ dialPadValue: this.state.dialPadValue.slice(0, -1) }); + }; + + private onTabChange = (tabId: TabId) => { + this.setState({ currentTabId: tabId }); + }; + private async onLinkClick(e) { e.preventDefault(); selectText(e.target); @@ -1279,12 +1318,16 @@ export default class InviteDialog extends React.PureComponent; const identityServersEnabled = SettingsStore.getValue(UIFeature.IdentityServer); + const hasSelection = this.state.targets.length > 0 + || (this.state.filterText && this.state.filterText.includes('@')); + const cli = MatrixClientPeg.get(); const userId = cli.getUserId(); if (this.props.kind === KIND_DM) { @@ -1422,23 +1465,116 @@ export default class InviteDialog extends React.PureComponent + + consultConnectSection =

+ + {_t("Cancel")} + + + {_t("Transfer")} +
; } else { console.error("Unknown kind of InviteDialog: " + this.props.kind); } - const hasSelection = this.state.targets.length > 0 - || (this.state.filterText && this.state.filterText.includes('@')); + const goButton = this.props.kind == KIND_CALL_TRANSFER ? null : + {buttonText} + ; + + const usersSection = +

{helpText}

+
+ {this.renderEditor()} +
+ {goButton} + {spinner} +
+
+ {keySharingWarning} + {this.renderIdentityServerWarning()} +
{this.state.errorText}
+
+ {this.renderSection('recents')} + {this.renderSection('suggestions')} + {extraSection} +
+ {footer} +
; + + let dialogContent; + if (this.props.kind === KIND_CALL_TRANSFER) { + const tabs = []; + tabs.push(new Tab( + TabId.UserDirectory, _td("User Directory"), 'mx_InviteDialog_userDirectoryIcon', usersSection, + )); + + const backspaceButton = ( + + ); + + // Only show the backspace button if the field has content + let dialPadField; + if (this.state.dialPadValue.length !== 0) { + dialPadField = ; + } else { + dialPadField = ; + } + + const dialPadSection =
+
+ {dialPadField} +
+ +
; + tabs.push(new Tab(TabId.DialPad, _td("Dial pad"), 'mx_InviteDialog_dialPadIcon', dialPadSection)); + dialogContent = + + {consultConnectSection} + ; + } else { + dialogContent = + {usersSection} + {consultConnectSection} + ; + } + return (
-

{helpText}

-
- {this.renderEditor()} -
- - {buttonText} - - {spinner} -
-
- {keySharingWarning} - {this.renderIdentityServerWarning()} -
{this.state.errorText}
-
- {this.renderSection('recents')} - {this.renderSection('suggestions')} - {extraSection} -
- {footer} + {dialogContent}
); diff --git a/src/components/views/dialogs/TermsDialog.tsx b/src/components/views/dialogs/TermsDialog.tsx index afa732033f..58126f77c3 100644 --- a/src/components/views/dialogs/TermsDialog.tsx +++ b/src/components/views/dialogs/TermsDialog.tsx @@ -90,9 +90,9 @@ export default class TermsDialog extends React.PureComponent{_t("Identity Server")}
({host})
; + return
{_t("Identity server")}
({host})
; case SERVICE_TYPES.IM: - return
{_t("Integration Manager")}
({host})
; + return
{_t("Integration manager")}
({host})
; } } diff --git a/src/components/views/elements/AppPermission.js b/src/components/views/elements/AppPermission.js index 152d3c6b95..c1f370b626 100644 --- a/src/components/views/elements/AppPermission.js +++ b/src/components/views/elements/AppPermission.js @@ -114,7 +114,7 @@ export default class AppPermission extends React.Component { // Due to i18n limitations, we can't dedupe the code for variables in these two messages. const warning = this.state.isWrapped - ? _t("Using this widget may share data with %(widgetDomain)s & your Integration Manager.", + ? _t("Using this widget may share data with %(widgetDomain)s & your integration manager.", { widgetDomain: this.state.widgetDomain }, { helpIcon: () => warningTooltip }) : _t("Using this widget may share data with %(widgetDomain)s.", { widgetDomain: this.state.widgetDomain }, { helpIcon: () => warningTooltip }); diff --git a/src/components/views/elements/DialPadBackspaceButton.tsx b/src/components/views/elements/DialPadBackspaceButton.tsx new file mode 100644 index 0000000000..69f0fcb39a --- /dev/null +++ b/src/components/views/elements/DialPadBackspaceButton.tsx @@ -0,0 +1,31 @@ +/* +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 * as React from "react"; +import AccessibleButton from "./AccessibleButton"; + +interface IProps { + // Callback for when the button is pressed + onBackspacePress: () => void; +} + +export default class DialPadBackspaceButton extends React.PureComponent { + render() { + return
+ +
; + } +} diff --git a/src/components/views/rooms/Stickerpicker.js b/src/components/views/rooms/Stickerpicker.js index a66186d116..c0e6826ba5 100644 --- a/src/components/views/rooms/Stickerpicker.js +++ b/src/components/views/rooms/Stickerpicker.js @@ -224,7 +224,7 @@ export default class Stickerpicker extends React.PureComponent { } _getStickerpickerContent() { - // Handle Integration Manager errors + // Handle integration manager errors if (this.state._imError) { return this._errorStickerpickerContent(); } diff --git a/src/components/views/settings/SetIdServer.tsx b/src/components/views/settings/SetIdServer.tsx index 9180c98101..dc38055c10 100644 --- a/src/components/views/settings/SetIdServer.tsx +++ b/src/components/views/settings/SetIdServer.tsx @@ -44,7 +44,7 @@ const REACHABILITY_TIMEOUT = 10000; // ms async function checkIdentityServerUrl(u) { const parsedUrl = url.parse(u); - if (parsedUrl.protocol !== 'https:') return _t("Identity Server URL must be HTTPS"); + if (parsedUrl.protocol !== 'https:') return _t("Identity server URL must be HTTPS"); // XXX: duplicated logic from js-sdk but it's quite tied up in the validation logic in the // js-sdk so probably as easy to duplicate it than to separate it out so we can reuse it @@ -53,17 +53,17 @@ async function checkIdentityServerUrl(u) { if (response.ok) { return null; } else if (response.status < 200 || response.status >= 300) { - return _t("Not a valid Identity Server (status code %(code)s)", { code: response.status }); + return _t("Not a valid identity server (status code %(code)s)", { code: response.status }); } else { - return _t("Could not connect to Identity Server"); + return _t("Could not connect to identity server"); } } catch (e) { - return _t("Could not connect to Identity Server"); + return _t("Could not connect to identity server"); } } interface IProps { - // Whether or not the ID server is missing terms. This affects the text + // Whether or not the identity server is missing terms. This affects the text // shown to the user. missingTerms: boolean; } @@ -87,7 +87,7 @@ export default class SetIdServer extends React.Component { let defaultIdServer = ''; if (!MatrixClientPeg.get().getIdentityServerUrl() && getDefaultIdentityServerUrl()) { - // If no ID server is configured but there's one in the config, prepopulate + // If no identity server is configured but there's one in the config, prepopulate // the field to help the user. defaultIdServer = abbreviateUrl(getDefaultIdentityServerUrl()); } @@ -112,7 +112,7 @@ export default class SetIdServer extends React.Component { } private onAction = (payload: ActionPayload) => { - // We react to changes in the ID server in the event the user is staring at this form + // We react to changes in the identity server in the event the user is staring at this form // when changing their identity server on another device. if (payload.action !== "id_server_changed") return; @@ -356,7 +356,7 @@ export default class SetIdServer extends React.Component { let sectionTitle; let bodyText; if (idServerUrl) { - sectionTitle = _t("Identity Server (%(server)s)", { server: abbreviateUrl(idServerUrl) }); + sectionTitle = _t("Identity server (%(server)s)", { server: abbreviateUrl(idServerUrl) }); bodyText = _t( "You are currently using to discover and be discoverable by " + "existing contacts you know. You can change your identity server below.", @@ -371,7 +371,7 @@ export default class SetIdServer extends React.Component { ); } } else { - sectionTitle = _t("Identity Server"); + sectionTitle = _t("Identity server"); bodyText = _t( "You are not currently using an identity server. " + "To discover and be discoverable by existing contacts you know, " + diff --git a/src/components/views/settings/SetIntegrationManager.tsx b/src/components/views/settings/SetIntegrationManager.tsx index ada78e2848..f1922f93ee 100644 --- a/src/components/views/settings/SetIntegrationManager.tsx +++ b/src/components/views/settings/SetIntegrationManager.tsx @@ -65,13 +65,13 @@ export default class SetIntegrationManager extends React.Component(%(serverName)s) to manage bots, widgets, " + + "Use an integration manager (%(serverName)s) to manage bots, widgets, " + "and sticker packs.", { serverName: currentManager.name }, { b: sub => {sub} }, ); } else { - bodyText = _t("Use an Integration Manager to manage bots, widgets, and sticker packs."); + bodyText = _t("Use an integration manager to manage bots, widgets, and sticker packs."); } return ( @@ -86,7 +86,7 @@ export default class SetIntegrationManager extends React.Component
{_t( - "Integration Managers receive configuration data, and can modify widgets, " + + "Integration managers receive configuration data, and can modify widgets, " + "send room invites, and set power levels on your behalf.", )} diff --git a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js index 44ddaf08e4..f1b7df3eb5 100644 --- a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js @@ -364,7 +364,7 @@ export default class GeneralUserSettingsTab extends React.Component { onFinished={this.state.requiredPolicyInfo.resolve} introElement={intro} /> - { /* has its own heading as it includes the current ID server */ } + { /* has its own heading as it includes the current identity server */ }
); @@ -387,7 +387,7 @@ export default class GeneralUserSettingsTab extends React.Component { return (
{threepidSection} - { /* has its own heading as it includes the current ID server */ } + { /* has its own heading as it includes the current identity server */ }
); diff --git a/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx b/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx index 608d973992..f2857720a5 100644 --- a/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx @@ -290,7 +290,7 @@ export default class HelpUserSettingsTab extends React.Component {_t("Advanced")}
{_t("Homeserver is")} {MatrixClientPeg.get().getHomeserverUrl()}
- {_t("Identity Server is")} {MatrixClientPeg.get().getIdentityServerUrl()}
+ {_t("Identity server is")} {MatrixClientPeg.get().getIdentityServerUrl()}

{_t("Access Token")}
diff --git a/src/components/views/voip/DialPad.tsx b/src/components/views/voip/DialPad.tsx index dff7a8f748..6687c89b52 100644 --- a/src/components/views/voip/DialPad.tsx +++ b/src/components/views/voip/DialPad.tsx @@ -19,16 +19,17 @@ import AccessibleButton from "../elements/AccessibleButton"; import { replaceableComponent } from "../../../utils/replaceableComponent"; const BUTTONS = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '*', '0', '#']; +const BUTTON_LETTERS = ['', 'ABC', 'DEF', 'GHI', 'JKL', 'MNO', 'PQRS', 'TUV', 'WXYZ', '', '+', '']; enum DialPadButtonKind { Digit, - Delete, Dial, } interface IButtonProps { kind: DialPadButtonKind; digit?: string; + digitSubtext?: string; onButtonPress: (string) => void; } @@ -42,11 +43,10 @@ class DialPadButton extends React.PureComponent { case DialPadButtonKind.Digit: return {this.props.digit} +
+ {this.props.digitSubtext} +
; - case DialPadButtonKind.Delete: - return ; case DialPadButtonKind.Dial: return ; } @@ -55,7 +55,7 @@ class DialPadButton extends React.PureComponent { interface IProps { onDigitPress: (string) => void; - hasDialAndDelete: boolean; + hasDial: boolean; onDeletePress?: (string) => void; onDialPress?: (string) => void; } @@ -65,16 +65,15 @@ export default class Dialpad extends React.PureComponent { render() { const buttonNodes = []; - for (const button of BUTTONS) { + for (let i = 0; i < BUTTONS.length; i++) { + const button = BUTTONS[i]; + const digitSubtext = BUTTON_LETTERS[i]; buttonNodes.push(); } - if (this.props.hasDialAndDelete) { - buttonNodes.push(); + if (this.props.hasDial) { buttonNodes.push(); diff --git a/src/components/views/voip/DialPadModal.tsx b/src/components/views/voip/DialPadModal.tsx index 5e5903531e..033aa2e700 100644 --- a/src/components/views/voip/DialPadModal.tsx +++ b/src/components/views/voip/DialPadModal.tsx @@ -15,7 +15,6 @@ limitations under the License. */ import * as React from "react"; -import { _t } from "../../../languageHandler"; import AccessibleButton from "../elements/AccessibleButton"; import Field from "../elements/Field"; import DialPad from './DialPad'; @@ -23,6 +22,7 @@ import dis from '../../../dispatcher/dispatcher'; import { replaceableComponent } from "../../../utils/replaceableComponent"; import { DialNumberPayload } from "../../../dispatcher/payloads/DialNumberPayload"; import { Action } from "../../../dispatcher/actions"; +import DialPadBackspaceButton from "../elements/DialPadBackspaceButton"; interface IProps { onFinished: (boolean) => void; @@ -74,22 +74,38 @@ export default class DialpadModal extends React.PureComponent { }; render() { + const backspaceButton = ( + + ); + + // Only show the backspace button if the field has content + let dialPadField; + if (this.state.value.length !== 0) { + dialPadField = ; + } else { + dialPadField = ; + } + return
+
+ +
-
- {_t("Dial pad")} - -
- + {dialPadField}
-
- and connect to instead?": "Disconnect from the identity server and connect to instead?", @@ -1221,20 +1223,20 @@ "Disconnect anyway": "Disconnect anyway", "You are still sharing your personal data on the identity server .": "You are still sharing your personal data on the identity server .", "We recommend that you remove your email addresses and phone numbers from the identity server before disconnecting.": "We recommend that you remove your email addresses and phone numbers from the identity server before disconnecting.", - "Identity Server (%(server)s)": "Identity Server (%(server)s)", + "Identity server (%(server)s)": "Identity server (%(server)s)", "You are currently using to discover and be discoverable by existing contacts you know. You can change your identity server below.": "You are currently using to discover and be discoverable by existing contacts you know. You can change your identity server below.", "If you don't want to use to discover and be discoverable by existing contacts you know, enter another identity server below.": "If you don't want to use to discover and be discoverable by existing contacts you know, enter another identity server below.", - "Identity Server": "Identity Server", + "Identity server": "Identity server", "You are not currently using an identity server. To discover and be discoverable by existing contacts you know, add one below.": "You are not currently using an identity server. To discover and be discoverable by existing contacts you know, add one below.", "Disconnecting from your identity server will mean you won't be discoverable by other users and you won't be able to invite others by email or phone.": "Disconnecting from your identity server will mean you won't be discoverable by other users and you won't be able to invite others by email or phone.", "Using an identity server is optional. If you choose not to use an identity server, you won't be discoverable by other users and you won't be able to invite others by email or phone.": "Using an identity server is optional. If you choose not to use an identity server, you won't be discoverable by other users and you won't be able to invite others by email or phone.", "Do not use an identity server": "Do not use an identity server", "Enter a new identity server": "Enter a new identity server", "Change": "Change", - "Use an Integration Manager (%(serverName)s) to manage bots, widgets, and sticker packs.": "Use an Integration Manager (%(serverName)s) to manage bots, widgets, and sticker packs.", - "Use an Integration Manager to manage bots, widgets, and sticker packs.": "Use an Integration Manager to manage bots, widgets, and sticker packs.", + "Use an integration manager (%(serverName)s) to manage bots, widgets, and sticker packs.": "Use an integration manager (%(serverName)s) to manage bots, widgets, and sticker packs.", + "Use an integration manager to manage bots, widgets, and sticker packs.": "Use an integration manager to manage bots, widgets, and sticker packs.", "Manage integrations": "Manage integrations", - "Integration Managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "Integration Managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.", + "Integration managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "Integration managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.", "Add": "Add", "Error encountered (%(errorDetail)s).": "Error encountered (%(errorDetail)s).", "Checking for an update...": "Checking for an update...", @@ -1288,7 +1290,7 @@ "%(brand)s version:": "%(brand)s version:", "olm version:": "olm version:", "Homeserver is": "Homeserver is", - "Identity Server is": "Identity Server is", + "Identity server is": "Identity server is", "Access Token": "Access Token", "Your access token gives full access to your account. Do not share it with anyone.": "Your access token gives full access to your account. Do not share it with anyone.", "Copy": "Copy", @@ -1967,7 +1969,7 @@ "%(brand)s URL": "%(brand)s URL", "Room ID": "Room ID", "Widget ID": "Widget ID", - "Using this widget may share data with %(widgetDomain)s & your Integration Manager.": "Using this widget may share data with %(widgetDomain)s & your Integration Manager.", + "Using this widget may share data with %(widgetDomain)s & your integration manager.": "Using this widget may share data with %(widgetDomain)s & your integration manager.", "Using this widget may share data with %(widgetDomain)s.": "Using this widget may share data with %(widgetDomain)s.", "Widgets do not use message encryption.": "Widgets do not use message encryption.", "Widget added by": "Widget added by", @@ -2285,7 +2287,7 @@ "Integrations are disabled": "Integrations are disabled", "Enable 'Manage Integrations' in Settings to do this.": "Enable 'Manage Integrations' in Settings to do this.", "Integrations not allowed": "Integrations not allowed", - "Your %(brand)s doesn't allow you to use an Integration Manager to do this. Please contact an admin.": "Your %(brand)s doesn't allow you to use an Integration Manager to do this. Please contact an admin.", + "Your %(brand)s doesn't allow you to use an integration manager to do this. Please contact an admin.": "Your %(brand)s doesn't allow you to use an integration manager to do this. Please contact an admin.", "To continue, use Single Sign On to prove your identity.": "To continue, use Single Sign On to prove your identity.", "Confirm to continue": "Confirm to continue", "Click the button below to confirm your identity.": "Click the button below to confirm your identity.", @@ -2294,7 +2296,6 @@ "Something went wrong trying to invite the users.": "Something went wrong trying to invite the users.", "We couldn't invite those users. Please check the users you want to invite and try again.": "We couldn't invite those users. Please check the users you want to invite and try again.", "A call can only be transferred to a single user.": "A call can only be transferred to a single user.", - "Failed to transfer call": "Failed to transfer call", "Failed to find the following users": "Failed to find the following users", "The following users might not exist or are invalid, and cannot be invited: %(csvNames)s": "The following users might not exist or are invalid, and cannot be invited: %(csvNames)s", "Recent Conversations": "Recent Conversations", @@ -2317,6 +2318,8 @@ "Invited people will be able to read old messages.": "Invited people will be able to read old messages.", "Transfer": "Transfer", "Consult first": "Consult first", + "User Directory": "User Directory", + "Dial pad": "Dial pad", "a new master key signature": "a new master key signature", "a new cross-signing key signature": "a new cross-signing key signature", "a device cross-signing signature": "a device cross-signing signature", @@ -2440,7 +2443,7 @@ "Missing session data": "Missing session data", "Some session data, including encrypted message keys, is missing. Sign out and sign in to fix this, restoring keys from backup.": "Some session data, including encrypted message keys, is missing. Sign out and sign in to fix this, restoring keys from backup.", "Your browser likely removed this data when running low on disk space.": "Your browser likely removed this data when running low on disk space.", - "Integration Manager": "Integration Manager", + "Integration manager": "Integration manager", "Find others by phone or email": "Find others by phone or email", "Be found by phone or email": "Be found by phone or email", "Use bots, bridges, widgets and sticker packs": "Use bots, bridges, widgets and sticker packs", diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx index 1751eddb2c..830ea9e32e 100644 --- a/src/settings/Settings.tsx +++ b/src/settings/Settings.tsx @@ -812,7 +812,7 @@ export const SETTINGS: {[setting: string]: ISetting} = { [UIFeature.IdentityServer]: { supportedLevels: LEVELS_UI_FEATURE, default: true, - // Identity Server (Discovery) Settings make no sense if 3PIDs in general are hidden + // Identity server (discovery) settings make no sense if 3PIDs in general are hidden controller: new UIFeatureController(UIFeature.ThirdPartyID), }, [UIFeature.ThirdPartyID]: { diff --git a/src/stores/widgets/StopGapWidgetDriver.ts b/src/stores/widgets/StopGapWidgetDriver.ts index dadedcfe68..13cd260ef0 100644 --- a/src/stores/widgets/StopGapWidgetDriver.ts +++ b/src/stores/widgets/StopGapWidgetDriver.ts @@ -159,7 +159,7 @@ export class StopGapWidgetDriver extends WidgetDriver { if (results.length >= limit) break; const ev = events[i]; - if (ev.getType() !== eventType) continue; + if (ev.getType() !== eventType || ev.isState()) continue; if (eventType === EventType.RoomMessage && msgtype && msgtype !== ev.getContent()['msgtype']) continue; results.push(ev); } diff --git a/src/utils/WidgetUtils.ts b/src/utils/WidgetUtils.ts index e27381b1cf..ea56f2a563 100644 --- a/src/utils/WidgetUtils.ts +++ b/src/utils/WidgetUtils.ts @@ -407,7 +407,7 @@ export default class WidgetUtils { "integration_manager_" + (new Date().getTime()), WidgetType.INTEGRATION_MANAGER, uiUrl, - "Integration Manager: " + name, + "Integration manager: " + name, { "api_url": apiUrl }, ); } diff --git a/src/workers/blurhash.worker.ts b/src/workers/blurhash.worker.ts new file mode 100644 index 0000000000..031cc67c90 --- /dev/null +++ b/src/workers/blurhash.worker.ts @@ -0,0 +1,38 @@ +/* +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 { encode } from "blurhash"; + +const ctx: Worker = self as any; + +interface IBlurhashWorkerRequest { + seq: number; + imageData: ImageData; +} + +ctx.addEventListener("message", (event: MessageEvent): void => { + const { seq, imageData } = event.data; + const blurhash = encode( + imageData.data, + imageData.width, + imageData.height, + // use 4 components on the longer dimension, if square then both + imageData.width >= imageData.height ? 4 : 3, + imageData.height >= imageData.width ? 4 : 3, + ); + + ctx.postMessage({ seq, blurhash }); +}); diff --git a/test/end-to-end-tests/synapse/config-templates/consent/homeserver.yaml b/test/end-to-end-tests/synapse/config-templates/consent/homeserver.yaml index deb750666f..13aea8d18d 100644 --- a/test/end-to-end-tests/synapse/config-templates/consent/homeserver.yaml +++ b/test/end-to-end-tests/synapse/config-templates/consent/homeserver.yaml @@ -572,11 +572,11 @@ uploads_path: "{{SYNAPSE_ROOT}}uploads" ## Captcha ## # See docs/CAPTCHA_SETUP for full details of configuring this. -# This Home Server's ReCAPTCHA public key. +# This homeserver's ReCAPTCHA public key. # #recaptcha_public_key: "YOUR_PUBLIC_KEY" -# This Home Server's ReCAPTCHA private key. +# This homeserver's ReCAPTCHA private key. # #recaptcha_private_key: "YOUR_PRIVATE_KEY" @@ -685,7 +685,7 @@ registration_shared_secret: "{{REGISTRATION_SHARED_SECRET}}" # The list of identity servers trusted to verify third party # identifiers by this server. # -# Also defines the ID server which will be called when an account is +# Also defines the identity server which will be called when an account is # deactivated (one will be picked arbitrarily). # #trusted_third_party_id_servers: @@ -889,7 +889,7 @@ email: smtp_user: "exampleusername" smtp_pass: "examplepassword" require_transport_security: False - notif_from: "Your Friendly %(app)s Home Server " + notif_from: "Your Friendly %(app)s homeserver " app_name: Matrix # if template_dir is unset, uses the example templates that are part of # the Synapse distribution.