From 1c2e05e925529afe71910fa43cc6257c15f6cd03 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Mon, 31 Aug 2020 14:40:16 -0400 Subject: [PATCH 001/233] initial version of device rehydration support --- src/CrossSigningManager.js | 2 +- src/Login.js | 60 +++++++++++++++++++++++++++++++++++++- src/MatrixClientPeg.ts | 28 ++++++++++++++++-- 3 files changed, 85 insertions(+), 5 deletions(-) diff --git a/src/CrossSigningManager.js b/src/CrossSigningManager.js index 676c41d7d7..43d089010c 100644 --- a/src/CrossSigningManager.js +++ b/src/CrossSigningManager.js @@ -40,7 +40,7 @@ export class AccessCancelledError extends Error { } } -async function confirmToDismiss() { +export async function confirmToDismiss() { const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); const [sure] = await Modal.createDialog(QuestionDialog, { title: _t("Cancel entering passphrase?"), diff --git a/src/Login.js b/src/Login.js index 04805b4af9..4e46fc3665 100644 --- a/src/Login.js +++ b/src/Login.js @@ -18,7 +18,12 @@ See the License for the specific language governing permissions and limitations under the License. */ +import Modal from './Modal'; +import * as sdk from './index'; +import { AccessCancelledError, confirmToDismiss } from "./CrossSigningManager"; import Matrix from "matrix-js-sdk"; +import { deriveKey } from 'matrix-js-sdk/src/crypto/key_passphrase'; +import { decodeRecoveryKey } from 'matrix-js-sdk/src/crypto/recoverykey'; export default class Login { constructor(hsUrl, isUrl, fallbackHsUrl, opts) { @@ -159,12 +164,18 @@ export default class Login { * @returns {MatrixClientCreds} */ export async function sendLoginRequest(hsUrl, isUrl, loginType, loginParams) { + let rehydrationKeyInfo; + let rehydrationKey; + const client = Matrix.createClient({ baseUrl: hsUrl, idBaseUrl: isUrl, + cryptoCallbacks: { + getDehydrationKey + } }); - const data = await client.login(loginType, loginParams); + const data = await client.loginWithRehydration(null, loginType, loginParams); const wellknown = data.well_known; if (wellknown) { @@ -185,5 +196,52 @@ export async function sendLoginRequest(hsUrl, isUrl, loginType, loginParams) { userId: data.user_id, deviceId: data.device_id, accessToken: data.access_token, + rehydrationKeyInfo, + rehydrationKey, + olmAccount: data._olm_account, }; } + +async function getDehydrationKey(keyInfo) { + const inputToKey = async ({ passphrase, recoveryKey }) => { + if (passphrase) { + return deriveKey( + passphrase, + keyInfo.passphrase.salt, + keyInfo.passphrase.iterations, + ); + } else { + return decodeRecoveryKey(recoveryKey); + } + }; + const AccessSecretStorageDialog = + sdk.getComponent("dialogs.secretstorage.AccessSecretStorageDialog"); + const { finished } = Modal.createTrackedDialog("Access Secret Storage dialog", "", + AccessSecretStorageDialog, + /* props= */ + { + keyInfo, + checkPrivateKey: async (input) => { + // FIXME: + return true; + }, + }, + /* className= */ null, + /* isPriorityModal= */ false, + /* isStaticModal= */ false, + /* options= */ { + onBeforeClose: async (reason) => { + if (reason === "backgroundClick") { + return confirmToDismiss(); + } + return true; + }, + }, + ); + const [input] = await finished; + if (!input) { + throw new AccessCancelledError(); + } + const key = await inputToKey(input); + return key; +} diff --git a/src/MatrixClientPeg.ts b/src/MatrixClientPeg.ts index be16f5fe10..61b7a04069 100644 --- a/src/MatrixClientPeg.ts +++ b/src/MatrixClientPeg.ts @@ -42,6 +42,9 @@ export interface IMatrixClientCreds { accessToken: string; guest: boolean; pickleKey?: string; + rehydrationKey?: Uint8Array; + rehydrationKeyInfo?: {[props: string]: any}; + olmAccount?: any; } // TODO: Move this to the js-sdk @@ -248,12 +251,10 @@ class _MatrixClientPeg implements IMatrixClientPeg { private createClient(creds: IMatrixClientCreds): void { // TODO: Make these opts typesafe with the js-sdk - const opts = { + const opts: any = { baseUrl: creds.homeserverUrl, idBaseUrl: creds.identityServerUrl, accessToken: creds.accessToken, - userId: creds.userId, - deviceId: creds.deviceId, pickleKey: creds.pickleKey, timelineSupport: true, forceTURN: !SettingsStore.getValue('webRtcAllowPeerToPeer'), @@ -268,6 +269,23 @@ class _MatrixClientPeg implements IMatrixClientPeg { cryptoCallbacks: {}, }; + if (creds.olmAccount) { + opts.deviceToImport = { + olmDevice: { + pickledAccount: creds.olmAccount.pickle("DEFAULT_KEY"), + sessions: [], + pickleKey: "DEFAULT_KEY", + }, + userId: creds.userId, + deviceId: creds.deviceId, + }; + } else { + opts.userId = creds.userId; + opts.deviceId = creds.deviceId; + } + + // FIXME: modify crossSigningCallbacks.getSecretStorageKey so that it tries using rehydrationkey and/or saves the passphrase info + // These are always installed regardless of the labs flag so that // cross-signing features can toggle on without reloading and also be // accessed immediately after login. @@ -275,6 +293,10 @@ class _MatrixClientPeg implements IMatrixClientPeg { this.matrixClient = createMatrixClient(opts); + if (creds.rehydrationKey) { + this.matrixClient.cacheDehydrationKey(creds.rehydrationKey, creds.rehydrationKeyInfo || {}); + } + // we're going to add eventlisteners for each matrix event tile, so the // potential number of event listeners is quite high. this.matrixClient.setMaxListeners(500); From 4398f1eb949c8f4b5b3e61c51c756bec3c9c97d8 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Thu, 3 Sep 2020 16:28:42 -0400 Subject: [PATCH 002/233] support setting up dehydration from blank account, SSO support, and other fixes --- src/CrossSigningManager.js | 26 +++++++++++ src/Lifecycle.js | 44 ++++++++++++++++++- src/Login.js | 18 +++++--- src/MatrixClientPeg.ts | 24 +++++++--- .../CreateSecretStorageDialog.js | 5 +++ 5 files changed, 103 insertions(+), 14 deletions(-) diff --git a/src/CrossSigningManager.js b/src/CrossSigningManager.js index 43d089010c..111fc26889 100644 --- a/src/CrossSigningManager.js +++ b/src/CrossSigningManager.js @@ -30,6 +30,16 @@ import {encodeBase64} from "matrix-js-sdk/src/crypto/olmlib"; let secretStorageKeys = {}; let secretStorageBeingAccessed = false; +let dehydrationInfo = {}; + +export function cacheDehydrationKey(key, keyInfo = {}) { + dehydrationInfo = {key, keyInfo}; +} + +export function getDehydrationKeyCache() { + return dehydrationInfo; +} + function isCachingAllowed() { return secretStorageBeingAccessed; } @@ -64,6 +74,22 @@ async function getSecretStorageKey({ keys: keyInfos }, ssssItemName) { return [name, secretStorageKeys[name]]; } + // if we dehydrated a device, see if that key works for SSSS + if (dehydrationInfo.key) { + try { + if (await MatrixClientPeg.get().checkSecretStorageKey(dehydrationInfo.key, info)) { + const key = dehydrationInfo.key; + // Save to cache to avoid future prompts in the current session + if (isCachingAllowed()) { + secretStorageKeys[name] = key; + } + dehydrationInfo = {}; + return [name, key]; + } + } catch {} + dehydrationInfo = {}; + } + const inputToKey = async ({ passphrase, recoveryKey }) => { if (passphrase) { return deriveKey( diff --git a/src/Lifecycle.js b/src/Lifecycle.js index d2de31eb80..9a84d4e1f4 100644 --- a/src/Lifecycle.js +++ b/src/Lifecycle.js @@ -42,6 +42,7 @@ import {Mjolnir} from "./mjolnir/Mjolnir"; import DeviceListener from "./DeviceListener"; import {Jitsi} from "./widgets/Jitsi"; import {SSO_HOMESERVER_URL_KEY, SSO_ID_SERVER_URL_KEY} from "./BasePlatform"; +import {decodeBase64, encodeBase64} from "matrix-js-sdk/src/crypto/olmlib"; const HOMESERVER_URL_KEY = "mx_hs_url"; const ID_SERVER_URL_KEY = "mx_is_url"; @@ -311,6 +312,25 @@ async function _restoreFromLocalStorage(opts) { console.log("No pickle key available"); } + const rehydrationKeyInfoJSON = sessionStorage.getItem("mx_rehydration_key_info"); + const rehydrationKeyInfo = rehydrationKeyInfoJSON && JSON.parse(rehydrationKeyInfoJSON); + const rehydrationKeyB64 = sessionStorage.getItem("mx_rehydration_key"); + const rehydrationKey = rehydrationKeyB64 && decodeBase64(rehydrationKeyB64); + const rehydrationOlmPickle = sessionStorage.getItem("mx_rehydration_account"); + let olmAccount; + if (rehydrationOlmPickle) { + olmAccount = new global.Olm.Account(); + try { + olmAccount.unpickle("DEFAULT_KEY", rehydrationOlmPickle); + } catch { + olmAccount.free(); + olmAccount = undefined; + } + } + sessionStorage.removeItem("mx_rehydration_key_info"); + sessionStorage.removeItem("mx_rehydration_key"); + sessionStorage.removeItem("mx_rehydration_account"); + console.log(`Restoring session for ${userId}`); await _doSetLoggedIn({ userId: userId, @@ -320,6 +340,9 @@ async function _restoreFromLocalStorage(opts) { identityServerUrl: isUrl, guest: isGuest, pickleKey: pickleKey, + rehydrationKey: rehydrationKey, + rehydrationKeyInfo: rehydrationKeyInfo, + olmAccount: olmAccount, }, false); return true; } else { @@ -463,7 +486,13 @@ async function _doSetLoggedIn(credentials, clearStorage) { if (localStorage) { try { - _persistCredentialsToLocalStorage(credentials); + // drop dehydration key and olm account before persisting. (Those + // get persisted for token login, but aren't needed at this point.) + const strippedCredentials = Object.assign({}, credentials); + delete strippedCredentials.rehydrationKeyInfo; + delete strippedCredentials.rehydrationKey; + delete strippedCredentials.olmAcconut; + _persistCredentialsToLocalStorage(strippedCredentials); // The user registered as a PWLU (PassWord-Less User), the generated password // is cached here such that the user can change it at a later time. @@ -528,6 +557,19 @@ function _persistCredentialsToLocalStorage(credentials) { localStorage.setItem("mx_device_id", credentials.deviceId); } + // Temporarily save dehydration information if it's provided. This is + // needed for token logins, because the page reloads after the login, so we + // can't keep it in memory. + if (credentials.rehydrationKeyInfo) { + sessionStorage.setItem("mx_rehydration_key_info", JSON.stringify(credentials.rehydrationKeyInfo)); + } + if (credentials.rehydrationKey) { + sessionStorage.setItem("mx_rehydration_key", encodeBase64(credentials.rehydrationKey)); + } + if (credentials.olmAccount) { + sessionStorage.setItem("mx_rehydration_account", credentials.olmAccount.pickle("DEFAULT_KEY")); + } + console.log(`Session persisted for ${credentials.userId}`); } diff --git a/src/Login.js b/src/Login.js index 4e46fc3665..0563952c5d 100644 --- a/src/Login.js +++ b/src/Login.js @@ -20,7 +20,12 @@ limitations under the License. import Modal from './Modal'; import * as sdk from './index'; -import { AccessCancelledError, confirmToDismiss } from "./CrossSigningManager"; +import { + AccessCancelledError, + cacheDehydrationKey, + confirmToDismiss, + getDehydrationKeyCache, +} from "./CrossSigningManager"; import Matrix from "matrix-js-sdk"; import { deriveKey } from 'matrix-js-sdk/src/crypto/key_passphrase'; import { decodeRecoveryKey } from 'matrix-js-sdk/src/crypto/recoverykey'; @@ -164,9 +169,6 @@ export default class Login { * @returns {MatrixClientCreds} */ export async function sendLoginRequest(hsUrl, isUrl, loginType, loginParams) { - let rehydrationKeyInfo; - let rehydrationKey; - const client = Matrix.createClient({ baseUrl: hsUrl, idBaseUrl: isUrl, @@ -190,14 +192,16 @@ export async function sendLoginRequest(hsUrl, isUrl, loginType, loginParams) { } } + const dehydrationKeyCache = getDehydrationKeyCache(); + return { homeserverUrl: hsUrl, identityServerUrl: isUrl, userId: data.user_id, deviceId: data.device_id, accessToken: data.access_token, - rehydrationKeyInfo, - rehydrationKey, + rehydrationKeyInfo: dehydrationKeyCache.keyInfo, + rehydrationKey: dehydrationKeyCache.key, olmAccount: data._olm_account, }; } @@ -243,5 +247,7 @@ async function getDehydrationKey(keyInfo) { throw new AccessCancelledError(); } const key = await inputToKey(input); + // need to copy the key because rehydration (unpickling) will clobber it + cacheDehydrationKey(new Uint8Array(key), keyInfo); return key; } diff --git a/src/MatrixClientPeg.ts b/src/MatrixClientPeg.ts index 61b7a04069..18af378fac 100644 --- a/src/MatrixClientPeg.ts +++ b/src/MatrixClientPeg.ts @@ -31,7 +31,7 @@ import {verificationMethods} from 'matrix-js-sdk/src/crypto'; import MatrixClientBackedSettingsHandler from "./settings/handlers/MatrixClientBackedSettingsHandler"; import * as StorageManager from './utils/StorageManager'; import IdentityAuthClient from './IdentityAuthClient'; -import { crossSigningCallbacks } from './CrossSigningManager'; +import { cacheDehydrationKey, crossSigningCallbacks } from './CrossSigningManager'; import {SHOW_QR_CODE_METHOD} from "matrix-js-sdk/src/crypto/verification/QRCode"; export interface IMatrixClientCreds { @@ -270,33 +270,43 @@ class _MatrixClientPeg implements IMatrixClientPeg { }; if (creds.olmAccount) { + console.log("got a dehydrated account"); opts.deviceToImport = { olmDevice: { - pickledAccount: creds.olmAccount.pickle("DEFAULT_KEY"), + pickledAccount: creds.olmAccount.pickle(creds.pickleKey || "DEFAULT_KEY"), sessions: [], - pickleKey: "DEFAULT_KEY", + pickleKey: creds.pickleKey || "DEFAULT_KEY", }, userId: creds.userId, deviceId: creds.deviceId, }; + creds.olmAccount.free(); } else { opts.userId = creds.userId; opts.deviceId = creds.deviceId; } - // FIXME: modify crossSigningCallbacks.getSecretStorageKey so that it tries using rehydrationkey and/or saves the passphrase info - // These are always installed regardless of the labs flag so that // cross-signing features can toggle on without reloading and also be // accessed immediately after login. Object.assign(opts.cryptoCallbacks, crossSigningCallbacks); - this.matrixClient = createMatrixClient(opts); + // set dehydration key after cross-signing gets set up -- we wait until + // cross-signing is set up because we want to cross-sign the dehydrated + // key + const origGetSecretStorageKey = opts.cryptoCallbacks.getSecretStorageKey + opts.cryptoCallbacks.getSecretStorageKey = async (keyinfo, ssssItemName) => { + const [name, key] = await origGetSecretStorageKey(keyinfo, ssssItemName); + this.matrixClient.setDehydrationKey(key, {passphrase: keyinfo.keys[name].passphrase}); + return [name, key]; + } if (creds.rehydrationKey) { - this.matrixClient.cacheDehydrationKey(creds.rehydrationKey, creds.rehydrationKeyInfo || {}); + cacheDehydrationKey(creds.rehydrationKey, creds.rehydrationKeyInfo); } + this.matrixClient = createMatrixClient(opts); + // we're going to add eventlisteners for each matrix event tile, so the // potential number of event listeners is quite high. this.matrixClient.setMaxListeners(500); diff --git a/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js b/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js index 53b3033330..b1c9dc5a60 100644 --- a/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js +++ b/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js @@ -304,6 +304,11 @@ export default class CreateSecretStorageDialog extends React.PureComponent { }, }); } + const dehydrationKeyInfo = + this._recoveryKey.keyInfo && this._recoveryKey.keyInfo.passphrase + ? {passphrase: this._recoveryKey.keyInfo.passphrase} + : {}; + await cli.setDehydrationKey(this._recoveryKey.privateKey, dehydrationKeyInfo); this.props.onFinished(true); } catch (e) { if (this.state.canUploadKeysWithPasswordOnly && e.httpStatus === 401 && e.data.flows) { From 999b5afa0ad9c40a810a39dd2d77a92ed8ab6b09 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 4 Sep 2020 21:41:14 -0600 Subject: [PATCH 003/233] Acknowledge the visibility request --- src/FromWidgetPostMessageApi.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/FromWidgetPostMessageApi.js b/src/FromWidgetPostMessageApi.js index d5d7c08d50..bbccc47d28 100644 --- a/src/FromWidgetPostMessageApi.js +++ b/src/FromWidgetPostMessageApi.js @@ -218,6 +218,9 @@ export default class FromWidgetPostMessageApi { if (ActiveWidgetStore.widgetHasCapability(widgetId, Capability.AlwaysOnScreen)) { ActiveWidgetStore.setWidgetPersistence(widgetId, val); } + + // acknowledge + this.sendResponse(event, {}); } else if (action === 'get_openid') { // Handled by caller } else { From b4af0140d425c03ffe2e8044edf30f1fe8326d7a Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 16 Sep 2020 12:38:47 -0600 Subject: [PATCH 004/233] Render Jitsi widget state events in a more obvious way A clear improvement to this would be to include join/leave buttons in the tiles, however this is currently deferred. --- res/css/_components.scss | 1 + .../views/messages/_MJitsiWidgetEvent.scss | 55 ++++++++++++++ src/TextForEvent.js | 4 - .../views/messages/MJitsiWidgetEvent.tsx | 74 +++++++++++++++++++ src/components/views/rooms/EventTile.js | 20 ++++- src/i18n/strings/en_EN.json | 4 + 6 files changed, 152 insertions(+), 6 deletions(-) create mode 100644 res/css/views/messages/_MJitsiWidgetEvent.scss create mode 100644 src/components/views/messages/MJitsiWidgetEvent.tsx diff --git a/res/css/_components.scss b/res/css/_components.scss index 54e7436886..26ad802955 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -139,6 +139,7 @@ @import "./views/messages/_MEmoteBody.scss"; @import "./views/messages/_MFileBody.scss"; @import "./views/messages/_MImageBody.scss"; +@import "./views/messages/_MJitsiWidgetEvent.scss"; @import "./views/messages/_MNoticeBody.scss"; @import "./views/messages/_MStickerBody.scss"; @import "./views/messages/_MTextBody.scss"; diff --git a/res/css/views/messages/_MJitsiWidgetEvent.scss b/res/css/views/messages/_MJitsiWidgetEvent.scss new file mode 100644 index 0000000000..3e51e89744 --- /dev/null +++ b/res/css/views/messages/_MJitsiWidgetEvent.scss @@ -0,0 +1,55 @@ +/* +Copyright 2020 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_MJitsiWidgetEvent { + display: grid; + grid-template-columns: 24px minmax(0, 1fr) min-content; + + &::before { + grid-column: 1; + grid-row: 1 / 3; + width: 16px; + height: 16px; + content: ""; + top: 0; + bottom: 0; + left: 0; + right: 0; + mask-repeat: no-repeat; + mask-position: center; + mask-size: contain; + background-color: $composer-e2e-icon-color; // XXX: Variable abuse + margin-top: 4px; + mask-image: url('$(res)/img/element-icons/call/video-call.svg'); + } + + .mx_MJitsiWidgetEvent_title { + font-weight: 600; + font-size: $font-15px; + grid-column: 2; + grid-row: 1; + } + + .mx_MJitsiWidgetEvent_subtitle { + grid-column: 2; + grid-row: 2; + } + + .mx_MJitsiWidgetEvent_title, + .mx_MJitsiWidgetEvent_subtitle { + overflow-wrap: break-word; + } +} diff --git a/src/TextForEvent.js b/src/TextForEvent.js index a76c1f59e6..46e1878d5f 100644 --- a/src/TextForEvent.js +++ b/src/TextForEvent.js @@ -476,10 +476,6 @@ function textForWidgetEvent(event) { const {name: prevName, type: prevType, url: prevUrl} = event.getPrevContent(); const {name, type, url} = event.getContent() || {}; - if (WidgetType.JITSI.matches(type) || WidgetType.JITSI.matches(prevType)) { - return textForJitsiWidgetEvent(event, senderName, url, prevUrl); - } - let widgetName = name || prevName || type || prevType || ''; // Apply sentence case to widget name if (widgetName && widgetName.length > 0) { diff --git a/src/components/views/messages/MJitsiWidgetEvent.tsx b/src/components/views/messages/MJitsiWidgetEvent.tsx new file mode 100644 index 0000000000..1bfefbff6a --- /dev/null +++ b/src/components/views/messages/MJitsiWidgetEvent.tsx @@ -0,0 +1,74 @@ +/* +Copyright 2020 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 { MatrixEvent } from "matrix-js-sdk/src/models/event"; +import { _t } from "../../../languageHandler"; + +interface IProps { + mxEvent: MatrixEvent; +} + +interface IState { +} + +export default class MJitsiWidgetEvent extends React.PureComponent { + constructor(props) { + super(props); + this.state = {}; + } + + render() { + const url = this.props.mxEvent.getContent()['url']; + const prevUrl = this.props.mxEvent.getPrevContent()['url']; + const senderName = this.props.mxEvent.sender?.name || this.props.mxEvent.getSender(); + + if (!url) { + // removed + return ( +
+
+ {_t("Video conference ended by %(senderName)s", {senderName})} +
+
+ ); + } else if (prevUrl) { + // modified + return ( +
+
+ {_t("Video conference updated by %(senderName)s", {senderName})} +
+
+ {_t("Join the conference at the top of this room.")} +
+
+ ); + } else { + // assume added + return ( +
+
+ {_t("Video conference started by %(senderName)s", {senderName})} +
+
+ {_t("Join the conference at the top of this room.")} +
+
+ ); + } + } +} diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index ab9f240f2d..ef9317704d 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -34,6 +34,7 @@ import * as ObjectUtils from "../../../ObjectUtils"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; import {E2E_STATE} from "./E2EIcon"; import {toRem} from "../../../utils/units"; +import {WidgetType} from "../../../widgets/WidgetType"; const eventTileTypes = { 'm.room.message': 'messages.MessageEvent', @@ -110,6 +111,19 @@ export function getHandlerTile(ev) { } } + // TODO: Enable support for m.widget event type (https://github.com/vector-im/element-web/issues/13111) + if (type === "im.vector.modular.widgets") { + let type = ev.getContent()['type']; + if (!type) { + // deleted/invalid widget - try the past widget type + type = ev.getPrevContent()['type']; + } + + if (WidgetType.JITSI.matches(type)) { + return "messages.MJitsiWidgetEvent"; + } + } + return ev.isState() ? stateEventTileTypes[type] : eventTileTypes[type]; } @@ -619,16 +633,18 @@ export default class EventTile extends React.Component { const msgtype = content.msgtype; const eventType = this.props.mxEvent.getType(); + let tileHandler = getHandlerTile(this.props.mxEvent); + // Info messages are basically information about commands processed on a room const isBubbleMessage = eventType.startsWith("m.key.verification") || (eventType === "m.room.message" && msgtype && msgtype.startsWith("m.key.verification")) || - (eventType === "m.room.encryption"); + (eventType === "m.room.encryption") || + (tileHandler === "messages.MJitsiWidgetEvent"); let isInfoMessage = ( !isBubbleMessage && eventType !== 'm.room.message' && eventType !== 'm.sticker' && eventType !== 'm.room.create' ); - let tileHandler = getHandlerTile(this.props.mxEvent); // If we're showing hidden events in the timeline, we should use the // source tile when there's no regular tile for an event and also for // replace relations (which otherwise would display as a confusing diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index b2b4e01202..9d1d39477c 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1405,6 +1405,10 @@ "Invalid file%(extra)s": "Invalid file%(extra)s", "Error decrypting image": "Error decrypting image", "Show image": "Show image", + "Video conference ended by %(senderName)s": "Video conference ended by %(senderName)s", + "Video conference updated by %(senderName)s": "Video conference updated by %(senderName)s", + "Join the conference at the top of this room.": "Join the conference at the top of this room.", + "Video conference started by %(senderName)s": "Video conference started by %(senderName)s", "You have ignored this user, so their message is hidden. Show anyways.": "You have ignored this user, so their message is hidden. Show anyways.", "You verified %(name)s": "You verified %(name)s", "You cancelled verifying %(name)s": "You cancelled verifying %(name)s", From 12fb1ee1cf82a2d4c70636681314c5bc1a087a78 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 16 Sep 2020 12:43:28 -0600 Subject: [PATCH 005/233] Clean up now-unused code --- src/TextForEvent.js | 19 ------------------- src/i18n/strings/en_EN.json | 3 --- 2 files changed, 22 deletions(-) diff --git a/src/TextForEvent.js b/src/TextForEvent.js index 46e1878d5f..c55380bd9b 100644 --- a/src/TextForEvent.js +++ b/src/TextForEvent.js @@ -19,7 +19,6 @@ import { _t } from './languageHandler'; import * as Roles from './Roles'; import {isValid3pidInvite} from "./RoomInvite"; import SettingsStore from "./settings/SettingsStore"; -import {WidgetType} from "./widgets/WidgetType"; import {ALL_RULE_TYPES, ROOM_RULE_TYPES, SERVER_RULE_TYPES, USER_RULE_TYPES} from "./mjolnir/BanList"; function textForMemberEvent(ev) { @@ -501,24 +500,6 @@ function textForWidgetEvent(event) { } } -function textForJitsiWidgetEvent(event, senderName, url, prevUrl) { - if (url) { - if (prevUrl) { - return _t('Group call modified by %(senderName)s', { - senderName, - }); - } else { - return _t('Group call started by %(senderName)s', { - senderName, - }); - } - } else { - return _t('Group call ended by %(senderName)s', { - senderName, - }); - } -} - function textForMjolnirEvent(event) { const senderName = event.getSender(); const {entity: prevEntity} = event.getPrevContent(); diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 9d1d39477c..01d334505c 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -280,9 +280,6 @@ "%(widgetName)s widget modified by %(senderName)s": "%(widgetName)s widget modified by %(senderName)s", "%(widgetName)s widget added by %(senderName)s": "%(widgetName)s widget added by %(senderName)s", "%(widgetName)s widget removed by %(senderName)s": "%(widgetName)s widget removed by %(senderName)s", - "Group call modified by %(senderName)s": "Group call modified by %(senderName)s", - "Group call started by %(senderName)s": "Group call started by %(senderName)s", - "Group call ended by %(senderName)s": "Group call ended by %(senderName)s", "%(senderName)s removed the rule banning users matching %(glob)s": "%(senderName)s removed the rule banning users matching %(glob)s", "%(senderName)s removed the rule banning rooms matching %(glob)s": "%(senderName)s removed the rule banning rooms matching %(glob)s", "%(senderName)s removed the rule banning servers matching %(glob)s": "%(senderName)s removed the rule banning servers matching %(glob)s", From 1ffc6d5bd34fa2d2e87c0ea533c7cd2d9104cf5f Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 16 Sep 2020 14:35:50 -0600 Subject: [PATCH 006/233] Make the hangup button do things for conference calls Behaviour constraints: * If you're not in the conference, use a grey button that does nothing. * If you're in the conference, show a button: * If you're able to modify widgets in the room, annotate it in the context of ending the call for everyone and remove the widget. Use a confirmation dialog. * If you're not able to modify widgets in the room, hang up. For this we know that persistent Jitsi widgets will mean that the user is in the call, so we use that to determine if they are actually participating. --- res/css/views/rooms/_MessageComposer.scss | 2 +- src/CallHandler.js | 77 ++++++++++++------- src/WidgetMessaging.js | 11 +++ src/components/views/rooms/MessageComposer.js | 63 +++++++++++++-- src/i18n/strings/en_EN.json | 7 +- src/stores/WidgetStore.ts | 19 +++++ src/widgets/WidgetApi.ts | 7 +- 7 files changed, 144 insertions(+), 42 deletions(-) diff --git a/res/css/views/rooms/_MessageComposer.scss b/res/css/views/rooms/_MessageComposer.scss index a403a8dc4c..71c0db947e 100644 --- a/res/css/views/rooms/_MessageComposer.scss +++ b/res/css/views/rooms/_MessageComposer.scss @@ -217,7 +217,7 @@ limitations under the License. } } - &.mx_MessageComposer_hangup::before { + &.mx_MessageComposer_hangup:not(.mx_AccessibleButton_disabled)::before { background-color: $warning-color; } } diff --git a/src/CallHandler.js b/src/CallHandler.js index ad40332af5..e40c97f025 100644 --- a/src/CallHandler.js +++ b/src/CallHandler.js @@ -70,6 +70,8 @@ import {base32} from "rfc4648"; import QuestionDialog from "./components/views/dialogs/QuestionDialog"; import ErrorDialog from "./components/views/dialogs/ErrorDialog"; +import WidgetStore from "./stores/WidgetStore"; +import ActiveWidgetStore from "./stores/ActiveWidgetStore"; global.mxCalls = { //room_id: MatrixCall @@ -310,6 +312,14 @@ function _onAction(payload) { console.info("Place conference call in %s", payload.room_id); _startCallApp(payload.room_id, payload.type); break; + case 'end_conference': + console.info("Terminating conference call in %s", payload.room_id); + _terminateCallApp(payload.room_id); + break; + case 'hangup_conference': + console.info("Leaving conference call in %s", payload.room_id); + _hangupWithCallApp(payload.room_id); + break; case 'incoming_call': { if (callHandler.getAnyActiveCall()) { @@ -357,10 +367,12 @@ async function _startCallApp(roomId, type) { show: true, }); + // prevent double clicking the call button const room = MatrixClientPeg.get().getRoom(roomId); const currentJitsiWidgets = WidgetUtils.getRoomWidgetsOfType(room, WidgetType.JITSI); - - if (WidgetEchoStore.roomHasPendingWidgetsOfType(roomId, currentJitsiWidgets, WidgetType.JITSI)) { + const hasJitsi = currentJitsiWidgets.length > 0 + || WidgetEchoStore.roomHasPendingWidgetsOfType(roomId, currentJitsiWidgets, WidgetType.JITSI); + if (hasJitsi) { Modal.createTrackedDialog('Call already in progress', '', ErrorDialog, { title: _t('Call in Progress'), description: _t('A call is currently being placed!'), @@ -368,33 +380,6 @@ async function _startCallApp(roomId, type) { return; } - if (currentJitsiWidgets.length > 0) { - console.warn( - "Refusing to start conference call widget in " + roomId + - " a conference call widget is already present", - ); - - if (WidgetUtils.canUserModifyWidgets(roomId)) { - Modal.createTrackedDialog('Already have Jitsi Widget', '', QuestionDialog, { - title: _t('End Call'), - description: _t('Remove the group call from the room?'), - button: _t('End Call'), - cancelButton: _t('Cancel'), - onFinished: (endCall) => { - if (endCall) { - WidgetUtils.setRoomWidget(roomId, currentJitsiWidgets[0].getContent()['id']); - } - }, - }); - } else { - Modal.createTrackedDialog('Already have Jitsi Widget', '', ErrorDialog, { - title: _t('Call in Progress'), - description: _t("You don't have permission to remove the call from the room"), - }); - } - return; - } - const jitsiDomain = Jitsi.getInstance().preferredDomain; const jitsiAuth = await Jitsi.getInstance().getJitsiAuth(); let confId; @@ -444,6 +429,40 @@ async function _startCallApp(roomId, type) { }); } +function _terminateCallApp(roomId) { + Modal.createTrackedDialog('Confirm Jitsi Terminate', '', QuestionDialog, { + hasCancelButton: true, + title: _t("End conference"), + description: _t("Ending the conference will end the call for everyone. Continue?"), + button: _t("End conference"), + onFinished: (proceed) => { + if (!proceed) return; + + // We'll just obliterate them all. There should only ever be one, but might as well + // be safe. + const roomInfo = WidgetStore.instance.getRoom(roomId); + const jitsiWidgets = roomInfo.widgets.filter(w => WidgetType.JITSI.matches(w.type)); + jitsiWidgets.forEach(w => { + // setting invalid content removes it + WidgetUtils.setRoomWidget(roomId, w.id); + }); + }, + }); +} + +function _hangupWithCallApp(roomId) { + const roomInfo = WidgetStore.instance.getRoom(roomId); + if (!roomInfo) return; // "should never happen" clauses go here + + const jitsiWidgets = roomInfo.widgets.filter(w => WidgetType.JITSI.matches(w.type)); + jitsiWidgets.forEach(w => { + const messaging = ActiveWidgetStore.getWidgetMessaging(w.id); + if (!messaging) return; // more "should never happen" words + + messaging.hangup(); + }); +} + // FIXME: Nasty way of making sure we only register // with the dispatcher once if (!global.mxCallHandler) { diff --git a/src/WidgetMessaging.js b/src/WidgetMessaging.js index c68e926ac1..0f8626ec66 100644 --- a/src/WidgetMessaging.js +++ b/src/WidgetMessaging.js @@ -107,6 +107,17 @@ export default class WidgetMessaging { }); } + /** + * Tells the widget to hang up on its call. + * @returns {Promise<*>} Resolves when teh widget has acknowledged the message. + */ + hangup() { + return this.messageToWidget({ + api: OUTBOUND_API_NAME, + action: KnownWidgetActions.Hangup, + }); + } + /** * Request a screenshot from a widget * @return {Promise} To be resolved with screenshot data when it has been generated diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js index 81c2ae7a33..3eab58557e 100644 --- a/src/components/views/rooms/MessageComposer.js +++ b/src/components/views/rooms/MessageComposer.js @@ -1,6 +1,7 @@ /* Copyright 2015, 2016 OpenMarket Ltd Copyright 2017, 2018 New Vector Ltd +Copyright 2020 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. @@ -32,6 +33,10 @@ import {aboveLeftOf, ContextMenu, ContextMenuTooltipButton, useContextMenu} from import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; import ReplyPreview from "./ReplyPreview"; import {UIFeature} from "../../../settings/UIFeature"; +import WidgetStore from "../../../stores/WidgetStore"; +import WidgetUtils from "../../../utils/WidgetUtils"; +import {UPDATE_EVENT} from "../../../stores/AsyncStore"; +import ActiveWidgetStore from "../../../stores/ActiveWidgetStore"; function ComposerAvatar(props) { const MemberStatusMessageAvatar = sdk.getComponent('avatars.MemberStatusMessageAvatar'); @@ -85,8 +90,15 @@ VideoCallButton.propTypes = { }; function HangupButton(props) { - const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); const onHangupClick = () => { + if (props.isConference) { + dis.dispatch({ + action: props.canEndConference ? 'end_conference' : 'hangup_conference', + room_id: props.roomId, + }); + return; + } + const call = CallHandler.getCallForRoom(props.roomId); if (!call) { return; @@ -98,14 +110,28 @@ function HangupButton(props) { room_id: call.roomId, }); }; - return (); + title={tooltip} + disabled={!canLeaveConference} + /> + ); } HangupButton.propTypes = { roomId: PropTypes.string.isRequired, + isConference: PropTypes.bool.isRequired, + canEndConference: PropTypes.bool, + isInConference: PropTypes.bool, }; const EmojiButton = ({addEmoji}) => { @@ -226,12 +252,17 @@ export default class MessageComposer extends React.Component { this._onRoomViewStoreUpdate = this._onRoomViewStoreUpdate.bind(this); this._onTombstoneClick = this._onTombstoneClick.bind(this); this.renderPlaceholderText = this.renderPlaceholderText.bind(this); + WidgetStore.instance.on(UPDATE_EVENT, this._onWidgetUpdate); + ActiveWidgetStore.on('update', this._onActiveWidgetUpdate); this._dispatcherRef = null; + this.state = { isQuoting: Boolean(RoomViewStore.getQuotingEvent()), tombstone: this._getRoomTombstone(), canSendMessages: this.props.room.maySendMessage(), showCallButtons: SettingsStore.getValue("showCallButtonsInComposer"), + hasConference: WidgetStore.instance.doesRoomHaveConference(this.props.room), + joinedConference: WidgetStore.instance.isJoinedToConferenceIn(this.props.room), }; } @@ -247,6 +278,14 @@ export default class MessageComposer extends React.Component { } }; + _onWidgetUpdate = () => { + this.setState({hasConference: WidgetStore.instance.doesRoomHaveConference(this.props.room)}); + }; + + _onActiveWidgetUpdate = () => { + this.setState({joinedConference: WidgetStore.instance.isJoinedToConferenceIn(this.props.room)}); + }; + componentDidMount() { this.dispatcherRef = dis.register(this.onAction); MatrixClientPeg.get().on("RoomState.events", this._onRoomStateEvents); @@ -277,6 +316,8 @@ export default class MessageComposer extends React.Component { if (this._roomStoreToken) { this._roomStoreToken.remove(); } + WidgetStore.instance.removeListener(UPDATE_EVENT, this._onWidgetUpdate); + ActiveWidgetStore.removeListener('update', this._onActiveWidgetUpdate); dis.unregister(this.dispatcherRef); } @@ -392,9 +433,19 @@ export default class MessageComposer extends React.Component { } if (this.state.showCallButtons) { - if (callInProgress) { + if (this.state.hasConference) { + const canEndConf = WidgetUtils.canUserModifyWidgets(this.props.room.roomId); controls.push( - , + , + ); + } else if (callInProgress) { + controls.push( + , ); } else { controls.push( diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index b2b4e01202..b5ecf26cb7 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -50,12 +50,10 @@ "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!", - "End Call": "End Call", - "Remove the group call from the room?": "Remove the group call from the room?", - "Cancel": "Cancel", - "You don't have permission to remove the call from the room": "You don't have permission to remove the call from the room", "Permission Required": "Permission Required", "You do not have permission to start a conference call in this room": "You do not have permission to start a conference call in this room", + "End conference": "End conference", + "Ending the conference will end the call for everyone. Continue?": "Ending the conference will end the call for everyone. Continue?", "Replying With Files": "Replying With Files", "At this time it is not possible to reply with a file. Would you like to upload this file without replying?": "At this time it is not possible to reply with a file. Would you like to upload this file without replying?", "Continue": "Continue", @@ -143,6 +141,7 @@ "Cancel entering passphrase?": "Cancel entering passphrase?", "Are you sure you want to cancel entering passphrase?": "Are you sure you want to cancel entering passphrase?", "Go Back": "Go Back", + "Cancel": "Cancel", "Setting up keys": "Setting up keys", "Messages": "Messages", "Actions": "Actions", diff --git a/src/stores/WidgetStore.ts b/src/stores/WidgetStore.ts index 10327ce4e9..be2233961b 100644 --- a/src/stores/WidgetStore.ts +++ b/src/stores/WidgetStore.ts @@ -22,6 +22,7 @@ import { AsyncStoreWithClient } from "./AsyncStoreWithClient"; import defaultDispatcher from "../dispatcher/dispatcher"; import SettingsStore from "../settings/SettingsStore"; import WidgetEchoStore from "../stores/WidgetEchoStore"; +import ActiveWidgetStore from "../stores/ActiveWidgetStore"; import WidgetUtils from "../utils/WidgetUtils"; import {SettingLevel} from "../settings/SettingLevel"; import {WidgetType} from "../widgets/WidgetType"; @@ -206,6 +207,24 @@ export default class WidgetStore extends AsyncStoreWithClient { } return roomInfo.widgets; } + + public doesRoomHaveConference(room: Room): boolean { + const roomInfo = this.getRoom(room.roomId); + if (!roomInfo) return false; + + const currentWidgets = roomInfo.widgets.filter(w => WidgetType.JITSI.matches(w.type)); + const hasPendingWidgets = WidgetEchoStore.roomHasPendingWidgetsOfType(room.roomId, [], WidgetType.JITSI); + return currentWidgets.length > 0 || hasPendingWidgets; + } + + public isJoinedToConferenceIn(room: Room): boolean { + const roomInfo = this.getRoom(room.roomId); + if (!roomInfo) return false; + + // A persistent conference widget indicates that we're participating + const widgets = roomInfo.widgets.filter(w => WidgetType.JITSI.matches(w.type)); + return widgets.some(w => ActiveWidgetStore.getWidgetPersistence(w.id)); + } } window.mxWidgetStore = WidgetStore.instance; diff --git a/src/widgets/WidgetApi.ts b/src/widgets/WidgetApi.ts index 672cbf2a56..c25d607948 100644 --- a/src/widgets/WidgetApi.ts +++ b/src/widgets/WidgetApi.ts @@ -39,6 +39,7 @@ export enum KnownWidgetActions { SetAlwaysOnScreen = "set_always_on_screen", ClientReady = "im.vector.ready", Terminate = "im.vector.terminate", + Hangup = "im.vector.hangup", } export type WidgetAction = KnownWidgetActions | string; @@ -119,13 +120,15 @@ export class WidgetApi extends EventEmitter { // Automatically acknowledge so we can move on this.replyToRequest(payload, {}); - } else if (payload.action === KnownWidgetActions.Terminate) { + } else if (payload.action === KnownWidgetActions.Terminate + || payload.action === KnownWidgetActions.Hangup) { // Finalization needs to be async, so postpone with a promise let finalizePromise = Promise.resolve(); const wait = (promise) => { finalizePromise = finalizePromise.then(() => promise); }; - this.emit('terminate', wait); + const emitName = payload.action === KnownWidgetActions.Terminate ? 'terminate' : 'hangup'; + this.emit(emitName, wait); Promise.resolve(finalizePromise).then(() => { // Acknowledge that we're shut down now this.replyToRequest(payload, {}); From f412f8defeab4a6af02722f3c91872b9857de83b Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 16 Sep 2020 14:59:15 -0600 Subject: [PATCH 007/233] Change copy if the widget is unpinned --- .../views/messages/MJitsiWidgetEvent.tsx | 22 +++++++++++++++---- src/i18n/strings/en_EN.json | 3 ++- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/src/components/views/messages/MJitsiWidgetEvent.tsx b/src/components/views/messages/MJitsiWidgetEvent.tsx index 1bfefbff6a..6f87aaec28 100644 --- a/src/components/views/messages/MJitsiWidgetEvent.tsx +++ b/src/components/views/messages/MJitsiWidgetEvent.tsx @@ -17,6 +17,8 @@ limitations under the License. import React from 'react'; import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { _t } from "../../../languageHandler"; +import WidgetStore from "../../../stores/WidgetStore"; +import { WidgetType } from "../../../widgets/WidgetType"; interface IProps { mxEvent: MatrixEvent; @@ -36,12 +38,24 @@ export default class MJitsiWidgetEvent extends React.PureComponent WidgetType.JITSI.matches(w.type) && WidgetStore.instance.isPinned(w.id)); + + let joinCopy = _t('Join the conference at the top of this room'); + if (!isPinned) { + joinCopy = _t('Join the conference from the room information card on the right'); + } + if (!url) { // removed return (
- {_t("Video conference ended by %(senderName)s", {senderName})} + {_t('Video conference ended by %(senderName)s', {senderName})}
); @@ -50,10 +64,10 @@ export default class MJitsiWidgetEvent extends React.PureComponent
- {_t("Video conference updated by %(senderName)s", {senderName})} + {_t('Video conference updated by %(senderName)s', {senderName})}
- {_t("Join the conference at the top of this room.")} + {joinCopy}
); @@ -65,7 +79,7 @@ export default class MJitsiWidgetEvent extends React.PureComponent
- {_t("Join the conference at the top of this room.")} + {joinCopy}
); diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 01d334505c..dc218aefc5 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1402,9 +1402,10 @@ "Invalid file%(extra)s": "Invalid file%(extra)s", "Error decrypting image": "Error decrypting image", "Show image": "Show image", + "Join the conference at the top of this room": "Join the conference at the top of this room", + "Join the conference from the room information card on the right": "Join the conference from the room information card on the right", "Video conference ended by %(senderName)s": "Video conference ended by %(senderName)s", "Video conference updated by %(senderName)s": "Video conference updated by %(senderName)s", - "Join the conference at the top of this room.": "Join the conference at the top of this room.", "Video conference started by %(senderName)s": "Video conference started by %(senderName)s", "You have ignored this user, so their message is hidden. Show anyways.": "You have ignored this user, so their message is hidden. Show anyways.", "You verified %(name)s": "You verified %(name)s", From 959b8dd31419003d598991785005d34c2d28255d Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 16 Sep 2020 14:59:40 -0600 Subject: [PATCH 008/233] de-state --- src/components/views/messages/MJitsiWidgetEvent.tsx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/components/views/messages/MJitsiWidgetEvent.tsx b/src/components/views/messages/MJitsiWidgetEvent.tsx index 6f87aaec28..5171780ecc 100644 --- a/src/components/views/messages/MJitsiWidgetEvent.tsx +++ b/src/components/views/messages/MJitsiWidgetEvent.tsx @@ -24,13 +24,9 @@ interface IProps { mxEvent: MatrixEvent; } -interface IState { -} - -export default class MJitsiWidgetEvent extends React.PureComponent { +export default class MJitsiWidgetEvent extends React.PureComponent { constructor(props) { super(props); - this.state = {}; } render() { From dca48b984fa3440e2b78be33f984da459d16327e Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 16 Sep 2020 15:47:06 -0600 Subject: [PATCH 009/233] Be more sane --- src/components/views/messages/MJitsiWidgetEvent.tsx | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/components/views/messages/MJitsiWidgetEvent.tsx b/src/components/views/messages/MJitsiWidgetEvent.tsx index 5171780ecc..bd161b5ca2 100644 --- a/src/components/views/messages/MJitsiWidgetEvent.tsx +++ b/src/components/views/messages/MJitsiWidgetEvent.tsx @@ -34,15 +34,8 @@ export default class MJitsiWidgetEvent extends React.PureComponent { const prevUrl = this.props.mxEvent.getPrevContent()['url']; const senderName = this.props.mxEvent.sender?.name || this.props.mxEvent.getSender(); - // XXX: We are assuming that there will only be one Jitsi widget per room, which isn't entirely - // safe but if there's more than 1 the user will be super confused anyways - the copy doesn't - // need to concern itself with this. - const roomInfo = WidgetStore.instance.getRoom(this.props.mxEvent.getRoomId()); - const isPinned = roomInfo?.widgets - .some(w => WidgetType.JITSI.matches(w.type) && WidgetStore.instance.isPinned(w.id)); - let joinCopy = _t('Join the conference at the top of this room'); - if (!isPinned) { + if (!WidgetStore.instance.isPinned(this.props.mxEvent.getStateKey())) { joinCopy = _t('Join the conference from the room information card on the right'); } From e52a02d733505d1a15e0957de9011f2296fffb77 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 16 Sep 2020 17:26:00 -0600 Subject: [PATCH 010/233] Appease the linter --- src/components/views/messages/MJitsiWidgetEvent.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/views/messages/MJitsiWidgetEvent.tsx b/src/components/views/messages/MJitsiWidgetEvent.tsx index bd161b5ca2..3d191209f9 100644 --- a/src/components/views/messages/MJitsiWidgetEvent.tsx +++ b/src/components/views/messages/MJitsiWidgetEvent.tsx @@ -18,7 +18,6 @@ import React from 'react'; import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { _t } from "../../../languageHandler"; import WidgetStore from "../../../stores/WidgetStore"; -import { WidgetType } from "../../../widgets/WidgetType"; interface IProps { mxEvent: MatrixEvent; From 8129333dcc35d4ea8cec32521489e1819cc52f5b Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 16 Sep 2020 22:38:12 -0600 Subject: [PATCH 011/233] Make the PIP Jitsi look and feel like the 1:1 PIP * Similar sizing * Fix pointers so the jitsi widget doesn't feel clickable when it's not * We might want to introduce click-to-visit-room for the Jitsi widget (like the 1:1 call), however the Jitsi widget has many more controls to worry about * Remove the menu bar from the widget to avoid accidents --- res/css/views/rooms/_AppsDrawer.scss | 4 ++-- res/css/views/voip/_CallContainer.scss | 14 ++++++++++++-- src/components/views/elements/PersistentApp.js | 1 + 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/res/css/views/rooms/_AppsDrawer.scss b/res/css/views/rooms/_AppsDrawer.scss index fee3d61153..b9249d310a 100644 --- a/res/css/views/rooms/_AppsDrawer.scss +++ b/res/css/views/rooms/_AppsDrawer.scss @@ -15,7 +15,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -$MiniAppTileHeight: 114px; +$MiniAppTileHeight: 200px; .mx_AppsDrawer { margin: 5px 5px 5px 18px; @@ -220,7 +220,7 @@ $MiniAppTileHeight: 114px; } .mx_AppTileBody_mini { - height: 112px; + height: $MiniAppTileHeight; width: 100%; overflow: hidden; } diff --git a/res/css/views/voip/_CallContainer.scss b/res/css/views/voip/_CallContainer.scss index 4d26d8a312..650302b7e1 100644 --- a/res/css/views/voip/_CallContainer.scss +++ b/res/css/views/voip/_CallContainer.scss @@ -23,9 +23,16 @@ limitations under the License. z-index: 100; box-shadow: 0px 14px 24px rgba(0, 0, 0, 0.08); - cursor: pointer; + // Disable pointer events for Jitsi widgets to function. Direct + // calls have their own cursor and behaviour, but we need to make + // sure the cursor hits the iframe for Jitsi which will be at a + // different level. + pointer-events: none; .mx_CallPreview { + pointer-events: initial; // restore pointer events so the user can leave/interact + cursor: pointer; + .mx_VideoView { width: 350px; } @@ -37,7 +44,7 @@ limitations under the License. } .mx_AppTile_persistedWrapper div { - min-width: 300px; + min-width: 350px; } .mx_IncomingCallBox { @@ -45,6 +52,9 @@ limitations under the License. background-color: $primary-bg-color; padding: 8px; + pointer-events: initial; // restore pointer events so the user can accept/decline + cursor: pointer; + .mx_IncomingCallBox_CallerInfo { display: flex; direction: row; diff --git a/src/components/views/elements/PersistentApp.js b/src/components/views/elements/PersistentApp.js index 686739a9f7..a3e413151a 100644 --- a/src/components/views/elements/PersistentApp.js +++ b/src/components/views/elements/PersistentApp.js @@ -82,6 +82,7 @@ export default class PersistentApp extends React.Component { showDelete={false} showMinimise={false} miniMode={true} + showMenubar={false} />; } } From e849cd8fe54466468ef6867d0546f43aafe57dc9 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 16 Sep 2020 18:13:52 -0600 Subject: [PATCH 012/233] Null-check the widget before continuing Deleted widgets should return isPinned=false --- src/stores/WidgetStore.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/stores/WidgetStore.ts b/src/stores/WidgetStore.ts index 10327ce4e9..f3b8ee1299 100644 --- a/src/stores/WidgetStore.ts +++ b/src/stores/WidgetStore.ts @@ -158,7 +158,8 @@ export default class WidgetStore extends AsyncStoreWithClient { let pinned = roomInfo && roomInfo.pinned[widgetId]; // Jitsi widgets should be pinned by default - if (pinned === undefined && WidgetType.JITSI.matches(this.widgetMap.get(widgetId).type)) pinned = true; + const widget = this.widgetMap.get(widgetId); + if (pinned === undefined && WidgetType.JITSI.matches(widget?.type)) pinned = true; return pinned; } From 55ceb2abd6278b26b8a7d3cdf30ea0703c85088f Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 17 Sep 2020 09:33:05 -0600 Subject: [PATCH 013/233] speeeeeeling Co-authored-by: J. Ryan Stinnett --- src/WidgetMessaging.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/WidgetMessaging.js b/src/WidgetMessaging.js index 0f8626ec66..9394abf025 100644 --- a/src/WidgetMessaging.js +++ b/src/WidgetMessaging.js @@ -109,7 +109,7 @@ export default class WidgetMessaging { /** * Tells the widget to hang up on its call. - * @returns {Promise<*>} Resolves when teh widget has acknowledged the message. + * @returns {Promise<*>} Resolves when the widget has acknowledged the message. */ hangup() { return this.messageToWidget({ From 849a5e4a3976b7856e0c1efb998ed375c0a5887f Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 17 Sep 2020 14:58:48 -0600 Subject: [PATCH 014/233] Round the jitsi pip corners --- res/css/views/rooms/_AppsDrawer.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/res/css/views/rooms/_AppsDrawer.scss b/res/css/views/rooms/_AppsDrawer.scss index b9249d310a..244e88ca3e 100644 --- a/res/css/views/rooms/_AppsDrawer.scss +++ b/res/css/views/rooms/_AppsDrawer.scss @@ -223,6 +223,7 @@ $MiniAppTileHeight: 200px; height: $MiniAppTileHeight; width: 100%; overflow: hidden; + border-radius: 8px; } .mx_AppTile .mx_AppTileBody, From feaa5f31eabd94cf34db78bd518a1c85ee31f7be Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 17 Sep 2020 15:00:35 -0600 Subject: [PATCH 015/233] Match consistency --- src/CallHandler.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/CallHandler.js b/src/CallHandler.js index e40c97f025..3de1566234 100644 --- a/src/CallHandler.js +++ b/src/CallHandler.js @@ -318,7 +318,7 @@ function _onAction(payload) { break; case 'hangup_conference': console.info("Leaving conference call in %s", payload.room_id); - _hangupWithCallApp(payload.room_id); + _hangupCallApp(payload.room_id); break; case 'incoming_call': { @@ -450,7 +450,7 @@ function _terminateCallApp(roomId) { }); } -function _hangupWithCallApp(roomId) { +function _hangupCallApp(roomId) { const roomInfo = WidgetStore.instance.getRoom(roomId); if (!roomInfo) return; // "should never happen" clauses go here From b1b7215532596acac7fe17fd83f65ca317dd8e7d Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Fri, 18 Sep 2020 18:08:17 -0400 Subject: [PATCH 016/233] fix lint and merge issues --- src/Login.js | 6 +++--- src/SecurityManager.js | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Login.js b/src/Login.js index 0563952c5d..c04b086afa 100644 --- a/src/Login.js +++ b/src/Login.js @@ -25,7 +25,7 @@ import { cacheDehydrationKey, confirmToDismiss, getDehydrationKeyCache, -} from "./CrossSigningManager"; +} from "./SecurityManager"; import Matrix from "matrix-js-sdk"; import { deriveKey } from 'matrix-js-sdk/src/crypto/key_passphrase'; import { decodeRecoveryKey } from 'matrix-js-sdk/src/crypto/recoverykey'; @@ -173,8 +173,8 @@ export async function sendLoginRequest(hsUrl, isUrl, loginType, loginParams) { baseUrl: hsUrl, idBaseUrl: isUrl, cryptoCallbacks: { - getDehydrationKey - } + getDehydrationKey, + }, }); const data = await client.loginWithRehydration(null, loginType, loginParams); diff --git a/src/SecurityManager.js b/src/SecurityManager.js index e8bd63d2ff..967c0cc266 100644 --- a/src/SecurityManager.js +++ b/src/SecurityManager.js @@ -91,7 +91,7 @@ async function getSecretStorageKey({ keys: keyInfos }, ssssItemName) { // if we dehydrated a device, see if that key works for SSSS if (dehydrationInfo.key) { try { - if (await MatrixClientPeg.get().checkSecretStorageKey(dehydrationInfo.key, info)) { + if (await MatrixClientPeg.get().checkSecretStorageKey(dehydrationInfo.key, keyInfo)) { const key = dehydrationInfo.key; // Save to cache to avoid future prompts in the current session if (isCachingAllowed()) { From 4e2397a79db8242f6ff04f9b5c5e61693d21533c Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Fri, 18 Sep 2020 20:53:39 -0400 Subject: [PATCH 017/233] doc fixes and minor code improvement --- src/MatrixClientPeg.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/MatrixClientPeg.ts b/src/MatrixClientPeg.ts index a5fa0fb3cf..84bc610896 100644 --- a/src/MatrixClientPeg.ts +++ b/src/MatrixClientPeg.ts @@ -271,11 +271,12 @@ class _MatrixClientPeg implements IMatrixClientPeg { if (creds.olmAccount) { console.log("got a dehydrated account"); + const pickleKey = creds.pickleKey || "DEFAULT_KEY"; opts.deviceToImport = { olmDevice: { - pickledAccount: creds.olmAccount.pickle(creds.pickleKey || "DEFAULT_KEY"), + pickledAccount: creds.olmAccount.pickle(pickleKey), sessions: [], - pickleKey: creds.pickleKey || "DEFAULT_KEY", + pickleKey: pickleKey, }, userId: creds.userId, deviceId: creds.deviceId, @@ -293,7 +294,7 @@ class _MatrixClientPeg implements IMatrixClientPeg { // set dehydration key after cross-signing gets set up -- we wait until // cross-signing is set up because we want to cross-sign the dehydrated - // key + // device const origGetSecretStorageKey = opts.cryptoCallbacks.getSecretStorageKey opts.cryptoCallbacks.getSecretStorageKey = async (keyinfo, ssssItemName) => { const [name, key] = await origGetSecretStorageKey(keyinfo, ssssItemName); @@ -302,6 +303,8 @@ class _MatrixClientPeg implements IMatrixClientPeg { } if (creds.rehydrationKey) { + // cache the key so that the SSSS prompt tries using it without + // prompting the user cacheDehydrationKey(creds.rehydrationKey, creds.rehydrationKeyInfo); } From ca4b11ec6a4d94fb5fa546edd1431c795d1ddee5 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Wed, 23 Sep 2020 15:27:40 +0100 Subject: [PATCH 018/233] Upgrade matrix-js-sdk to 8.4.0-rc.1 --- package.json | 2 +- yarn.lock | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 53b54cbb60..446810e058 100644 --- a/package.json +++ b/package.json @@ -78,7 +78,7 @@ "is-ip": "^2.0.0", "linkifyjs": "^2.1.9", "lodash": "^4.17.19", - "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop", + "matrix-js-sdk": "8.4.0-rc.1", "minimist": "^1.2.5", "pako": "^1.0.11", "parse5": "^5.1.1", diff --git a/yarn.lock b/yarn.lock index ad1057cdcd..fd97a1c854 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5919,9 +5919,10 @@ mathml-tag-names@^2.0.1: resolved "https://registry.yarnpkg.com/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz#4ddadd67308e780cf16a47685878ee27b736a0a3" integrity sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg== -"matrix-js-sdk@github:matrix-org/matrix-js-sdk#develop": - version "8.3.0" - resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/b9886d4f3479c041fc6d91ebc88c4a998e9d2e7c" +matrix-js-sdk@8.4.0-rc.1: + version "8.4.0-rc.1" + resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-8.4.0-rc.1.tgz#9547e6d0088ec22fc6463c3144aee8c03266c215" + integrity sha512-u5I8OesrGePVj+NoZByXwV4QBujrMPb4BlKWII4VscvVitLoD/iuz9beNvic3esNF8U3ruWVDcOwA0XQIoumQQ== dependencies: "@babel/runtime" "^7.8.3" another-json "^0.2.0" From 65923c3c55fb763ae89dc9f226a7218f94c75368 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Wed, 23 Sep 2020 15:32:46 +0100 Subject: [PATCH 019/233] Prepare changelog for v3.5.0-rc.1 --- CHANGELOG.md | 112 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 112 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6fa9cc29f9..03d066be99 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,115 @@ +Changes in [3.5.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.5.0-rc.1) (2020-09-23) +============================================================================================================= +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.4.1...v3.5.0-rc.1) + + * Upgrade JS SDK to 8.4.0-rc.1 + * Update from Weblate + [\#5246](https://github.com/matrix-org/matrix-react-sdk/pull/5246) + * Upgrade sanitize-html, set nesting limit + [\#5245](https://github.com/matrix-org/matrix-react-sdk/pull/5245) + * Add a note to use the desktop builds when seshat isn't available + [\#5225](https://github.com/matrix-org/matrix-react-sdk/pull/5225) + * Add some permission checks to the communities v2 prototype + [\#5240](https://github.com/matrix-org/matrix-react-sdk/pull/5240) + * Support HS-preferred Secure Backup setup methods + [\#5242](https://github.com/matrix-org/matrix-react-sdk/pull/5242) + * Only show User Info verify button if the other user has e2ee devices + [\#5234](https://github.com/matrix-org/matrix-react-sdk/pull/5234) + * Fix New Room List arrow key management + [\#5237](https://github.com/matrix-org/matrix-react-sdk/pull/5237) + * Fix Room Directory View & Preview actions for federated joins + [\#5235](https://github.com/matrix-org/matrix-react-sdk/pull/5235) + * Add a UI feature to disable advanced encryption options + [\#5238](https://github.com/matrix-org/matrix-react-sdk/pull/5238) + * UI Feature Flag: Communities + [\#5216](https://github.com/matrix-org/matrix-react-sdk/pull/5216) + * Rename apps back to widgets + [\#5236](https://github.com/matrix-org/matrix-react-sdk/pull/5236) + * Adjust layout and formatting of notifications / files cards + [\#5229](https://github.com/matrix-org/matrix-react-sdk/pull/5229) + * Fix Search Results Tile undefined variable access regression + [\#5232](https://github.com/matrix-org/matrix-react-sdk/pull/5232) + * Fix Cmd/Ctrl+Shift+U for File Upload + [\#5233](https://github.com/matrix-org/matrix-react-sdk/pull/5233) + * Disable the e2ee toggle when creating a room on a server with forced e2e + [\#5231](https://github.com/matrix-org/matrix-react-sdk/pull/5231) + * UI Feature Flag: Disable advanced options and tidy up some copy + [\#5215](https://github.com/matrix-org/matrix-react-sdk/pull/5215) + * UI Feature Flag: 3PIDs + [\#5228](https://github.com/matrix-org/matrix-react-sdk/pull/5228) + * Defer encryption setup until first E2EE room + [\#5219](https://github.com/matrix-org/matrix-react-sdk/pull/5219) + * Tidy devDeps, all the webpack stuff lives in the layer above + [\#5179](https://github.com/matrix-org/matrix-react-sdk/pull/5179) + * UI Feature Flag: Hide flair + [\#5214](https://github.com/matrix-org/matrix-react-sdk/pull/5214) + * UI Feature Flag: Identity server + [\#5218](https://github.com/matrix-org/matrix-react-sdk/pull/5218) + * UI Feature Flag: Share dialog QR code and social icons + [\#5221](https://github.com/matrix-org/matrix-react-sdk/pull/5221) + * UI Feature Flag: Registration, Password Reset, Deactivate + [\#5227](https://github.com/matrix-org/matrix-react-sdk/pull/5227) + * Retry joinRoom up to 5 times in the case of a 504 GATEWAY TIMEOUT + [\#5204](https://github.com/matrix-org/matrix-react-sdk/pull/5204) + * UI Feature Flag: Disable VoIP + [\#5217](https://github.com/matrix-org/matrix-react-sdk/pull/5217) + * Fix setState() usage in the constructor of RoomDirectory + [\#5224](https://github.com/matrix-org/matrix-react-sdk/pull/5224) + * Hide Analytics sections if piwik config is not provided + [\#5211](https://github.com/matrix-org/matrix-react-sdk/pull/5211) + * UI Feature Flag: Disable feedback button + [\#5213](https://github.com/matrix-org/matrix-react-sdk/pull/5213) + * Clean up UserInfo to not show a blank Power Selector for users not in room + [\#5220](https://github.com/matrix-org/matrix-react-sdk/pull/5220) + * Also hide bug reporting prompts from the Error Boundaries + [\#5212](https://github.com/matrix-org/matrix-react-sdk/pull/5212) + * Tactical improvements to 3PID invites + [\#5201](https://github.com/matrix-org/matrix-react-sdk/pull/5201) + * If no bug_report_endpoint_url, hide rageshaking from the App + [\#5210](https://github.com/matrix-org/matrix-react-sdk/pull/5210) + * Introduce a concept of UI features, using it for URL previews at first + [\#5208](https://github.com/matrix-org/matrix-react-sdk/pull/5208) + * Remove defunct "always show encryption icons" setting + [\#5207](https://github.com/matrix-org/matrix-react-sdk/pull/5207) + * Don't show Notifications Prompt Toast if user has master rule enabled + [\#5203](https://github.com/matrix-org/matrix-react-sdk/pull/5203) + * Fix Bridges tab crashing when the room does not have bridges + [\#5206](https://github.com/matrix-org/matrix-react-sdk/pull/5206) + * Don't count widgets which no longer exist towards pinned count + [\#5202](https://github.com/matrix-org/matrix-react-sdk/pull/5202) + * Fix crashes with cannot read isResizing of undefined + [\#5205](https://github.com/matrix-org/matrix-react-sdk/pull/5205) + * Prompt to remove the jitsi widget when pressing the call button + [\#5193](https://github.com/matrix-org/matrix-react-sdk/pull/5193) + * Show verification status in the room summary card + [\#5195](https://github.com/matrix-org/matrix-react-sdk/pull/5195) + * Fix user info scrolling in new card view + [\#5198](https://github.com/matrix-org/matrix-react-sdk/pull/5198) + * Fix sticker picker height + [\#5197](https://github.com/matrix-org/matrix-react-sdk/pull/5197) + * Call jitsi widgets 'group calls' + [\#5191](https://github.com/matrix-org/matrix-react-sdk/pull/5191) + * Don't show 'unpin' for persistent widgets + [\#5194](https://github.com/matrix-org/matrix-react-sdk/pull/5194) + * Split up cross-signing and secure backup settings + [\#5182](https://github.com/matrix-org/matrix-react-sdk/pull/5182) + * Fix onNewScreen to use replace when going from roomId->roomAlias + [\#5185](https://github.com/matrix-org/matrix-react-sdk/pull/5185) + * bring back 1.2M style badge counts rather than 99+ + [\#5192](https://github.com/matrix-org/matrix-react-sdk/pull/5192) + * Run the rageshake command through the bug report dialog + [\#5189](https://github.com/matrix-org/matrix-react-sdk/pull/5189) + * Account for via in pill matching regex + [\#5188](https://github.com/matrix-org/matrix-react-sdk/pull/5188) + * Remove now-unused create-react-class from lockfile + [\#5187](https://github.com/matrix-org/matrix-react-sdk/pull/5187) + * Fixed 1px jump upwards + [\#5163](https://github.com/matrix-org/matrix-react-sdk/pull/5163) + * Always allow widgets when using the local version + [\#5184](https://github.com/matrix-org/matrix-react-sdk/pull/5184) + * Migrate RoomView and RoomContext to Typescript + [\#5175](https://github.com/matrix-org/matrix-react-sdk/pull/5175) + Changes in [3.4.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.4.1) (2020-09-14) =================================================================================================== [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.4.0...v3.4.1) From 9ac3af4176f46558e2458a0b37d82654efe751f0 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Wed, 23 Sep 2020 15:32:47 +0100 Subject: [PATCH 020/233] v3.5.0-rc.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 446810e058..f19c247d0c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-react-sdk", - "version": "3.4.1", + "version": "3.5.0-rc.1", "description": "SDK for matrix.org using React", "author": "matrix.org", "repository": { From 8992dc96550f04a59619e0a9d56aff5c29bd37f2 Mon Sep 17 00:00:00 2001 From: "@a2sc:matrix.org" Date: Wed, 23 Sep 2020 20:14:03 +0000 Subject: [PATCH 021/233] Translated using Weblate (German) Currently translated at 100.0% (2375 of 2375 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/element-web/matrix-react-sdk/de/ --- src/i18n/strings/de_DE.json | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json index 9ed6119873..e851725434 100644 --- a/src/i18n/strings/de_DE.json +++ b/src/i18n/strings/de_DE.json @@ -2504,5 +2504,11 @@ "Start a conversation with someone using their name or username (like ).": "Starte ein Gespräch unter Verwendung des Namen oder Benutzernamens des Gegenübers (z. B. ).", "This won't invite them to %(communityName)s. To invite someone to %(communityName)s, click here": "Das wird sie nicht zu %(communityName)s einladen. Um jemand zu %(communityName)s einzuladen, klicke hier", "Invite someone using their name, username (like ) or share this room.": "Lade jemand mittels seinem/ihrem Namen oder Benutzernamen (z.B. ) ein, oder teile diesem Raum.", - "Unable to set up keys": "Schlüssel können nicht eingerichtet werden" + "Unable to set up keys": "Schlüssel können nicht eingerichtet werden", + "Use the Desktop app to see all encrypted files": "Nutze die Desktop-App um alle verschlüsselten Dateien zu sehen", + "Use the Desktop app to search encrypted messages": "Nutze die Desktop-App um verschlüsselte Nachrichten zu suchen", + "This version of %(brand)s does not support viewing some encrypted files": "Diese Version von %(brand)s unterstützt nicht alle verschlüsselten Dateien anzuzeigen", + "This version of %(brand)s does not support searching encrypted messages": "Diese Version von %(brand)s unterstützt nicht verschlüsselte Nachrichten zu durchsuchen", + "Cannot create rooms in this community": "Räume können in dieser Community nicht erstellt werden", + "You do not have permission to create rooms in this community.": "Du bist nicht berechtigt Räume in dieser Community zu erstellen." } From e7241f2dfdae723abd8c594cf80031e463718b9b Mon Sep 17 00:00:00 2001 From: Szimszon Date: Wed, 23 Sep 2020 14:33:40 +0000 Subject: [PATCH 022/233] Translated using Weblate (Hungarian) Currently translated at 100.0% (2375 of 2375 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/element-web/matrix-react-sdk/hu/ --- src/i18n/strings/hu.json | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/hu.json b/src/i18n/strings/hu.json index 8095401bf9..0207dcc381 100644 --- a/src/i18n/strings/hu.json +++ b/src/i18n/strings/hu.json @@ -2502,5 +2502,15 @@ "Minimize widget": "Widget minimalizálása", "Maximize widget": "Widget maximalizálása", "Your server requires encryption to be enabled in private rooms.": "A szervered megköveteli, hogy a titkosítás be legyen kapcsolva a privát szobákban.", - "Unable to set up keys": "Nem sikerült a kulcsok beállítása" + "Unable to set up keys": "Nem sikerült a kulcsok beállítása", + "Safeguard against losing access to encrypted messages & data": "Biztosítás a titkosított üzenetek és adatokhoz való hozzáférés elvesztése ellen", + "not found in storage": "a tárban nem található", + "Widgets": "Kisalkalmazások", + "Edit widgets, bridges & bots": "Kisalkalmazások, hidak és botok szerkesztése", + "Use the Desktop app to see all encrypted files": "Minden titkosított fájl eléréséhez használd az Asztali alkalmazást", + "Use the Desktop app to search encrypted messages": "A titkosított üzenetek kereséséhez használd az Asztali alkalmazást", + "This version of %(brand)s does not support viewing some encrypted files": "%(brand)s ezen verziója nem minden titkosított fájl megjelenítését támogatja", + "This version of %(brand)s does not support searching encrypted messages": "%(brand)s ezen verziója nem támogatja a keresést a titkosított üzenetekben", + "Cannot create rooms in this community": "A közösségben nem lehet szobát készíteni", + "You do not have permission to create rooms in this community.": "A közösségben szoba létrehozásához nincs jogosultságod." } From 1dc7b7468373f46750ca87ec69db375658b22855 Mon Sep 17 00:00:00 2001 From: linsui Date: Fri, 25 Sep 2020 04:58:46 +0000 Subject: [PATCH 023/233] Translated using Weblate (Chinese (Simplified)) Currently translated at 97.5% (2316 of 2375 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/element-web/matrix-react-sdk/zh_Hans/ --- src/i18n/strings/zh_Hans.json | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/zh_Hans.json b/src/i18n/strings/zh_Hans.json index eaf3b0d329..adfef89e93 100644 --- a/src/i18n/strings/zh_Hans.json +++ b/src/i18n/strings/zh_Hans.json @@ -2384,5 +2384,19 @@ "Cross-signing is ready for use.": "交叉签名已可用。", "Cross-signing is not set up.": "未设置交叉签名。", "Backup version:": "备份版本:", - "Algorithm:": "算法:" + "Algorithm:": "算法:", + "Set up Secure Backup": "设置安全备份", + "Safeguard against losing access to encrypted messages & data": "保护加密信息 & 数据的访问权", + "not found in storage": "未在存储中找到", + "Back up your encryption keys with your account data in case you lose access to your sessions. Your keys will be secured with a unique Recovery Key.": "请备份加密密钥及帐户数据,以防无法访问您的会话。您的密钥将使用唯一的恢复密钥进行保护。", + "Backup key stored:": "备份密钥已保存:", + "Backup key cached:": "备份密钥已缓存:", + "Secret storage:": "秘密存储:", + "ready": "就绪", + "not ready": "尚未就绪", + "Secure Backup": "安全备份", + "Privacy": "隐私", + "Explore community rooms": "探索社区聊天室", + "%(count)s results|one": "%(count)s 个结果", + "Room Info": "聊天室信息" } From 634ffb0140d2e13e6b9af32400e024ead6b5c577 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 25 Sep 2020 09:39:21 -0600 Subject: [PATCH 024/233] Add structure for widget messaging layer --- src/stores/widgets/SdkWidgetDriver.ts | 34 ++++++ src/stores/widgets/WidgetMessagingStore.ts | 117 +++++++++++++++++++++ src/stores/widgets/WidgetSurrogate.ts | 25 +++++ src/utils/iterables.ts | 21 ++++ src/utils/maps.ts | 17 +++ 5 files changed, 214 insertions(+) create mode 100644 src/stores/widgets/SdkWidgetDriver.ts create mode 100644 src/stores/widgets/WidgetMessagingStore.ts create mode 100644 src/stores/widgets/WidgetSurrogate.ts create mode 100644 src/utils/iterables.ts diff --git a/src/stores/widgets/SdkWidgetDriver.ts b/src/stores/widgets/SdkWidgetDriver.ts new file mode 100644 index 0000000000..1462303fa3 --- /dev/null +++ b/src/stores/widgets/SdkWidgetDriver.ts @@ -0,0 +1,34 @@ +/* + * Copyright 2020 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 { Capability, Widget, WidgetDriver, WidgetKind } from "matrix-widget-api"; +import { iterableUnion } from "../../utils/iterables"; + +export class SdkWidgetDriver extends WidgetDriver { + public constructor( + private widget: Widget, + private widgetKind: WidgetKind, + private locationEntityId: string, + private preapprovedCapabilities: Set = new Set(), + ) { + super(); + } + + public async validateCapabilities(requested: Set): Promise> { + // TODO: Prompt the user to accept capabilities + return iterableUnion(requested, this.preapprovedCapabilities); + } +} diff --git a/src/stores/widgets/WidgetMessagingStore.ts b/src/stores/widgets/WidgetMessagingStore.ts new file mode 100644 index 0000000000..6d05cae8c6 --- /dev/null +++ b/src/stores/widgets/WidgetMessagingStore.ts @@ -0,0 +1,117 @@ +/* + * Copyright 2020 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, Widget, WidgetDriver, WidgetKind } from "matrix-widget-api"; +import { AsyncStoreWithClient } from "../AsyncStoreWithClient"; +import defaultDispatcher from "../../dispatcher/dispatcher"; +import { ActionPayload } from "../../dispatcher/payloads"; +import { Room } from "matrix-js-sdk/src/models/room"; +import { WidgetSurrogate } from "./WidgetSurrogate"; +import { SdkWidgetDriver } from "./SdkWidgetDriver"; +import { EnhancedMap } from "../../utils/maps"; + +/** + * Temporary holding store for widget messaging instances. This is eventually + * going to be merged with a more complete WidgetStore, but for now it's + * easiest to split this into a single place. + */ +export class WidgetMessagingStore extends AsyncStoreWithClient { + private static internalInstance = new WidgetMessagingStore(); + + // > + private widgetMap = new EnhancedMap>(); + + public constructor() { + super(defaultDispatcher); + } + + public static get instance(): WidgetMessagingStore { + return WidgetMessagingStore.internalInstance; + } + + protected async onAction(payload: ActionPayload): Promise { + // nothing to do + } + + protected async onReady(): Promise { + // just in case + this.widgetMap.clear(); + } + + /** + * Gets the messaging instance for the widget. Returns a falsey value if none + * is present. + * @param {Room} room The room for which the widget lives within. + * @param {Widget} widget The widget to get messaging for. + * @returns {ClientWidgetApi} The messaging, or a falsey value. + */ + public messagingForRoomWidget(room: Room, widget: Widget): ClientWidgetApi { + return this.widgetMap.get(room.roomId)?.get(widget.id)?.messaging; + } + + /** + * Gets the messaging instance for the widget. Returns a falsey value if none + * is present. + * @param {Widget} widget The widget to get messaging for. + * @returns {ClientWidgetApi} The messaging, or a falsey value. + */ + public messagingForAccountWidget(widget: Widget): ClientWidgetApi { + return this.widgetMap.get(this.matrixClient?.getUserId())?.get(widget.id)?.messaging; + } + + private generateMessaging(locationId: string, widget: Widget, iframe: HTMLIFrameElement, driver: WidgetDriver) { + const messaging = new ClientWidgetApi(widget, iframe, driver); + this.widgetMap.getOrCreate(locationId, new EnhancedMap()) + .getOrCreate(widget.id, new WidgetSurrogate(widget, messaging)); + return messaging; + } + + /** + * Generates a messaging instance for the widget. If an instance already exists, it + * will be returned instead. + * @param {Room} room The room in which the widget lives. + * @param {Widget} widget The widget to generate/get messaging for. + * @param {HTMLIFrameElement} iframe The widget's iframe. + * @returns {ClientWidgetApi} The generated/cached messaging. + */ + public generateMessagingForRoomWidget(room: Room, widget: Widget, iframe: HTMLIFrameElement): ClientWidgetApi { + const existing = this.messagingForRoomWidget(room, widget); + if (existing) return existing; + + const driver = new SdkWidgetDriver(widget, WidgetKind.Room, room.roomId); + return this.generateMessaging(room.roomId, widget, iframe, driver); + } + + /** + * Generates a messaging instance for the widget. If an instance already exists, it + * will be returned instead. + * @param {Widget} widget The widget to generate/get messaging for. + * @param {HTMLIFrameElement} iframe The widget's iframe. + * @returns {ClientWidgetApi} The generated/cached messaging. + */ + public generateMessagingForAccountWidget(widget: Widget, iframe: HTMLIFrameElement): ClientWidgetApi { + if (!this.matrixClient) { + throw new Error("No matrix client to create account widgets with"); + } + + const existing = this.messagingForAccountWidget(widget); + if (existing) return existing; + + const userId = this.matrixClient.getUserId(); + const driver = new SdkWidgetDriver(widget, WidgetKind.Account, userId); + return this.generateMessaging(userId, widget, iframe, driver); + } +} diff --git a/src/stores/widgets/WidgetSurrogate.ts b/src/stores/widgets/WidgetSurrogate.ts new file mode 100644 index 0000000000..4d482124a6 --- /dev/null +++ b/src/stores/widgets/WidgetSurrogate.ts @@ -0,0 +1,25 @@ +/* + * Copyright 2020 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, Widget } from "matrix-widget-api"; + +export class WidgetSurrogate { + public constructor( + public readonly definition: Widget, + public readonly messaging: ClientWidgetApi, + ) { + } +} diff --git a/src/utils/iterables.ts b/src/utils/iterables.ts new file mode 100644 index 0000000000..3d2585906d --- /dev/null +++ b/src/utils/iterables.ts @@ -0,0 +1,21 @@ +/* + * Copyright 2020 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 { arrayUnion } from "./arrays"; + +export function iterableUnion, T>(a: C, b: C): Set { + return new Set(arrayUnion(Array.from(a), Array.from(b))); +} diff --git a/src/utils/maps.ts b/src/utils/maps.ts index 96832094f0..630e0af286 100644 --- a/src/utils/maps.ts +++ b/src/utils/maps.ts @@ -44,3 +44,20 @@ export function mapKeyChanges(a: Map, b: Map): K[] { const diff = mapDiff(a, b); return arrayMerge(diff.removed, diff.added, diff.changed); } + +/** + * A Map with added utility. + */ +export class EnhancedMap extends Map { + public constructor(entries?: Iterable<[K, V]>) { + super(entries); + } + + public getOrCreate(key: K, def: V): V { + if (this.has(key)) { + return this.get(key); + } + this.set(key, def); + return def; + } +} From 96fa34eecfc251507b9e4788a3cdcb1214694d40 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Sat, 26 Sep 2020 18:40:26 -0600 Subject: [PATCH 025/233] Add stop functions --- src/stores/widgets/WidgetMessagingStore.ts | 20 ++++++++++++++++++++ src/utils/maps.ts | 6 ++++++ 2 files changed, 26 insertions(+) diff --git a/src/stores/widgets/WidgetMessagingStore.ts b/src/stores/widgets/WidgetMessagingStore.ts index 6d05cae8c6..dfa8eed943 100644 --- a/src/stores/widgets/WidgetMessagingStore.ts +++ b/src/stores/widgets/WidgetMessagingStore.ts @@ -114,4 +114,24 @@ export class WidgetMessagingStore extends AsyncStoreWithClient { const driver = new SdkWidgetDriver(widget, WidgetKind.Account, userId); return this.generateMessaging(userId, widget, iframe, driver); } + + /** + * Stops the messaging instance for the widget, unregistering it. + * @param {Room} room The room where the widget resides. + * @param {Widget} widget The widget + */ + public stopMessagingForRoomWidget(room: Room, widget: Widget) { + const api = this.widgetMap.getOrCreate(room.roomId, new EnhancedMap()).remove(widget.id); + if (api) api.messaging.stop(); + } + + /** + * Stops the messaging instance for the widget, unregistering it. + * @param {Widget} widget The widget + */ + public stopMessagingForAccountWidget(widget: Widget) { + if (!this.matrixClient) return; + const api = this.widgetMap.getOrCreate(this.matrixClient.getUserId(), new EnhancedMap()).remove(widget.id); + if (api) api.messaging.stop(); + } } diff --git a/src/utils/maps.ts b/src/utils/maps.ts index 630e0af286..57d84bd33f 100644 --- a/src/utils/maps.ts +++ b/src/utils/maps.ts @@ -60,4 +60,10 @@ export class EnhancedMap extends Map { this.set(key, def); return def; } + + public remove(key: K): V { + const v = this.get(key); + this.delete(key); + return v; + } } From 49bbc98d011001cc9195ae57312ff34c0d1ee01b Mon Sep 17 00:00:00 2001 From: Nikita Epifanov Date: Sat, 26 Sep 2020 09:34:59 +0000 Subject: [PATCH 026/233] Translated using Weblate (Russian) Currently translated at 100.0% (2369 of 2369 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/element-web/matrix-react-sdk/ru/ --- src/i18n/strings/ru.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/i18n/strings/ru.json b/src/i18n/strings/ru.json index 946e8a5c89..1f423146e8 100644 --- a/src/i18n/strings/ru.json +++ b/src/i18n/strings/ru.json @@ -1325,7 +1325,7 @@ "Invalid homeserver discovery response": "Неверный ответ при попытке обнаружения домашнего сервера", "Failed to get autodiscovery configuration from server": "Не удалось получить конфигурацию автообнаружения с сервера", "Invalid base_url for m.homeserver": "Неверный base_url для m.homeserver", - "Homeserver URL does not appear to be a valid Matrix homeserver": "URL-адрес сервера не является действительным URL-адресом сервера Матрица", + "Homeserver URL does not appear to be a valid Matrix homeserver": "URL-адрес домашнего сервера не является допустимым домашним сервером Matrix", "Invalid identity server discovery response": "Неверный ответ на запрос идентификации сервера", "Invalid base_url for m.identity_server": "Неверный base_url для m.identity_server", "Identity server URL does not appear to be a valid identity server": "URL-адрес сервера идентификации не является действительным сервером идентификации", From fe5e1f4543c40e25403ea4f4ce61323c74cd56c1 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Mon, 28 Sep 2020 14:25:51 +0100 Subject: [PATCH 027/233] Upgrade matrix-js-sdk to 8.4.0 --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index f19c247d0c..0cb34ec3c7 100644 --- a/package.json +++ b/package.json @@ -78,7 +78,7 @@ "is-ip": "^2.0.0", "linkifyjs": "^2.1.9", "lodash": "^4.17.19", - "matrix-js-sdk": "8.4.0-rc.1", + "matrix-js-sdk": "8.4.0", "minimist": "^1.2.5", "pako": "^1.0.11", "parse5": "^5.1.1", diff --git a/yarn.lock b/yarn.lock index fd97a1c854..aa0e161f4c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5919,10 +5919,10 @@ mathml-tag-names@^2.0.1: resolved "https://registry.yarnpkg.com/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz#4ddadd67308e780cf16a47685878ee27b736a0a3" integrity sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg== -matrix-js-sdk@8.4.0-rc.1: - version "8.4.0-rc.1" - resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-8.4.0-rc.1.tgz#9547e6d0088ec22fc6463c3144aee8c03266c215" - integrity sha512-u5I8OesrGePVj+NoZByXwV4QBujrMPb4BlKWII4VscvVitLoD/iuz9beNvic3esNF8U3ruWVDcOwA0XQIoumQQ== +matrix-js-sdk@8.4.0: + version "8.4.0" + resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-8.4.0.tgz#40c42c7d6800ebec30722d7ce8f0a324bd519208" + integrity sha512-znXzcDfRQazoQpUkDKCuGB5T/uIm+lJaVa1a2xDUB5xuPJgBcAYpdWJRQBxDZ50s2GhUy81+lsmuZK9BC4fLqQ== dependencies: "@babel/runtime" "^7.8.3" another-json "^0.2.0" From 603415c2ece8776dc16a6da031c9143615f5151f Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 28 Sep 2020 16:20:16 +0200 Subject: [PATCH 028/233] fix index mismatch --- res/themes/light-custom/css/_custom.scss | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/res/themes/light-custom/css/_custom.scss b/res/themes/light-custom/css/_custom.scss index b830e86e02..6bb46e8a67 100644 --- a/res/themes/light-custom/css/_custom.scss +++ b/res/themes/light-custom/css/_custom.scss @@ -124,15 +124,15 @@ $pinned-unread-color: var(--warning-color); $warning-color: var(--warning-color); $button-danger-disabled-bg-color: var(--warning-color-50pct); // still needs alpha at 0.5 // -// --username colors -$username-variant1-color: var(--username-colors_1, $username-variant1-color); -$username-variant2-color: var(--username-colors_2, $username-variant2-color); -$username-variant3-color: var(--username-colors_3, $username-variant3-color); -$username-variant4-color: var(--username-colors_4, $username-variant4-color); -$username-variant5-color: var(--username-colors_5, $username-variant5-color); -$username-variant6-color: var(--username-colors_6, $username-variant6-color); -$username-variant7-color: var(--username-colors_7, $username-variant7-color); -$username-variant8-color: var(--username-colors_8, $username-variant8-color); +// --username colors (which use a 0-based index) +$username-variant1-color: var(--username-colors_0, $username-variant1-color); +$username-variant2-color: var(--username-colors_1, $username-variant2-color); +$username-variant3-color: var(--username-colors_2, $username-variant3-color); +$username-variant4-color: var(--username-colors_3, $username-variant4-color); +$username-variant5-color: var(--username-colors_4, $username-variant5-color); +$username-variant6-color: var(--username-colors_5, $username-variant6-color); +$username-variant7-color: var(--username-colors_6, $username-variant7-color); +$username-variant8-color: var(--username-colors_7, $username-variant8-color); // // --timeline-highlights-color $event-selected-color: var(--timeline-highlights-color); From 68734026667afb708f80ff8d765f33ad94f19e81 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 28 Sep 2020 15:47:03 +0100 Subject: [PATCH 029/233] Convert emojipicker to typescript Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- .../emojipicker/{Category.js => Category.tsx} | 61 ++++++--- .../views/emojipicker/{Emoji.js => Emoji.tsx} | 20 +-- .../{EmojiPicker.js => EmojiPicker.tsx} | 118 ++++++++++-------- .../emojipicker/{Header.js => Header.tsx} | 28 +++-- .../emojipicker/{Preview.js => Preview.tsx} | 13 +- .../{QuickReactions.js => QuickReactions.tsx} | 46 ++++--- .../{ReactionPicker.js => ReactionPicker.tsx} | 49 ++++---- .../emojipicker/{Search.js => Search.tsx} | 36 +++--- 8 files changed, 211 insertions(+), 160 deletions(-) rename src/components/views/emojipicker/{Category.js => Category.tsx} (68%) rename src/components/views/emojipicker/{Emoji.js => Emoji.tsx} (81%) rename src/components/views/emojipicker/{EmojiPicker.js => EmojiPicker.tsx} (70%) rename src/components/views/emojipicker/{Header.js => Header.tsx} (83%) rename src/components/views/emojipicker/{Preview.js => Preview.tsx} (88%) rename src/components/views/emojipicker/{QuickReactions.js => QuickReactions.tsx} (69%) rename src/components/views/emojipicker/{ReactionPicker.js => ReactionPicker.tsx} (77%) rename src/components/views/emojipicker/{Search.js => Search.tsx} (64%) diff --git a/src/components/views/emojipicker/Category.js b/src/components/views/emojipicker/Category.tsx similarity index 68% rename from src/components/views/emojipicker/Category.js rename to src/components/views/emojipicker/Category.tsx index eb3f83dcdf..c4feaac8ae 100644 --- a/src/components/views/emojipicker/Category.js +++ b/src/components/views/emojipicker/Category.tsx @@ -1,5 +1,6 @@ /* Copyright 2019 Tulir Asokan +Copyright 2020 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. @@ -14,32 +15,53 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from 'react'; -import PropTypes from 'prop-types'; +import React, {RefObject} from 'react'; + import { CATEGORY_HEADER_HEIGHT, EMOJI_HEIGHT, EMOJIS_PER_ROW } from "./EmojiPicker"; -import * as sdk from '../../../index'; +import LazyRenderList from "../elements/LazyRenderList"; +import {DATA_BY_CATEGORY, IEmoji} from "../../../emoji"; +import Emoji from './Emoji'; const OVERFLOW_ROWS = 3; -class Category extends React.PureComponent { - static propTypes = { - emojis: PropTypes.arrayOf(PropTypes.object).isRequired, - name: PropTypes.string.isRequired, - id: PropTypes.string.isRequired, - onMouseEnter: PropTypes.func.isRequired, - onMouseLeave: PropTypes.func.isRequired, - onClick: PropTypes.func.isRequired, - selectedEmojis: PropTypes.instanceOf(Set), - }; +export type CategoryKey = (keyof typeof DATA_BY_CATEGORY) | "recent"; - _renderEmojiRow = (rowIndex) => { +export interface ICategory { + id: CategoryKey; + name: string; + enabled: boolean; + visible: boolean; + ref: RefObject; +} + +interface IProps { + id: string; + name: string; + emojis: IEmoji[]; + selectedEmojis: Set; + heightBefore: number; + viewportHeight: number; + scrollTop: number; + onClick(emoji: IEmoji): void; + onMouseEnter(emoji: IEmoji): void; + onMouseLeave(emoji: IEmoji): void; +} + +class Category extends React.PureComponent { + private renderEmojiRow = (rowIndex: number) => { const { onClick, onMouseEnter, onMouseLeave, selectedEmojis, emojis } = this.props; const emojisForRow = emojis.slice(rowIndex * 8, (rowIndex + 1) * 8); - const Emoji = sdk.getComponent("emojipicker.Emoji"); return (
{ - emojisForRow.map(emoji => - ) + emojisForRow.map(emoji => (( + + ))) }
); }; @@ -52,7 +74,6 @@ class Category extends React.PureComponent { for (let counter = 0; counter < rows.length; ++counter) { rows[counter] = counter; } - const LazyRenderList = sdk.getComponent('elements.LazyRenderList'); const viewportTop = scrollTop; const viewportBottom = viewportTop + viewportHeight; @@ -84,7 +105,7 @@ class Category extends React.PureComponent { height={localHeight} overflowItems={OVERFLOW_ROWS} overflowMargin={0} - renderItem={this._renderEmojiRow}> + renderItem={this.renderEmojiRow}> ); diff --git a/src/components/views/emojipicker/Emoji.js b/src/components/views/emojipicker/Emoji.tsx similarity index 81% rename from src/components/views/emojipicker/Emoji.js rename to src/components/views/emojipicker/Emoji.tsx index 36aa4ff782..5d715fb935 100644 --- a/src/components/views/emojipicker/Emoji.js +++ b/src/components/views/emojipicker/Emoji.tsx @@ -1,5 +1,6 @@ /* Copyright 2019 Tulir Asokan +Copyright 2020 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,18 +16,19 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; + import {MenuItem} from "../../structures/ContextMenu"; +import {IEmoji} from "../../../emoji"; -class Emoji extends React.PureComponent { - static propTypes = { - onClick: PropTypes.func, - onMouseEnter: PropTypes.func, - onMouseLeave: PropTypes.func, - emoji: PropTypes.object.isRequired, - selectedEmojis: PropTypes.instanceOf(Set), - }; +interface IProps { + emoji: IEmoji; + selectedEmojis?: Set; + onClick(emoji: IEmoji): void; + onMouseEnter(emoji: IEmoji): void; + onMouseLeave(emoji: IEmoji): void; +} +class Emoji extends React.PureComponent { render() { const { onClick, onMouseEnter, onMouseLeave, emoji, selectedEmojis } = this.props; const isSelected = selectedEmojis && selectedEmojis.has(emoji.unicode); diff --git a/src/components/views/emojipicker/EmojiPicker.js b/src/components/views/emojipicker/EmojiPicker.tsx similarity index 70% rename from src/components/views/emojipicker/EmojiPicker.js rename to src/components/views/emojipicker/EmojiPicker.tsx index 16a0fc67e7..3aa6b109b2 100644 --- a/src/components/views/emojipicker/EmojiPicker.js +++ b/src/components/views/emojipicker/EmojiPicker.tsx @@ -1,5 +1,6 @@ /* Copyright 2019 Tulir Asokan +Copyright 2020 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,25 +16,43 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; -import * as sdk from '../../../index'; import { _t } from '../../../languageHandler'; - import * as recent from '../../../emojipicker/recent'; -import {DATA_BY_CATEGORY, getEmojiFromUnicode} from "../../../emoji"; +import {DATA_BY_CATEGORY, getEmojiFromUnicode, IEmoji} from "../../../emoji"; import AutoHideScrollbar from "../../structures/AutoHideScrollbar"; +import Header from "./Header"; +import Search from "./Search"; +import Preview from "./Preview"; +import QuickReactions from "./QuickReactions"; +import Category, {ICategory, CategoryKey} from "./Category"; export const CATEGORY_HEADER_HEIGHT = 22; export const EMOJI_HEIGHT = 37; export const EMOJIS_PER_ROW = 8; -class EmojiPicker extends React.Component { - static propTypes = { - onChoose: PropTypes.func.isRequired, - selectedEmojis: PropTypes.instanceOf(Set), - showQuickReactions: PropTypes.bool, - }; +interface IProps { + selectedEmojis: Set; + showQuickReactions?: boolean; + onChoose(unicode: string): boolean; +} + +interface IState { + filter: string; + previewEmoji?: IEmoji; + scrollTop: number; + // initial estimation of height, dialog is hardcoded to 450px height. + // should be enough to never have blank rows of emojis as + // 3 rows of overflow are also rendered. The actual value is updated on scroll. + viewportHeight: number; +} + +class EmojiPicker extends React.Component { + private readonly recentlyUsed: IEmoji[]; + private readonly memoizedDataByCategory: Record; + private readonly categories: ICategory[]; + + private bodyRef = React.createRef(); constructor(props) { super(props); @@ -42,9 +61,6 @@ class EmojiPicker extends React.Component { filter: "", previewEmoji: null, scrollTop: 0, - // initial estimation of height, dialog is hardcoded to 450px height. - // should be enough to never have blank rows of emojis as - // 3 rows of overflow are also rendered. The actual value is updated on scroll. viewportHeight: 280, }; @@ -110,18 +126,9 @@ class EmojiPicker extends React.Component { visible: false, ref: React.createRef(), }]; - - this.bodyRef = React.createRef(); - - this.onChangeFilter = this.onChangeFilter.bind(this); - this.onHoverEmoji = this.onHoverEmoji.bind(this); - this.onHoverEmojiEnd = this.onHoverEmojiEnd.bind(this); - this.onClickEmoji = this.onClickEmoji.bind(this); - this.scrollToCategory = this.scrollToCategory.bind(this); - this.updateVisibility = this.updateVisibility.bind(this); } - onScroll = () => { + private onScroll = () => { const body = this.bodyRef.current; this.setState({ scrollTop: body.scrollTop, @@ -130,7 +137,7 @@ class EmojiPicker extends React.Component { this.updateVisibility(); }; - updateVisibility() { + private updateVisibility = () => { const body = this.bodyRef.current; const rect = body.getBoundingClientRect(); for (const cat of this.categories) { @@ -147,21 +154,21 @@ class EmojiPicker extends React.Component { // We update this here instead of through React to avoid re-render on scroll. if (cat.visible) { cat.ref.current.classList.add("mx_EmojiPicker_anchor_visible"); - cat.ref.current.setAttribute("aria-selected", true); - cat.ref.current.setAttribute("tabindex", 0); + cat.ref.current.setAttribute("aria-selected", "true"); + cat.ref.current.setAttribute("tabindex", "0"); } else { cat.ref.current.classList.remove("mx_EmojiPicker_anchor_visible"); - cat.ref.current.setAttribute("aria-selected", false); - cat.ref.current.setAttribute("tabindex", -1); + cat.ref.current.setAttribute("aria-selected", "false"); + cat.ref.current.setAttribute("tabindex", "-1"); } } - } + }; - scrollToCategory(category) { + private scrollToCategory = (category: string) => { this.bodyRef.current.querySelector(`[data-category-id="${category}"]`).scrollIntoView(); - } + }; - onChangeFilter(filter) { + private onChangeFilter = (filter: string) => { filter = filter.toLowerCase(); // filter is case insensitive stored lower-case for (const cat of this.categories) { let emojis; @@ -181,27 +188,27 @@ class EmojiPicker extends React.Component { // Header underlines need to be updated, but updating requires knowing // where the categories are, so we wait for a tick. setTimeout(this.updateVisibility, 0); - } + }; - onHoverEmoji(emoji) { + private onHoverEmoji = (emoji: IEmoji) => { this.setState({ previewEmoji: emoji, }); - } + }; - onHoverEmojiEnd(emoji) { + private onHoverEmojiEnd = (emoji: IEmoji) => { this.setState({ previewEmoji: null, }); - } + }; - onClickEmoji(emoji) { + private onClickEmoji = (emoji: IEmoji) => { if (this.props.onChoose(emoji.unicode) !== false) { recent.add(emoji.unicode); } - } + }; - _categoryHeightForEmojiCount(count) { + private static categoryHeightForEmojiCount(count: number) { if (count === 0) { return 0; } @@ -209,25 +216,30 @@ class EmojiPicker extends React.Component { } render() { - const Header = sdk.getComponent("emojipicker.Header"); - const Search = sdk.getComponent("emojipicker.Search"); - const Category = sdk.getComponent("emojipicker.Category"); - const Preview = sdk.getComponent("emojipicker.Preview"); - const QuickReactions = sdk.getComponent("emojipicker.QuickReactions"); let heightBefore = 0; return (
-
+
- this.bodyRef.current = e} onScroll={this.onScroll}> + {this.categories.map(category => { const emojis = this.memoizedDataByCategory[category.id]; - const categoryElement = (); - const height = this._categoryHeightForEmojiCount(emojis.length); + const categoryElement = (( + + )); + const height = EmojiPicker.categoryHeightForEmojiCount(emojis.length); heightBefore += height; return categoryElement; })} diff --git a/src/components/views/emojipicker/Header.js b/src/components/views/emojipicker/Header.tsx similarity index 83% rename from src/components/views/emojipicker/Header.js rename to src/components/views/emojipicker/Header.tsx index c53437e02d..9a93722483 100644 --- a/src/components/views/emojipicker/Header.js +++ b/src/components/views/emojipicker/Header.tsx @@ -1,5 +1,6 @@ /* Copyright 2019 Tulir Asokan +Copyright 2020 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,19 +16,19 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; import classNames from "classnames"; import {_t} from "../../../languageHandler"; import {Key} from "../../../Keyboard"; +import {CategoryKey, ICategory} from "./Category"; -class Header extends React.PureComponent { - static propTypes = { - categories: PropTypes.arrayOf(PropTypes.object).isRequired, - onAnchorClick: PropTypes.func.isRequired, - }; +interface IProps { + categories: ICategory[]; + onAnchorClick(id: CategoryKey): void +} - findNearestEnabled(index, delta) { +class Header extends React.PureComponent { + private findNearestEnabled(index: number, delta: number) { index += this.props.categories.length; const cats = [...this.props.categories, ...this.props.categories, ...this.props.categories]; @@ -37,12 +38,12 @@ class Header extends React.PureComponent { } } - changeCategoryRelative(delta) { + private changeCategoryRelative(delta: number) { const current = this.props.categories.findIndex(c => c.visible); this.changeCategoryAbsolute(current + delta, delta); } - changeCategoryAbsolute(index, delta=1) { + private changeCategoryAbsolute(index: number, delta=1) { const category = this.props.categories[this.findNearestEnabled(index, delta)]; if (category) { this.props.onAnchorClick(category.id); @@ -52,7 +53,7 @@ class Header extends React.PureComponent { // Implements ARIA Tabs with Automatic Activation pattern // https://www.w3.org/TR/wai-aria-practices/examples/tabs/tabs-1/tabs.html - onKeyDown = (ev) => { + private onKeyDown = (ev: React.KeyboardEvent) => { let handled = true; switch (ev.key) { case Key.ARROW_LEFT: @@ -80,7 +81,12 @@ class Header extends React.PureComponent { render() { return ( -