From 2f988bc97fc1e414bd70149ac5f77301a3ef2833 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Thu, 26 Nov 2020 13:51:03 +0100 Subject: [PATCH 01/52] Added UI --- .../views/settings/SpellCheckSettings.tsx | 111 ++++++++++++++++++ .../tabs/user/GeneralUserSettingsTab.js | 18 +++ 2 files changed, 129 insertions(+) create mode 100644 src/components/views/settings/SpellCheckSettings.tsx diff --git a/src/components/views/settings/SpellCheckSettings.tsx b/src/components/views/settings/SpellCheckSettings.tsx new file mode 100644 index 0000000000..1bdcd882c9 --- /dev/null +++ b/src/components/views/settings/SpellCheckSettings.tsx @@ -0,0 +1,111 @@ +/* +Copyright 2019 New Vector Ltd + +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 LanguageDropdown from "../../../components/views/elements/LanguageDropdown"; +import AccessibleButton from "../../../components/views/elements/AccessibleButton"; +import {_t} from "../../../languageHandler"; + +interface ExistingSpellCheckLanguageIProps { + language: string, + onRemoved(language: string), +}; + +interface SpellCheckLanguagesIProps { + languages: Array, + onLanguagesChange(languages: Array), +}; + +interface SpellCheckLanguagesIState { + newLanguage: string, +} + +export class ExistingSpellCheckLanguage extends React.Component { + _onRemove = (e) => { + e.stopPropagation(); + e.preventDefault(); + + return this.props.onRemoved(this.props.language); + }; + + render() { + return ( +
+ {this.props.language} + + {_t("Remove")} + +
+ ); + } +} + +export default class SpellCheckLanguages extends React.Component { + constructor(props) { + super(props); + this.state = { + newLanguage: "", + } + } + + _onRemoved = (language) => { + const languages = this.props.languages.filter((e) => e !== language); + this.props.onLanguagesChange(languages); + }; + + _onAddClick = (e) => { + e.stopPropagation(); + e.preventDefault(); + + const language = this.state.newLanguage; + + if (!language) return; + if (this.props.languages.includes(language)) return; + + this.props.languages.push(language) + this.props.onLanguagesChange(this.props.languages); + }; + + _onNewLanguageChange = (language: string) => { + if (this.state.newLanguage === language) return; + this.setState({newLanguage: language}); + }; + + render() { + const existingSpellCheckLanguages = this.props.languages.map((e) => { + return ; + }); + + let addButton = ( + + {_t("Add")} + + ); + + return ( +
+ {existingSpellCheckLanguages} +
+ + {addButton} + +
+ ); + }; +} diff --git a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js index 35285351ab..6d04d83047 100644 --- a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js @@ -22,6 +22,7 @@ import ProfileSettings from "../../ProfileSettings"; import * as languageHandler from "../../../../../languageHandler"; import SettingsStore from "../../../../../settings/SettingsStore"; import LanguageDropdown from "../../../elements/LanguageDropdown"; +import SpellCheckSettings from "../../SpellCheckSettings" import AccessibleButton from "../../../elements/AccessibleButton"; import DeactivateAccountDialog from "../../../dialogs/DeactivateAccountDialog"; import PropTypes from "prop-types"; @@ -49,6 +50,7 @@ export default class GeneralUserSettingsTab extends React.Component { this.state = { language: languageHandler.getCurrentLanguage(), + spellCheckLanguages: [], haveIdServer: Boolean(MatrixClientPeg.get().getIdentityServerUrl()), serverSupportsSeparateAddAndBind: null, idServerHasUnsignedTerms: false, @@ -182,6 +184,10 @@ export default class GeneralUserSettingsTab extends React.Component { PlatformPeg.get().reload(); }; + _onSpellCheckLanguagesChange = (languages) => { + this.setState({spellCheckLanguages: languages}) + }; + _onPasswordChangeError = (err) => { // TODO: Figure out a design that doesn't involve replacing the current dialog let errMsg = err.error || ""; @@ -303,6 +309,17 @@ export default class GeneralUserSettingsTab extends React.Component { ); } + _renderSpellCheckSection() { + return ( +
+ {_t("Spell checking")} + +
+ ); + } + _renderDiscoverySection() { const SetIdServer = sdk.getComponent("views.settings.SetIdServer"); @@ -409,6 +426,7 @@ export default class GeneralUserSettingsTab extends React.Component { {this._renderProfileSection()} {this._renderAccountSection()} {this._renderLanguageSection()} + {this._renderSpellCheckSection()} { discoverySection } {this._renderIntegrationManagerSection() /* Has its own title */} { accountManagementSection } From 051368eaab50e4a7d6f2ce554ddff0ed957bb965 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Thu, 26 Nov 2020 13:53:22 +0100 Subject: [PATCH 02/52] Fix i18n --- src/i18n/strings/en_EN.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 0d50128f32..7fbcc1a350 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1109,6 +1109,7 @@ "Use an Integration Manager to manage bots, widgets, and sticker packs.": "Use an Integration Manager to manage bots, widgets, and sticker packs.", "Manage integrations": "Manage integrations", "Integration Managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "Integration Managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.", + "Add": "Add", "Error encountered (%(errorDetail)s).": "Error encountered (%(errorDetail)s).", "Checking for an update...": "Checking for an update...", "No update available.": "No update available.", @@ -1140,6 +1141,7 @@ "Set a new account password...": "Set a new account password...", "Account": "Account", "Language and region": "Language and region", + "Spell checking": "Spell checking", "Agree to the identity server (%(serverName)s) Terms of Service to allow yourself to be discoverable by email address or phone number.": "Agree to the identity server (%(serverName)s) Terms of Service to allow yourself to be discoverable by email address or phone number.", "Account management": "Account management", "Deactivating your account is a permanent action - be careful!": "Deactivating your account is a permanent action - be careful!", @@ -1337,7 +1339,6 @@ "Invalid Email Address": "Invalid Email Address", "This doesn't appear to be a valid email address": "This doesn't appear to be a valid email address", "Unable to add email address": "Unable to add email address", - "Add": "Add", "We've sent you an email to verify your address. Please follow the instructions there and then click the button below.": "We've sent you an email to verify your address. Please follow the instructions there and then click the button below.", "Email Address": "Email Address", "Remove %(phone)s?": "Remove %(phone)s?", From 557e650a2c2e2eb24584f21bd4175e69cc7500a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sat, 28 Nov 2020 19:37:49 +0100 Subject: [PATCH 03/52] Added spell-check-languages setting --- src/settings/Settings.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/settings/Settings.ts b/src/settings/Settings.ts index 31e133be72..409cd293d2 100644 --- a/src/settings/Settings.ts +++ b/src/settings/Settings.ts @@ -402,6 +402,10 @@ export const SETTINGS: {[setting: string]: ISetting} = { supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG, default: "en", }, + "spell-check-languages": { + supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG, + default: [], + }, "breadcrumb_rooms": { // not really a setting supportedLevels: [SettingLevel.ACCOUNT], From 43daec03e24820a485f97da0b5cb0311f137e729 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sat, 28 Nov 2020 19:38:52 +0100 Subject: [PATCH 04/52] Added setSpellCheckLanguages() method --- src/languageHandler.tsx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/languageHandler.tsx b/src/languageHandler.tsx index b61f57d4b3..9b9e304294 100644 --- a/src/languageHandler.tsx +++ b/src/languageHandler.tsx @@ -346,6 +346,13 @@ export function setLanguage(preferredLangs: string | string[]) { }); } +export function setSpellCheckLanguages(preferredLangs: string[]) { + const plaf = PlatformPeg.get(); + if (plaf) { + plaf.setLanguage(preferredLangs); + } +} + export function getAllLanguagesFromJson() { return getLangsJson().then((langsObject) => { const langs = []; From 5e4f9907cf87e02b41791ce729b38d7474dcbf01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sat, 28 Nov 2020 19:39:09 +0100 Subject: [PATCH 05/52] Added persistance --- .../views/settings/tabs/user/GeneralUserSettingsTab.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js index 6d04d83047..585f4fd5b7 100644 --- a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js @@ -50,7 +50,7 @@ export default class GeneralUserSettingsTab extends React.Component { this.state = { language: languageHandler.getCurrentLanguage(), - spellCheckLanguages: [], + spellCheckLanguages: SettingsStore.getValue("spell-check-languages", null, /*excludeDefault=*/true), haveIdServer: Boolean(MatrixClientPeg.get().getIdentityServerUrl()), serverSupportsSeparateAddAndBind: null, idServerHasUnsignedTerms: false, @@ -185,7 +185,9 @@ export default class GeneralUserSettingsTab extends React.Component { }; _onSpellCheckLanguagesChange = (languages) => { + SettingsStore.setValue("spell-check-languages", null, SettingLevel.DEVICE, languages); this.setState({spellCheckLanguages: languages}) + PlatformPeg.get().reload(); }; _onPasswordChangeError = (err) => { From f0bbed0c44270f8411c0ce0f4ee0cf08142a1c1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sun, 29 Nov 2020 12:17:07 +0100 Subject: [PATCH 06/52] Allow default value --- .../views/settings/tabs/user/GeneralUserSettingsTab.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js index 585f4fd5b7..68a16463b0 100644 --- a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js @@ -50,7 +50,7 @@ export default class GeneralUserSettingsTab extends React.Component { this.state = { language: languageHandler.getCurrentLanguage(), - spellCheckLanguages: SettingsStore.getValue("spell-check-languages", null, /*excludeDefault=*/true), + spellCheckLanguages: SettingsStore.getValue("spell-check-languages", null, false), haveIdServer: Boolean(MatrixClientPeg.get().getIdentityServerUrl()), serverSupportsSeparateAddAndBind: null, idServerHasUnsignedTerms: false, From 8f40cd39fda1ee83ec6b177ba7935f913ed5a45b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sun, 29 Nov 2020 12:29:05 +0100 Subject: [PATCH 07/52] Added styling --- res/css/_components.scss | 1 + .../views/settings/_SpellCheckLanguages.scss | 36 +++++++++++++++++++ .../views/settings/SpellCheckSettings.tsx | 9 +++-- .../tabs/user/GeneralUserSettingsTab.js | 7 ++-- 4 files changed, 44 insertions(+), 9 deletions(-) create mode 100644 res/css/views/settings/_SpellCheckLanguages.scss diff --git a/res/css/_components.scss b/res/css/_components.scss index 445ed70ff4..1eb4b91a31 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -207,6 +207,7 @@ @import "./views/settings/_DevicesPanel.scss"; @import "./views/settings/_E2eAdvancedPanel.scss"; @import "./views/settings/_EmailAddresses.scss"; +@import "./views/settings/_SpellCheckLanguages.scss"; @import "./views/settings/_IntegrationManager.scss"; @import "./views/settings/_Notifications.scss"; @import "./views/settings/_PhoneNumbers.scss"; diff --git a/res/css/views/settings/_SpellCheckLanguages.scss b/res/css/views/settings/_SpellCheckLanguages.scss new file mode 100644 index 0000000000..734f669f0e --- /dev/null +++ b/res/css/views/settings/_SpellCheckLanguages.scss @@ -0,0 +1,36 @@ +/* +Copyright 2019 New Vector Ltd +Copyright 2019 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +.mx_ExistingSpellCheckLanguage { + display: flex; + align-items: center; + margin-bottom: 5px; +} + +.mx_ExistingSpellCheckLanguage_language { + flex: 1; + margin-right: 10px; +} + +.mx_GeneralUserSettingsTab_spellCheckLanguageInput { + margin-top: 1em; + margin-bottom: 1em; +} + +.mx_SpellCheckLanguages { + @mixin mx_Settings_fullWidthField; +} \ No newline at end of file diff --git a/src/components/views/settings/SpellCheckSettings.tsx b/src/components/views/settings/SpellCheckSettings.tsx index 1bdcd882c9..befd98112e 100644 --- a/src/components/views/settings/SpellCheckSettings.tsx +++ b/src/components/views/settings/SpellCheckSettings.tsx @@ -43,8 +43,8 @@ export class ExistingSpellCheckLanguage extends React.Component - {this.props.language} +
+ {this.props.language} {_t("Remove")} @@ -96,10 +96,9 @@ export default class SpellCheckLanguages extends React.Component +
{existingSpellCheckLanguages} -
+ diff --git a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js index 68a16463b0..258ff6d318 100644 --- a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js @@ -313,11 +313,10 @@ export default class GeneralUserSettingsTab extends React.Component { _renderSpellCheckSection() { return ( -
+
{_t("Spell checking")} - +
); } From 7609f2004e6a004c22e0da189f58cee823bf4468 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sun, 29 Nov 2020 13:22:50 +0100 Subject: [PATCH 08/52] Added newline to end --- res/css/views/settings/_SpellCheckLanguages.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/css/views/settings/_SpellCheckLanguages.scss b/res/css/views/settings/_SpellCheckLanguages.scss index 734f669f0e..ddfa0bf9e0 100644 --- a/res/css/views/settings/_SpellCheckLanguages.scss +++ b/res/css/views/settings/_SpellCheckLanguages.scss @@ -33,4 +33,4 @@ limitations under the License. .mx_SpellCheckLanguages { @mixin mx_Settings_fullWidthField; -} \ No newline at end of file +} From ead00dcdede9e6a24904599baba39bc91de0681a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sun, 29 Nov 2020 14:46:09 +0100 Subject: [PATCH 09/52] Set spell-check languages without reloading --- .../views/settings/tabs/user/GeneralUserSettingsTab.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js index 258ff6d318..ad7e04d677 100644 --- a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js @@ -187,7 +187,11 @@ export default class GeneralUserSettingsTab extends React.Component { _onSpellCheckLanguagesChange = (languages) => { SettingsStore.setValue("spell-check-languages", null, SettingLevel.DEVICE, languages); this.setState({spellCheckLanguages: languages}) - PlatformPeg.get().reload(); + + const plaf = PlatformPeg.get(); + if (plaf) { + plaf.setLanguage(languages); + } }; _onPasswordChangeError = (err) => { From 38080c5b2bccafb7dae5c9737e69ad7f295f1d7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sun, 29 Nov 2020 20:46:47 +0100 Subject: [PATCH 10/52] Added getAvailableSpellCheckLanguages() methods --- src/BasePlatform.ts | 4 ++++ src/languageHandler.tsx | 5 +++++ 2 files changed, 9 insertions(+) diff --git a/src/BasePlatform.ts b/src/BasePlatform.ts index 0a1f06f0b3..9ac35092a7 100644 --- a/src/BasePlatform.ts +++ b/src/BasePlatform.ts @@ -237,6 +237,10 @@ export default abstract class BasePlatform { setLanguage(preferredLangs: string[]) {} + getAvailableSpellCheckLanguages(): Promise | null { + return null; + } + protected getSSOCallbackUrl(fragmentAfterLogin: string): URL { const url = new URL(window.location.href); url.hash = fragmentAfterLogin || ""; diff --git a/src/languageHandler.tsx b/src/languageHandler.tsx index 9b9e304294..b827e83ded 100644 --- a/src/languageHandler.tsx +++ b/src/languageHandler.tsx @@ -353,6 +353,11 @@ export function setSpellCheckLanguages(preferredLangs: string[]) { } } +export async function getAvailableSpellCheckLanguages(): Promise { + const plaf = PlatformPeg.get(); + return plaf.getAvailableSpellCheckLanguages(); +} + export function getAllLanguagesFromJson() { return getLangsJson().then((langsObject) => { const langs = []; From 5d9f5ba979d3fc89bf6eec5ad2f319bea7168aec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 30 Nov 2020 08:35:51 +0100 Subject: [PATCH 11/52] Fix indentation --- src/components/views/elements/LanguageDropdown.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/views/elements/LanguageDropdown.js b/src/components/views/elements/LanguageDropdown.js index e37109caff..03ec456af5 100644 --- a/src/components/views/elements/LanguageDropdown.js +++ b/src/components/views/elements/LanguageDropdown.js @@ -100,10 +100,10 @@ export default class LanguageDropdown extends React.Component { let language = SettingsStore.getValue("language", null, /*excludeDefault:*/true); let value = null; if (language) { - value = this.props.value || language; + value = this.props.value || language; } else { - language = navigator.language || navigator.userLanguage; - value = this.props.value || language; + language = navigator.language || navigator.userLanguage; + value = this.props.value || language; } return Date: Tue, 1 Dec 2020 16:59:02 +0100 Subject: [PATCH 12/52] Change label --- .../views/settings/tabs/user/GeneralUserSettingsTab.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js index ad7e04d677..8d06ea3b36 100644 --- a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js @@ -318,7 +318,7 @@ export default class GeneralUserSettingsTab extends React.Component { _renderSpellCheckSection() { return (
- {_t("Spell checking")} + {_t("Spell check dictionaries")}
From cf61d50df40614a45c36c0d9886c3583ca69513e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 1 Dec 2020 16:59:21 +0100 Subject: [PATCH 13/52] Added SpellCheckLanguagesDropdown --- .../elements/SpellCheckLanguagesDropdown.tsx | 125 ++++++++++++++++++ .../views/settings/SpellCheckSettings.tsx | 4 +- 2 files changed, 127 insertions(+), 2 deletions(-) create mode 100644 src/components/views/elements/SpellCheckLanguagesDropdown.tsx diff --git a/src/components/views/elements/SpellCheckLanguagesDropdown.tsx b/src/components/views/elements/SpellCheckLanguagesDropdown.tsx new file mode 100644 index 0000000000..db158fa3dd --- /dev/null +++ b/src/components/views/elements/SpellCheckLanguagesDropdown.tsx @@ -0,0 +1,125 @@ +/* +Copyright 2017 Marcel Radzio (MTRNord) +Copyright 2017 Vector Creations Ltd. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from 'react'; +import PropTypes from 'prop-types'; + +import Dropdown from "../../views/elements/Dropdown" +import PlatformPeg from "../../../PlatformPeg"; +import * as sdk from '../../../index'; +import * as languageHandler from '../../../languageHandler'; +import SettingsStore from "../../../settings/SettingsStore"; +import { _t } from "../../../languageHandler"; + +function languageMatchesSearchQuery(query, language) { + if (language.label.toUpperCase().includes(query.toUpperCase())) return true; + if (language.value.toUpperCase() === query.toUpperCase()) return true; + return false; +} + +interface SpellCheckLanguagesDropdownIProps { + className: string, + value: string, + onOptionChange(language: string), +}; + +interface SpellCheckLanguagesDropdownIState { + searchQuery: string, + languages: any, +} + +export default class SpellCheckLanguagesDropdown extends React.Component { + constructor(props) { + super(props); + this._onSearchChange = this._onSearchChange.bind(this); + + this.state = { + searchQuery: '', + languages: null, + }; + } + + componentDidMount() { + languageHandler.getAvailableSpellCheckLanguages().then((languages) => { + languages.sort(function(a, b) { + if (a < b) return -1; + if (a > b) return 1; + return 0; + }); + var langs = []; + languages.forEach((language) => { + langs.push({ + label: language, + value: language, + }) + }) + this.setState({languages: langs}); + }).catch((e) => { + this.setState({languages: ['en']}); + }); + } + + _onSearchChange(search) { + this.setState({ + searchQuery: search, + }); + } + + render() { + if (this.state.languages === null) { + const Spinner = sdk.getComponent('elements.Spinner'); + return ; + } + + let displayedLanguages; + if (this.state.searchQuery) { + displayedLanguages = this.state.languages.filter((lang) => { + return languageMatchesSearchQuery(this.state.searchQuery, lang); + }); + } else { + displayedLanguages = this.state.languages; + } + + const options = displayedLanguages.map((language) => { + return
+ { language.label } +
; + }); + + // default value here too, otherwise we need to handle null / undefined; + // values between mounting and the initial value propgating + let language = SettingsStore.getValue("language", null, /*excludeDefault:*/true); + let value = null; + if (language) { + value = this.props.value || language; + } else { + language = navigator.language || navigator.userLanguage; + value = this.props.value || language; + } + + return + { options } + ; + } +} diff --git a/src/components/views/settings/SpellCheckSettings.tsx b/src/components/views/settings/SpellCheckSettings.tsx index befd98112e..37476d5f34 100644 --- a/src/components/views/settings/SpellCheckSettings.tsx +++ b/src/components/views/settings/SpellCheckSettings.tsx @@ -15,7 +15,7 @@ limitations under the License. */ import React from 'react'; -import LanguageDropdown from "../../../components/views/elements/LanguageDropdown"; +import SpellCheckLanguagesDropdown from "../../../components/views/elements/SpellCheckLanguagesDropdown"; import AccessibleButton from "../../../components/views/elements/AccessibleButton"; import {_t} from "../../../languageHandler"; @@ -99,7 +99,7 @@ export default class SpellCheckLanguages extends React.Component {existingSpellCheckLanguages} - {addButton} From a6d6af1a937fb6bc6cec2e320fbff453bef3c680 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 1 Dec 2020 17:19:45 +0100 Subject: [PATCH 14/52] Added defaults --- src/settings/Settings.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/settings/Settings.ts b/src/settings/Settings.ts index 409cd293d2..c83dbab897 100644 --- a/src/settings/Settings.ts +++ b/src/settings/Settings.ts @@ -404,7 +404,7 @@ export const SETTINGS: {[setting: string]: ISetting} = { }, "spell-check-languages": { supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG, - default: [], + default: ["en"], }, "breadcrumb_rooms": { // not really a setting From e9203d75715dbd6a677849dbb83a3d4706b2e6e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 1 Dec 2020 17:21:23 +0100 Subject: [PATCH 15/52] Removed unnecessary imports --- src/components/views/elements/SpellCheckLanguagesDropdown.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/components/views/elements/SpellCheckLanguagesDropdown.tsx b/src/components/views/elements/SpellCheckLanguagesDropdown.tsx index db158fa3dd..5e0fe3132c 100644 --- a/src/components/views/elements/SpellCheckLanguagesDropdown.tsx +++ b/src/components/views/elements/SpellCheckLanguagesDropdown.tsx @@ -16,10 +16,8 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; import Dropdown from "../../views/elements/Dropdown" -import PlatformPeg from "../../../PlatformPeg"; import * as sdk from '../../../index'; import * as languageHandler from '../../../languageHandler'; import SettingsStore from "../../../settings/SettingsStore"; From 8287f197f40869941d402e45da87c88d19514545 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 1 Dec 2020 19:49:31 +0100 Subject: [PATCH 16/52] Fix i18n --- src/i18n/strings/en_EN.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 282c1ce686..9ccd0e1e75 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1149,7 +1149,7 @@ "Set a new account password...": "Set a new account password...", "Account": "Account", "Language and region": "Language and region", - "Spell checking": "Spell checking", + "Spell check dictionaries": "Spell check dictionaries", "Agree to the identity server (%(serverName)s) Terms of Service to allow yourself to be discoverable by email address or phone number.": "Agree to the identity server (%(serverName)s) Terms of Service to allow yourself to be discoverable by email address or phone number.", "Account management": "Account management", "Deactivating your account is a permanent action - be careful!": "Deactivating your account is a permanent action - be careful!", From 44a363f188fb95927fff942b4c6b5a3914dbe31d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 1 Dec 2020 20:16:48 +0100 Subject: [PATCH 17/52] Fix default value --- src/settings/Settings.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/settings/Settings.ts b/src/settings/Settings.ts index c83dbab897..3540767a99 100644 --- a/src/settings/Settings.ts +++ b/src/settings/Settings.ts @@ -404,7 +404,7 @@ export const SETTINGS: {[setting: string]: ISetting} = { }, "spell-check-languages": { supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG, - default: ["en"], + default: ["en-US"], }, "breadcrumb_rooms": { // not really a setting From 3c2bb6e4f6d19e337d902613adbddf42fcba2f3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 1 Dec 2020 20:17:24 +0100 Subject: [PATCH 18/52] Cleanup --- src/BasePlatform.ts | 2 ++ .../views/settings/tabs/user/GeneralUserSettingsTab.js | 5 +---- src/languageHandler.tsx | 2 +- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/BasePlatform.ts b/src/BasePlatform.ts index 9ac35092a7..2af2ea51c5 100644 --- a/src/BasePlatform.ts +++ b/src/BasePlatform.ts @@ -237,6 +237,8 @@ export default abstract class BasePlatform { setLanguage(preferredLangs: string[]) {} + setSpellCheckLanguages(preferredLangs: string[]) {} + getAvailableSpellCheckLanguages(): Promise | null { return null; } diff --git a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js index 8d06ea3b36..6ed887d749 100644 --- a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js @@ -188,10 +188,7 @@ export default class GeneralUserSettingsTab extends React.Component { SettingsStore.setValue("spell-check-languages", null, SettingLevel.DEVICE, languages); this.setState({spellCheckLanguages: languages}) - const plaf = PlatformPeg.get(); - if (plaf) { - plaf.setLanguage(languages); - } + languageHandler.setSpellCheckLanguages(languages); }; _onPasswordChangeError = (err) => { diff --git a/src/languageHandler.tsx b/src/languageHandler.tsx index b827e83ded..38d3c8347a 100644 --- a/src/languageHandler.tsx +++ b/src/languageHandler.tsx @@ -349,7 +349,7 @@ export function setLanguage(preferredLangs: string | string[]) { export function setSpellCheckLanguages(preferredLangs: string[]) { const plaf = PlatformPeg.get(); if (plaf) { - plaf.setLanguage(preferredLangs); + plaf.setSpellCheckLanguages(preferredLangs); } } From db5bc0cb7ade92fc056283af639e3c782d384e4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 1 Dec 2020 20:36:25 +0100 Subject: [PATCH 19/52] Fix formatting --- .../elements/SpellCheckLanguagesDropdown.tsx | 7 ++++--- .../views/settings/SpellCheckSettings.tsx | 17 +++++++++-------- .../tabs/user/GeneralUserSettingsTab.js | 6 +++--- 3 files changed, 16 insertions(+), 14 deletions(-) diff --git a/src/components/views/elements/SpellCheckLanguagesDropdown.tsx b/src/components/views/elements/SpellCheckLanguagesDropdown.tsx index 5e0fe3132c..53c3f310b7 100644 --- a/src/components/views/elements/SpellCheckLanguagesDropdown.tsx +++ b/src/components/views/elements/SpellCheckLanguagesDropdown.tsx @@ -33,14 +33,15 @@ interface SpellCheckLanguagesDropdownIProps { className: string, value: string, onOptionChange(language: string), -}; +} interface SpellCheckLanguagesDropdownIState { searchQuery: string, languages: any, } -export default class SpellCheckLanguagesDropdown extends React.Component { +export default class SpellCheckLanguagesDropdown extends React.Component { constructor(props) { super(props); this._onSearchChange = this._onSearchChange.bind(this); @@ -58,7 +59,7 @@ export default class SpellCheckLanguagesDropdown extends React.Component b) return 1; return 0; }); - var langs = []; + const langs = []; languages.forEach((language) => { langs.push({ label: language, diff --git a/src/components/views/settings/SpellCheckSettings.tsx b/src/components/views/settings/SpellCheckSettings.tsx index 37476d5f34..bfe0774570 100644 --- a/src/components/views/settings/SpellCheckSettings.tsx +++ b/src/components/views/settings/SpellCheckSettings.tsx @@ -22,12 +22,12 @@ import {_t} from "../../../languageHandler"; interface ExistingSpellCheckLanguageIProps { language: string, onRemoved(language: string), -}; +} interface SpellCheckLanguagesIProps { languages: Array, onLanguagesChange(languages: Array), -}; +} interface SpellCheckLanguagesIState { newLanguage: string, @@ -71,7 +71,7 @@ export default class SpellCheckLanguages extends React.Component; }); - let addButton = ( + const addButton = ( {_t("Add")} @@ -99,12 +99,13 @@ export default class SpellCheckLanguages extends React.Component {existingSpellCheckLanguages} - + {addButton}
); - }; + } } diff --git a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js index 6ed887d749..95a8abbb24 100644 --- a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js @@ -22,7 +22,7 @@ import ProfileSettings from "../../ProfileSettings"; import * as languageHandler from "../../../../../languageHandler"; import SettingsStore from "../../../../../settings/SettingsStore"; import LanguageDropdown from "../../../elements/LanguageDropdown"; -import SpellCheckSettings from "../../SpellCheckSettings" +import SpellCheckSettings from "../../SpellCheckSettings"; import AccessibleButton from "../../../elements/AccessibleButton"; import DeactivateAccountDialog from "../../../dialogs/DeactivateAccountDialog"; import PropTypes from "prop-types"; @@ -186,8 +186,8 @@ export default class GeneralUserSettingsTab extends React.Component { _onSpellCheckLanguagesChange = (languages) => { SettingsStore.setValue("spell-check-languages", null, SettingLevel.DEVICE, languages); - this.setState({spellCheckLanguages: languages}) - + this.setState({spellCheckLanguages: languages}); + languageHandler.setSpellCheckLanguages(languages); }; From bab541a652e402c1aede8caa00b22b13a2adb0c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Wed, 2 Dec 2020 20:14:58 +0100 Subject: [PATCH 20/52] Hide spell-check settings if not using Electron --- src/BasePlatform.ts | 8 ++++++++ .../views/settings/tabs/user/GeneralUserSettingsTab.js | 5 ++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/BasePlatform.ts b/src/BasePlatform.ts index 2af2ea51c5..54d15675cb 100644 --- a/src/BasePlatform.ts +++ b/src/BasePlatform.ts @@ -128,6 +128,14 @@ export default abstract class BasePlatform { hideUpdateToast(); } + /** + * Return true if platform supports multi-language + * spell-checking, otherwise false. + */ + supportsMultiLanguageSpellCheck(): boolean { + return false; + } + /** * Returns true if the platform supports displaying * notifications, otherwise false. diff --git a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js index 95a8abbb24..4d1210dc40 100644 --- a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js @@ -400,6 +400,9 @@ export default class GeneralUserSettingsTab extends React.Component { } render() { + const plaf = PlatformPeg.get(); + const supportsMultiLanguageSpellCheck = plaf.supportsMultiLanguageSpellCheck() ? true : false; + const discoWarning = this.state.requiredPolicyInfo.hasTerms ? Date: Thu, 3 Dec 2020 11:50:08 +0100 Subject: [PATCH 21/52] Simplifie --- .../views/settings/tabs/user/GeneralUserSettingsTab.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js index 4d1210dc40..febbcc8e36 100644 --- a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js @@ -401,7 +401,7 @@ export default class GeneralUserSettingsTab extends React.Component { render() { const plaf = PlatformPeg.get(); - const supportsMultiLanguageSpellCheck = plaf.supportsMultiLanguageSpellCheck() ? true : false; + const supportsMultiLanguageSpellCheck = plaf.supportsMultiLanguageSpellCheck(); const discoWarning = this.state.requiredPolicyInfo.hasTerms ? Date: Thu, 3 Dec 2020 11:50:20 +0100 Subject: [PATCH 22/52] Added in if statement --- src/languageHandler.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/languageHandler.tsx b/src/languageHandler.tsx index 38d3c8347a..985719fce7 100644 --- a/src/languageHandler.tsx +++ b/src/languageHandler.tsx @@ -355,7 +355,9 @@ export function setSpellCheckLanguages(preferredLangs: string[]) { export async function getAvailableSpellCheckLanguages(): Promise { const plaf = PlatformPeg.get(); - return plaf.getAvailableSpellCheckLanguages(); + if (plaf) { + return plaf.getAvailableSpellCheckLanguages(); + } } export function getAllLanguagesFromJson() { From 5de99c7708c58b2808d288160ce8fb69f4f5027c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Thu, 18 Feb 2021 19:40:24 +0100 Subject: [PATCH 23/52] Fix licenses MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/css/views/settings/_SpellCheckLanguages.scss | 3 +-- src/components/views/elements/SpellCheckLanguagesDropdown.tsx | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/res/css/views/settings/_SpellCheckLanguages.scss b/res/css/views/settings/_SpellCheckLanguages.scss index ddfa0bf9e0..bb322c983f 100644 --- a/res/css/views/settings/_SpellCheckLanguages.scss +++ b/res/css/views/settings/_SpellCheckLanguages.scss @@ -1,6 +1,5 @@ /* -Copyright 2019 New Vector Ltd -Copyright 2019 The Matrix.org Foundation C.I.C. +Copyright 2021 Šimon Brandner Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/components/views/elements/SpellCheckLanguagesDropdown.tsx b/src/components/views/elements/SpellCheckLanguagesDropdown.tsx index 53c3f310b7..029d162573 100644 --- a/src/components/views/elements/SpellCheckLanguagesDropdown.tsx +++ b/src/components/views/elements/SpellCheckLanguagesDropdown.tsx @@ -1,6 +1,5 @@ /* -Copyright 2017 Marcel Radzio (MTRNord) -Copyright 2017 Vector Creations Ltd. +Copyright 2021 Šimon Brandner Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. From ed02503462d93e43659bddd3280a19a0b31e26f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Thu, 18 Feb 2021 19:41:19 +0100 Subject: [PATCH 24/52] Fix one more license MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/settings/SpellCheckSettings.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/settings/SpellCheckSettings.tsx b/src/components/views/settings/SpellCheckSettings.tsx index bfe0774570..d08f263b5f 100644 --- a/src/components/views/settings/SpellCheckSettings.tsx +++ b/src/components/views/settings/SpellCheckSettings.tsx @@ -1,5 +1,5 @@ /* -Copyright 2019 New Vector Ltd +Copyright 2021 Šimon Brandner Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. From 2ebc1252cbbfc9731dc412947287ef5e4c9ce460 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Thu, 18 Feb 2021 19:54:54 +0100 Subject: [PATCH 25/52] Removed unnecessary functions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- .../elements/SpellCheckLanguagesDropdown.tsx | 37 ++++++++++--------- .../tabs/user/GeneralUserSettingsTab.js | 6 ++- src/languageHandler.tsx | 14 ------- 3 files changed, 25 insertions(+), 32 deletions(-) diff --git a/src/components/views/elements/SpellCheckLanguagesDropdown.tsx b/src/components/views/elements/SpellCheckLanguagesDropdown.tsx index 029d162573..c647f6e410 100644 --- a/src/components/views/elements/SpellCheckLanguagesDropdown.tsx +++ b/src/components/views/elements/SpellCheckLanguagesDropdown.tsx @@ -18,7 +18,7 @@ import React from 'react'; import Dropdown from "../../views/elements/Dropdown" import * as sdk from '../../../index'; -import * as languageHandler from '../../../languageHandler'; +import PlatformPeg from "../../../PlatformPeg"; import SettingsStore from "../../../settings/SettingsStore"; import { _t } from "../../../languageHandler"; @@ -52,23 +52,26 @@ export default class SpellCheckLanguagesDropdown extends React.Component { - languages.sort(function(a, b) { - if (a < b) return -1; - if (a > b) return 1; - return 0; - }); - const langs = []; - languages.forEach((language) => { - langs.push({ - label: language, - value: language, + const plaf = PlatformPeg.get(); + if (plaf) { + plaf.getAvailableSpellCheckLanguages().then((languages) => { + languages.sort(function(a, b) { + if (a < b) return -1; + if (a > b) return 1; + return 0; + }); + const langs = []; + languages.forEach((language) => { + langs.push({ + label: language, + value: language, + }) }) - }) - this.setState({languages: langs}); - }).catch((e) => { - this.setState({languages: ['en']}); - }); + this.setState({languages: langs}); + }).catch((e) => { + this.setState({languages: ['en']}); + }); + } } _onSearchChange(search) { diff --git a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js index febbcc8e36..e87dca88c8 100644 --- a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js @@ -188,7 +188,10 @@ export default class GeneralUserSettingsTab extends React.Component { SettingsStore.setValue("spell-check-languages", null, SettingLevel.DEVICE, languages); this.setState({spellCheckLanguages: languages}); - languageHandler.setSpellCheckLanguages(languages); + const plaf = PlatformPeg.get(); + if (plaf) { + plaf.setSpellCheckLanguages(languages); + } }; _onPasswordChangeError = (err) => { @@ -402,6 +405,7 @@ export default class GeneralUserSettingsTab extends React.Component { render() { const plaf = PlatformPeg.get(); const supportsMultiLanguageSpellCheck = plaf.supportsMultiLanguageSpellCheck(); + console.log("LOG", supportsMultiLanguageSpellCheck); const discoWarning = this.state.requiredPolicyInfo.hasTerms ? { - const plaf = PlatformPeg.get(); - if (plaf) { - return plaf.getAvailableSpellCheckLanguages(); - } -} - export function getAllLanguagesFromJson() { return getLangsJson().then((langsObject) => { const langs = []; From 305d64cda88aebef8e2a0e799606224baeac4dc1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Thu, 18 Feb 2021 20:09:39 +0100 Subject: [PATCH 26/52] Removed log MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- .../views/settings/tabs/user/GeneralUserSettingsTab.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js index e87dca88c8..41597604e9 100644 --- a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js @@ -405,7 +405,6 @@ export default class GeneralUserSettingsTab extends React.Component { render() { const plaf = PlatformPeg.get(); const supportsMultiLanguageSpellCheck = plaf.supportsMultiLanguageSpellCheck(); - console.log("LOG", supportsMultiLanguageSpellCheck); const discoWarning = this.state.requiredPolicyInfo.hasTerms ? Date: Thu, 18 Feb 2021 20:12:48 +0100 Subject: [PATCH 27/52] Use getSpellCheckLanguages() instead of a setting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/BasePlatform.ts | 4 ++++ .../settings/tabs/user/GeneralUserSettingsTab.js | 11 ++++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/BasePlatform.ts b/src/BasePlatform.ts index fe655371a5..9d7077097b 100644 --- a/src/BasePlatform.ts +++ b/src/BasePlatform.ts @@ -250,6 +250,10 @@ export default abstract class BasePlatform { setSpellCheckLanguages(preferredLangs: string[]) {} + getSpellCheckLanguages(): Promise | null { + return null; + } + getAvailableSpellCheckLanguages(): Promise | null { return null; } diff --git a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js index 41597604e9..3936864215 100644 --- a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js @@ -50,7 +50,7 @@ export default class GeneralUserSettingsTab extends React.Component { this.state = { language: languageHandler.getCurrentLanguage(), - spellCheckLanguages: SettingsStore.getValue("spell-check-languages", null, false), + spellCheckLanguages: [], haveIdServer: Boolean(MatrixClientPeg.get().getIdentityServerUrl()), serverSupportsSeparateAddAndBind: null, idServerHasUnsignedTerms: false, @@ -87,6 +87,15 @@ export default class GeneralUserSettingsTab extends React.Component { this._getThreepidState(); } + async componentDidMount() { + const plaf = PlatformPeg.get(); + if (plaf) { + this.setState({ + spellCheckLanguages: await plaf.getSpellCheckLanguages(), + }); + } + } + componentWillUnmount() { dis.unregister(this.dispatcherRef); } From 5a6e393fa2c6344a00efc14ab4eb17fc6a258a64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Thu, 18 Feb 2021 20:13:55 +0100 Subject: [PATCH 28/52] Removed spell-check-languages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- .../views/settings/tabs/user/GeneralUserSettingsTab.js | 1 - src/settings/Settings.ts | 4 ---- 2 files changed, 5 deletions(-) diff --git a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js index 3936864215..b17ab18c39 100644 --- a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js @@ -194,7 +194,6 @@ export default class GeneralUserSettingsTab extends React.Component { }; _onSpellCheckLanguagesChange = (languages) => { - SettingsStore.setValue("spell-check-languages", null, SettingLevel.DEVICE, languages); this.setState({spellCheckLanguages: languages}); const plaf = PlatformPeg.get(); diff --git a/src/settings/Settings.ts b/src/settings/Settings.ts index b486f0fbf8..ca5e2f1d04 100644 --- a/src/settings/Settings.ts +++ b/src/settings/Settings.ts @@ -424,10 +424,6 @@ export const SETTINGS: {[setting: string]: ISetting} = { supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG, default: "en", }, - "spell-check-languages": { - supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG, - default: ["en-US"], - }, "breadcrumb_rooms": { // not really a setting supportedLevels: [SettingLevel.ACCOUNT], From e3f08adacb082ef0f6e72e7b20adc494a86aae5f Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Wed, 24 Feb 2021 17:27:56 +0000 Subject: [PATCH 29/52] Upgrade matrix-js-sdk to 9.8.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 d4f931d811..7b4e577406 100644 --- a/package.json +++ b/package.json @@ -80,7 +80,7 @@ "katex": "^0.12.0", "linkifyjs": "^2.1.9", "lodash": "^4.17.20", - "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop", + "matrix-js-sdk": "9.8.0-rc.1", "matrix-widget-api": "^0.1.0-beta.13", "minimist": "^1.2.5", "pako": "^2.0.3", diff --git a/yarn.lock b/yarn.lock index 01450908cc..bef26e27b4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5572,9 +5572,10 @@ mathml-tag-names@^2.1.3: 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 "9.7.0" - resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/c82bc35202f93efa2cb9b27b140f83df37c64ab2" +matrix-js-sdk@9.8.0-rc.1: + version "9.8.0-rc.1" + resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-9.8.0-rc.1.tgz#229122583bec5971f22a423a4a40d749e07602d9" + integrity sha512-Tmo5cdyBBgYcMZMaAavEvtdCsEwr5sYE0RLd6etLOSTxmGRSYpqKvvKQqGsYrogmZYNbx9nNZYYYV2aJkCKcQg== dependencies: "@babel/runtime" "^7.12.5" another-json "^0.2.0" From 364f24851355fe0b1a716cd5973bbc19cf575d7a Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Wed, 24 Feb 2021 17:32:53 +0000 Subject: [PATCH 30/52] Prepare changelog for v3.15.0-rc.1 --- CHANGELOG.md | 62 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c87f1c62e6..e727adabfa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,65 @@ +Changes in [3.15.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.15.0-rc.1) (2021-02-24) +=============================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.14.0...v3.15.0-rc.1) + + * Upgrade to JS SDK 9.8.0-rc.1 + * Translations update from Weblate + [\#5683](https://github.com/matrix-org/matrix-react-sdk/pull/5683) + * Fix object diffing when objects have different keys + [\#5681](https://github.com/matrix-org/matrix-react-sdk/pull/5681) + * Add if it's missing + [\#5673](https://github.com/matrix-org/matrix-react-sdk/pull/5673) + * Add email only if the verification is complete + [\#5629](https://github.com/matrix-org/matrix-react-sdk/pull/5629) + * Fix portrait videocalls + [\#5676](https://github.com/matrix-org/matrix-react-sdk/pull/5676) + * Tweak code block icon positions + [\#5643](https://github.com/matrix-org/matrix-react-sdk/pull/5643) + * Revert "Improve URL preview formatting and image upload thumbnail size" + [\#5677](https://github.com/matrix-org/matrix-react-sdk/pull/5677) + * Fix context menu leaving visible area + [\#5644](https://github.com/matrix-org/matrix-react-sdk/pull/5644) + * Jitsi conferences names, take 3 + [\#5675](https://github.com/matrix-org/matrix-react-sdk/pull/5675) + * Update isUserOnDarkTheme to take use_system_theme in account + [\#5670](https://github.com/matrix-org/matrix-react-sdk/pull/5670) + * Discard some dead code + [\#5665](https://github.com/matrix-org/matrix-react-sdk/pull/5665) + * Add developer tool to explore and edit settings + [\#5664](https://github.com/matrix-org/matrix-react-sdk/pull/5664) + * Use and create new room helpers + [\#5663](https://github.com/matrix-org/matrix-react-sdk/pull/5663) + * Clear message previews when the maximum limit is reached for history + [\#5661](https://github.com/matrix-org/matrix-react-sdk/pull/5661) + * VoIP virtual rooms, mk II + [\#5639](https://github.com/matrix-org/matrix-react-sdk/pull/5639) + * Disable chat effects when reduced motion preferred + [\#5660](https://github.com/matrix-org/matrix-react-sdk/pull/5660) + * Improve URL preview formatting and image upload thumbnail size + [\#5637](https://github.com/matrix-org/matrix-react-sdk/pull/5637) + * Fix border radius when the panel is collapsed + [\#5641](https://github.com/matrix-org/matrix-react-sdk/pull/5641) + * Use a more generic layout setting - useIRCLayout → layout + [\#5571](https://github.com/matrix-org/matrix-react-sdk/pull/5571) + * Remove redundant lockOrigin parameter from usercontent + [\#5657](https://github.com/matrix-org/matrix-react-sdk/pull/5657) + * Set ICE candidate pool size option + [\#5655](https://github.com/matrix-org/matrix-react-sdk/pull/5655) + * Prepare to encrypt when a call arrives + [\#5654](https://github.com/matrix-org/matrix-react-sdk/pull/5654) + * Use config for host signup branding + [\#5650](https://github.com/matrix-org/matrix-react-sdk/pull/5650) + * Use randomly generated conference names for Jitsi + [\#5649](https://github.com/matrix-org/matrix-react-sdk/pull/5649) + * Modified regex to account for an immediate new line after slash commands + [\#5647](https://github.com/matrix-org/matrix-react-sdk/pull/5647) + * Fix codeblock scrollbar color for non-Firefox + [\#5642](https://github.com/matrix-org/matrix-react-sdk/pull/5642) + * Fix codeblock scrollbar colors + [\#5630](https://github.com/matrix-org/matrix-react-sdk/pull/5630) + * Added loading and disabled the button while searching for server + [\#5634](https://github.com/matrix-org/matrix-react-sdk/pull/5634) + Changes in [3.14.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.14.0) (2021-02-16) ===================================================================================================== [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.14.0-rc.1...v3.14.0) From 8860c8bfe7b48cbfb230f28e81d48ea7f474e3f4 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Wed, 24 Feb 2021 17:32:54 +0000 Subject: [PATCH 31/52] v3.15.0-rc.1 --- package.json | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 7b4e577406..f49b99831f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-react-sdk", - "version": "3.14.0", + "version": "3.15.0-rc.1", "description": "SDK for matrix.org using React", "author": "matrix.org", "repository": { @@ -27,7 +27,7 @@ "matrix-gen-i18n": "scripts/gen-i18n.js", "matrix-prune-i18n": "scripts/prune-i18n.js" }, - "main": "./src/index.js", + "main": "./lib/index.js", "matrix_src_main": "./src/index.js", "matrix_lib_main": "./lib/index.js", "matrix_lib_typings": "./lib/index.d.ts", @@ -189,5 +189,6 @@ "transformIgnorePatterns": [ "/node_modules/(?!matrix-js-sdk).+$" ] - } + }, + "typings": "./lib/index.d.ts" } From 7030c636f07c32c3616eeddba64e0a0c8dfc6587 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 25 Feb 2021 15:18:54 +0000 Subject: [PATCH 32/52] Initial Space Store for keeping track of space hierarchies from sync --- src/@types/global.d.ts | 2 + src/stores/SpaceStore.tsx | 462 ++++++++++++++++++ .../notifications/SpaceNotificationState.ts | 82 ++++ 3 files changed, 546 insertions(+) create mode 100644 src/stores/SpaceStore.tsx create mode 100644 src/stores/notifications/SpaceNotificationState.ts diff --git a/src/@types/global.d.ts b/src/@types/global.d.ts index 28f22780a2..4aa6df5488 100644 --- a/src/@types/global.d.ts +++ b/src/@types/global.d.ts @@ -38,6 +38,7 @@ import UserActivity from "../UserActivity"; import {ModalWidgetStore} from "../stores/ModalWidgetStore"; import { WidgetLayoutStore } from "../stores/widgets/WidgetLayoutStore"; import VoipUserMapper from "../VoipUserMapper"; +import {SpaceStoreClass} from "../stores/SpaceStore"; declare global { interface Window { @@ -68,6 +69,7 @@ declare global { mxUserActivity: UserActivity; mxModalWidgetStore: ModalWidgetStore; mxVoipUserMapper: VoipUserMapper; + mxSpaceStore: SpaceStoreClass; } interface Document { diff --git a/src/stores/SpaceStore.tsx b/src/stores/SpaceStore.tsx new file mode 100644 index 0000000000..d675879138 --- /dev/null +++ b/src/stores/SpaceStore.tsx @@ -0,0 +1,462 @@ +/* +Copyright 2021 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import {throttle, sortBy} from "lodash"; +import {EventType} from "matrix-js-sdk/src/@types/event"; +import {Room} from "matrix-js-sdk/src/models/room"; +import {MatrixEvent} from "matrix-js-sdk/src/models/event"; + +import {AsyncStoreWithClient} from "./AsyncStoreWithClient"; +import defaultDispatcher from "../dispatcher/dispatcher"; +import {ActionPayload} from "../dispatcher/payloads"; +import RoomListStore from "./room-list/RoomListStore"; +import SettingsStore from "../settings/SettingsStore"; +import DMRoomMap from "../utils/DMRoomMap"; +import {FetchRoomFn} from "./notifications/ListNotificationState"; +import {SpaceNotificationState} from "./notifications/SpaceNotificationState"; +import {RoomNotificationStateStore} from "./notifications/RoomNotificationStateStore"; +import {DefaultTagID} from "./room-list/models"; +import {EnhancedMap, mapDiff} from "../utils/maps"; +import {setHasDiff} from "../utils/sets"; +import {objectDiff} from "../utils/objects"; +import {arrayHasDiff} from "../utils/arrays"; + +type SpaceKey = string | symbol; + +interface IState {} + +const ACTIVE_SPACE_LS_KEY = "mx_active_space"; + +export const HOME_SPACE = Symbol("home-space"); + +export const UPDATE_TOP_LEVEL_SPACES = Symbol("top-level-spaces"); +export const UPDATE_SELECTED_SPACE = Symbol("selected-space"); +// Space Room ID/HOME_SPACE will be emitted when a Space's children change + +const partitionSpacesAndRooms = (arr: Room[]): [Room[], Room[]] => { // [spaces, rooms] + return arr.reduce((result, room: Room) => { + result[room.isSpaceRoom() ? 0 : 1].push(room); + return result; + }, [[], []]); +}; + +const getOrder = (ev: MatrixEvent): string | null => { + const content = ev.getContent(); + if (typeof content.order === "string" && Array.from(content.order).every((c: string) => { + const charCode = c.charCodeAt(0); + return charCode >= 0x20 && charCode <= 0x7F; + })) { + return content.order; + } + return null; +} + +const getRoomFn: FetchRoomFn = (room: Room) => { + return RoomNotificationStateStore.instance.getRoomState(room); +}; + +export class SpaceStoreClass extends AsyncStoreWithClient { + constructor() { + super(defaultDispatcher, {}); + } + + // The spaces representing the roots of the various tree-like hierarchies + private rootSpaces: Room[] = []; + // The list of rooms not present in any currently joined spaces + private orphanedRooms = new Set(); + // Map from room ID to set of spaces which list it as a child + private parentMap = new EnhancedMap>(); + // Map from space key to SpaceNotificationState instance representing that space + private notificationStateMap = new Map(); + // Map from space key to Set of room IDs that should be shown as part of that space's filter + private spaceFilteredRooms = new Map>(); + // The space currently selected in the Space Panel - if null then `Home` is selected + private _activeSpace?: Room = null; + + public get spacePanelSpaces(): Room[] { + return this.rootSpaces; + } + + public get activeSpace(): Room | null { + return this._activeSpace || null; + } + + public setActiveSpace(space: Room | null) { + if (space === this.activeSpace) return; + + this._activeSpace = space; + this.emit(UPDATE_SELECTED_SPACE, this.activeSpace); + + // persist space selected + if (space) { + window.localStorage.setItem(ACTIVE_SPACE_LS_KEY, space.roomId); + } else { + window.localStorage.removeItem(ACTIVE_SPACE_LS_KEY); + } + } + + public addRoomToSpace(space: Room, roomId: string, via: string[], autoJoin = false) { + return this.matrixClient.sendStateEvent(space.roomId, EventType.SpaceChild, { + via, + auto_join: autoJoin, + }, roomId); + } + + private getChildren(spaceId: string): Room[] { + const room = this.matrixClient?.getRoom(spaceId); + const childEvents = room?.currentState.getStateEvents(EventType.SpaceChild).filter(ev => ev.getContent()?.via); + return sortBy(childEvents, getOrder) + .map(ev => this.matrixClient.getRoom(ev.getStateKey())) + .filter(room => room?.getMyMembership() === "join") || []; + } + + public getChildRooms(spaceId: string): Room[] { + return this.getChildren(spaceId).filter(r => !r.isSpaceRoom()); + } + + public getChildSpaces(spaceId: string): Room[] { + return this.getChildren(spaceId).filter(r => r.isSpaceRoom()); + } + + public getParents(roomId: string, canonicalOnly = false): Room[] { + const room = this.matrixClient?.getRoom(roomId); + return room?.currentState.getStateEvents(EventType.SpaceParent) + .filter(ev => { + const content = ev.getContent(); + if (!content?.via) return false; + // TODO apply permissions check to verify that the parent mapping is valid + if (canonicalOnly && !content?.canonical) return false; + return true; + }) + .map(ev => this.matrixClient.getRoom(ev.getStateKey())) + .filter(Boolean) || []; + } + + public getCanonicalParent(roomId: string): Room | null { + const parents = this.getParents(roomId, true); + return sortBy(parents, r => r.roomId)?.[0] || null; + } + + public getSpaces = () => { + return this.matrixClient.getRooms().filter(r => r.isSpaceRoom() && r.getMyMembership() === "join"); + }; + + public getSpaceFilteredRoomIds = (space: Room | null): Set => { + return this.spaceFilteredRooms.get(space?.roomId || HOME_SPACE) || new Set(); + }; + + public rebuild = throttle(() => { // exported for tests + const visibleRooms = this.matrixClient.getVisibleRooms(); + + // Sort spaces by room ID to force the loop breaking to be deterministic + const spaces = sortBy(this.getSpaces(), space => space.roomId); + const unseenChildren = new Set([...visibleRooms, ...spaces]); + + const backrefs = new EnhancedMap>(); + + // TODO handle cleaning up links when a Space is removed + spaces.forEach(space => { + const children = this.getChildren(space.roomId); + children.forEach(child => { + unseenChildren.delete(child); + + backrefs.getOrCreate(child.roomId, new Set()).add(space.roomId); + }); + }); + + const [rootSpaces, orphanedRooms] = partitionSpacesAndRooms(Array.from(unseenChildren)); + + // untested algorithm to handle full-cycles + const detachedNodes = new Set(spaces); + + const markTreeChildren = (rootSpace: Room, unseen: Set) => { + const stack = [rootSpace]; + while (stack.length) { + const op = stack.pop(); + unseen.delete(op); + this.getChildSpaces(op.roomId).forEach(space => { + if (unseen.has(space)) { + stack.push(space); + } + }); + } + }; + + rootSpaces.forEach(rootSpace => { + markTreeChildren(rootSpace, detachedNodes); + }); + + // Handle spaces forming fully cyclical relationships. + // In order, assume each detachedNode is a root unless it has already + // been claimed as the child of prior detached node. + // Work from a copy of the detachedNodes set as it will be mutated as part of this operation. + Array.from(detachedNodes).forEach(detachedNode => { + if (!detachedNodes.has(detachedNode)) return; + // declare this detached node a new root, find its children, without ever looping back to it + detachedNodes.delete(detachedNode); + rootSpaces.push(detachedNode); + markTreeChildren(detachedNode, detachedNodes); + + // TODO only consider a detached node a root space if it has no *parents other than the ones forming cycles + }); + + // TODO neither of these handle an A->B->C->A with an additional C->D + // detachedNodes.forEach(space => { + // rootSpaces.push(space); + // }); + + this.orphanedRooms = new Set(orphanedRooms); + this.rootSpaces = rootSpaces; + this.parentMap = backrefs; + + // if the currently selected space no longer exists, remove its selection + if (this._activeSpace && detachedNodes.has(this._activeSpace)) { + this.setActiveSpace(null); + } + + this.onRoomsUpdate(); // TODO only do this if a change has happened + this.emit(UPDATE_TOP_LEVEL_SPACES, this.spacePanelSpaces); + }, 100, {trailing: true, leading: true}); + + onSpaceUpdate = () => { + this.rebuild(); + } + + private showInHomeSpace = (room: Room) => { + return !this.parentMap.get(room.roomId)?.size // put all orphaned rooms in the Home Space + || DMRoomMap.shared().getUserIdForRoomId(room.roomId) // put all DMs in the Home Space + || RoomListStore.instance.getTagsForRoom(room).includes(DefaultTagID.Favourite) // show all favourites + }; + + // Update a given room due to its tag changing (e.g DM-ness or Fav-ness) + // This can only change whether it shows up in the HOME_SPACE or not + private onRoomUpdate = (room: Room) => { + if (this.showInHomeSpace(room)) { + this.spaceFilteredRooms.get(HOME_SPACE)?.add(room.roomId); + this.emit(HOME_SPACE); + } else if (!this.orphanedRooms.has(room.roomId)) { + this.spaceFilteredRooms.get(HOME_SPACE)?.delete(room.roomId); + this.emit(HOME_SPACE); + } + }; + + private onRoomsUpdate = throttle(() => { + // TODO resolve some updates as deltas + const visibleRooms = this.matrixClient.getVisibleRooms(); + + const oldFilteredRooms = this.spaceFilteredRooms; + this.spaceFilteredRooms = new Map(); + + // put all invites (rooms & spaces) in the Home Space + const invites = this.matrixClient.getRooms().filter(r => r.getMyMembership() === "invite"); + this.spaceFilteredRooms.set(HOME_SPACE, new Set(invites.map(room => room.roomId))); + + visibleRooms.forEach(room => { + if (this.showInHomeSpace(room)) { + this.spaceFilteredRooms.get(HOME_SPACE).add(room.roomId); + } + }); + + this.rootSpaces.forEach(s => { + // traverse each space tree in DFS to build up the supersets as you go up, + // reusing results from like subtrees. + const fn = (spaceId: string, parentPath: Set): Set => { + if (parentPath.has(spaceId)) return; // prevent cycles + + // reuse existing results if multiple similar branches exist + if (this.spaceFilteredRooms.has(spaceId)) { + return this.spaceFilteredRooms.get(spaceId); + } + + const [childSpaces, childRooms] = partitionSpacesAndRooms(this.getChildren(spaceId)); + const roomIds = new Set(childRooms.map(r => r.roomId)); + const space = this.matrixClient?.getRoom(spaceId); + + // Add relevant DMs + space?.getJoinedMembers().forEach(member => { + DMRoomMap.shared().getDMRoomsForUserId(member.userId).forEach(roomId => { + roomIds.add(roomId); + }); + }); + + const newPath = new Set(parentPath).add(spaceId); + childSpaces.forEach(childSpace => { + fn(childSpace.roomId, newPath)?.forEach(roomId => { + roomIds.add(roomId); + }); + }); + this.spaceFilteredRooms.set(spaceId, roomIds); + return roomIds; + }; + + fn(s.roomId, new Set()); + }); + + const diff = mapDiff(oldFilteredRooms, this.spaceFilteredRooms); + // filter out keys which changed by reference only by checking whether the sets differ + const changed = diff.changed.filter(k => setHasDiff(oldFilteredRooms.get(k), this.spaceFilteredRooms.get(k))); + [...diff.added, ...diff.removed, ...changed].forEach(k => { + this.emit(k); + }); + + this.spaceFilteredRooms.forEach((roomIds, s) => { + // Update NotificationStates + const rooms = this.matrixClient.getRooms().filter(room => roomIds.has(room.roomId)); + this.getNotificationState(s)?.setRooms(rooms); + }); + }, 100, {trailing: true, leading: true}); + + private onRoom = (room: Room) => { + if (room?.isSpaceRoom()) { + this.onSpaceUpdate(); + this.emit(room.roomId); + } else { + // this.onRoomUpdate(room); + this.onRoomsUpdate(); + } + }; + + private onRoomState = (ev: MatrixEvent) => { + const room = this.matrixClient.getRoom(ev.getRoomId()); + if (!room) return; + + if (ev.getType() === EventType.SpaceChild && room.isSpaceRoom()) { + this.onSpaceUpdate(); + this.emit(room.roomId); + } else if (ev.getType() === EventType.SpaceParent) { + // TODO rebuild the space parent and not the room - check permissions? + // TODO confirm this after implementing parenting behaviour + if (room.isSpaceRoom()) { + this.onSpaceUpdate(); + } else { + this.onRoomUpdate(room); + } + this.emit(room.roomId); + } + }; + + private onRoomAccountData = (ev: MatrixEvent, room: Room, lastEvent: MatrixEvent) => { + if (ev.getType() === EventType.Tag && !room.isSpaceRoom()) { + // If the room was in favourites and now isn't or the opposite then update its position in the trees + if (!!ev.getContent()[DefaultTagID.Favourite] !== !!lastEvent.getContent()[DefaultTagID.Favourite]) { + this.onRoomUpdate(room); + } + } + } + + private onAccountData = (ev: MatrixEvent, lastEvent: MatrixEvent) => { + if (ev.getType() === EventType.Direct) { + const lastContent = lastEvent.getContent(); + const content = ev.getContent(); + + const diff = objectDiff>(lastContent, content); + // filter out keys which changed by reference only by checking whether the sets differ + const changed = diff.changed.filter(k => arrayHasDiff(lastContent[k], content[k])); + // DM tag changes, refresh relevant rooms + new Set([...diff.added, ...diff.removed, ...changed]).forEach(roomId => { + const room = this.matrixClient?.getRoom(roomId); + if (room) { + this.onRoomUpdate(room); + } + }); + } + }; + + protected async onNotReady() { + if (!SettingsStore.getValue("feature_spaces")) return; + if (this.matrixClient) { + this.matrixClient.removeListener("Room", this.onRoom); + this.matrixClient.removeListener("Room.myMembership", this.onRoom); + this.matrixClient.removeListener("RoomState.events", this.onRoomState); + this.matrixClient.removeListener("Room.accountData", this.onRoomAccountData); + this.matrixClient.removeListener("accountData", this.onAccountData); + } + await this.reset({}); + } + + protected async onReady() { + if (!SettingsStore.getValue("feature_spaces")) return; + this.matrixClient.on("Room", this.onRoom); + this.matrixClient.on("Room.myMembership", this.onRoom); + this.matrixClient.on("RoomState.events", this.onRoomState); + this.matrixClient.on("Room.accountData", this.onRoomAccountData); + this.matrixClient.on("accountData", this.onAccountData); + + await this.onSpaceUpdate(); // trigger an initial update + + // restore selected state from last session if any and still valid + const lastSpaceId = window.localStorage.getItem(ACTIVE_SPACE_LS_KEY); + if (lastSpaceId) { + const space = this.rootSpaces.find(s => s.roomId === lastSpaceId); + if (space) { + this.setActiveSpace(space); + } + } + } + + protected async onAction(payload: ActionPayload) { + switch (payload.action) { + case "view_room": { + const room = this.matrixClient?.getRoom(payload.room_id); + + if (room?.getMyMembership() === "join") { + if (room.isSpaceRoom()) { + this.setActiveSpace(room); + } else if (!this.spaceFilteredRooms.get(this._activeSpace?.roomId || HOME_SPACE).has(room.roomId)) { + // TODO maybe reverse these first 2 clauses once space panel active is fixed + let parent = this.rootSpaces.find(s => this.spaceFilteredRooms.get(s.roomId)?.has(room.roomId)); + if (!parent) { + parent = this.getCanonicalParent(room.roomId); + } + if (!parent) { + const parents = Array.from(this.parentMap.get(room.roomId) || []); + parent = parents.find(p => this.matrixClient.getRoom(p)); + } + if (parent) { + this.setActiveSpace(parent); + } + } + } + break; + } + case "after_leave_room": + if (this._activeSpace && payload.room_id === this._activeSpace.roomId) { + this.setActiveSpace(null); + } + break; + } + } + + public getNotificationState(key: SpaceKey): SpaceNotificationState { + if (this.notificationStateMap.has(key)) { + return this.notificationStateMap.get(key); + } + + const state = new SpaceNotificationState(key, getRoomFn); + this.notificationStateMap.set(key, state); + return state; + } +} + +export default class SpaceStore { + private static internalInstance = new SpaceStoreClass(); + + public static get instance(): SpaceStoreClass { + return SpaceStore.internalInstance; + } +} + +window.mxSpaceStore = SpaceStore.instance; diff --git a/src/stores/notifications/SpaceNotificationState.ts b/src/stores/notifications/SpaceNotificationState.ts new file mode 100644 index 0000000000..61a9701a07 --- /dev/null +++ b/src/stores/notifications/SpaceNotificationState.ts @@ -0,0 +1,82 @@ +/* +Copyright 2021 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { Room } from "matrix-js-sdk/src/models/room"; + +import { NotificationColor } from "./NotificationColor"; +import { arrayDiff } from "../../utils/arrays"; +import { RoomNotificationState } from "./RoomNotificationState"; +import { NOTIFICATION_STATE_UPDATE, NotificationState } from "./NotificationState"; +import { FetchRoomFn } from "./ListNotificationState"; + +export class SpaceNotificationState extends NotificationState { + private rooms: Room[] = []; + private states: { [spaceId: string]: RoomNotificationState } = {}; + + constructor(private spaceId: string | symbol, private getRoomFn: FetchRoomFn) { + super(); + } + + public get symbol(): string { + return null; // This notification state doesn't support symbols + } + + public setRooms(rooms: Room[]) { + const oldRooms = this.rooms; + const diff = arrayDiff(oldRooms, rooms); + this.rooms = rooms; + for (const oldRoom of diff.removed) { + const state = this.states[oldRoom.roomId]; + if (!state) continue; // We likely just didn't have a badge (race condition) + delete this.states[oldRoom.roomId]; + state.off(NOTIFICATION_STATE_UPDATE, this.onRoomNotificationStateUpdate); + } + for (const newRoom of diff.added) { + const state = this.getRoomFn(newRoom); + state.on(NOTIFICATION_STATE_UPDATE, this.onRoomNotificationStateUpdate); + this.states[newRoom.roomId] = state; + } + + this.calculateTotalState(); + } + + public destroy() { + super.destroy(); + for (const state of Object.values(this.states)) { + state.off(NOTIFICATION_STATE_UPDATE, this.onRoomNotificationStateUpdate); + } + this.states = {}; + } + + private onRoomNotificationStateUpdate = () => { + this.calculateTotalState(); + }; + + private calculateTotalState() { + const snapshot = this.snapshot(); + + this._count = 0; + this._color = NotificationColor.None; + for (const state of Object.values(this.states)) { + this._count += state.count; + this._color = Math.max(this.color, state.color); + } + + // finally, publish an update if needed + this.emitIfUpdated(snapshot); + } +} + From f21aedc6cf38d741de183fa1d158d7f7de673699 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 26 Feb 2021 10:23:09 +0000 Subject: [PATCH 33/52] Add Space Panel with Room List filtering --- res/css/_components.scss | 1 + res/css/structures/_LeftPanel.scss | 22 +- res/css/structures/_SpacePanel.scss | 237 ++++++++++++++++++ res/img/element-icons/expand-space-panel.svg | 4 + res/themes/light/css/_mods.scss | 4 + src/components/structures/LeftPanel.tsx | 23 +- src/components/views/avatars/RoomAvatar.tsx | 2 +- src/components/views/spaces/SpacePanel.tsx | 212 ++++++++++++++++ .../views/spaces/SpaceTreeLevel.tsx | 184 ++++++++++++++ src/i18n/strings/en_EN.json | 4 +- src/stores/room-list/RoomListStore.ts | 23 +- src/stores/room-list/SpaceWatcher.ts | 39 +++ src/stores/room-list/algorithms/Algorithm.ts | 3 + .../room-list/filters/SpaceFilterCondition.ts | 69 +++++ 14 files changed, 796 insertions(+), 31 deletions(-) create mode 100644 res/css/structures/_SpacePanel.scss create mode 100644 res/img/element-icons/expand-space-panel.svg create mode 100644 src/components/views/spaces/SpacePanel.tsx create mode 100644 src/components/views/spaces/SpaceTreeLevel.tsx create mode 100644 src/stores/room-list/SpaceWatcher.ts create mode 100644 src/stores/room-list/filters/SpaceFilterCondition.ts diff --git a/res/css/_components.scss b/res/css/_components.scss index 006bac09c9..29b5262826 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -27,6 +27,7 @@ @import "./structures/_RoomView.scss"; @import "./structures/_ScrollPanel.scss"; @import "./structures/_SearchBox.scss"; +@import "./structures/_SpacePanel.scss"; @import "./structures/_TabbedView.scss"; @import "./structures/_ToastContainer.scss"; @import "./structures/_UploadBar.scss"; diff --git a/res/css/structures/_LeftPanel.scss b/res/css/structures/_LeftPanel.scss index 168590502d..f1f27014ee 100644 --- a/res/css/structures/_LeftPanel.scss +++ b/res/css/structures/_LeftPanel.scss @@ -15,6 +15,7 @@ limitations under the License. */ $groupFilterPanelWidth: 56px; // only applies in this file, used for calculations +$roomListCollapsedWidth: 68px; .mx_LeftPanel { background-color: $roomlist-bg-color; @@ -37,18 +38,12 @@ $groupFilterPanelWidth: 56px; // only applies in this file, used for calculation // GroupFilterPanel handles its own CSS } - &:not(.mx_LeftPanel_hasGroupFilterPanel) { - .mx_LeftPanel_roomListContainer { - width: 100%; - } - } - // Note: The 'room list' in this context is actually everything that isn't the tag // panel, such as the menu options, breadcrumbs, filtering, etc .mx_LeftPanel_roomListContainer { - width: calc(100% - $groupFilterPanelWidth); background-color: $roomlist-bg-color; - + flex: 1 0 0; + min-width: 0; // Create another flexbox (this time a column) for the room list components display: flex; flex-direction: column; @@ -168,17 +163,10 @@ $groupFilterPanelWidth: 56px; // only applies in this file, used for calculation // These styles override the defaults for the minimized (66px) layout &.mx_LeftPanel_minimized { min-width: unset; - - // We have to forcefully set the width to override the resizer's style attribute. - &.mx_LeftPanel_hasGroupFilterPanel { - width: calc(68px + $groupFilterPanelWidth) !important; - } - &:not(.mx_LeftPanel_hasGroupFilterPanel) { - width: 68px !important; - } + width: unset !important; .mx_LeftPanel_roomListContainer { - width: 68px; + width: $roomListCollapsedWidth; .mx_LeftPanel_userHeader { flex-direction: row; diff --git a/res/css/structures/_SpacePanel.scss b/res/css/structures/_SpacePanel.scss new file mode 100644 index 0000000000..8de85f95ef --- /dev/null +++ b/res/css/structures/_SpacePanel.scss @@ -0,0 +1,237 @@ +/* +Copyright 2021 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); + + +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +$topLevelHeight: 32px; +$nestedHeight: 24px; +$gutterSize: 21px; +$activeStripeSize: 4px; + +.mx_SpacePanel { + flex: 0 0 auto; + background-color: $groupFilterPanel-bg-color; + padding: 0; + margin: 0; + + // Create another flexbox so the Panel fills the container + display: flex; + flex-direction: column; + overflow-y: auto; + + .mx_SpacePanel_spaceTreeWrapper { + flex: 1; + } + + .mx_SpacePanel_toggleCollapse { + flex: 0 0 auto; + width: 40px; + height: 40px; + mask-position: center; + mask-size: 32px; + mask-repeat: no-repeat; + margin-left: $gutterSize; + margin-bottom: 12px; + background-color: $roomlist-header-color; + mask-image: url('$(res)/img/element-icons/expand-space-panel.svg'); + + &.expanded { + transform: scaleX(-1); + } + } + + ul { + margin: 0; + list-style: none; + padding: 0; + padding-left: 16px; + } + + .mx_AutoHideScrollbar { + padding: 16px 12px 16px 0; + } + + .mx_SpaceButton_toggleCollapse { + cursor: pointer; + } + + .mx_SpaceItem { + position: relative; + } + + .mx_SpaceItem.collapsed { + & > .mx_SpaceButton > .mx_SpaceButton_toggleCollapse { + transform: rotate(-90deg); + } + + & > .mx_SpaceTreeLevel { + display: none; + } + } + + .mx_SpaceItem:not(.hasSubSpaces) > .mx_SpaceButton { + margin-left: $gutterSize; + + &.mx_SpaceButton_active { + &::before { + left: -$gutterSize; + } + } + } + + .mx_SpaceButton { + border-radius: 8px; + position: relative; + margin-bottom: 16px; + display: flex; + align-items: center; + + .mx_SpaceButton_name { + flex: 1; + margin-left: 8px; + white-space: nowrap; + display: block; + max-width: 150px; + text-overflow: ellipsis; + overflow: hidden; + + font-size: $font-14px; + line-height: $font-18px; + } + + .mx_SpaceButton_toggleCollapse { + width: calc($gutterSize - $activeStripeSize); + margin-left: 1px; + height: 20px; + mask-position: center; + mask-size: 20px; + mask-repeat: no-repeat; + background-color: $roomlist-header-color; + mask-image: url('$(res)/img/feather-customised/chevron-down.svg'); + } + + &.mx_SpaceButton_active { + &::before { + position: absolute; + content: ''; + width: $activeStripeSize; + top: 0; + left: 0; + bottom: 0; + background-color: $accent-color; + border-radius: 0 4px 4px 0; + } + } + + .mx_SpaceButton_avatarPlaceholder { + width: $topLevelHeight; + min-width: $topLevelHeight; + height: $topLevelHeight; + border-radius: 8px; + + &::before { + position: absolute; + content: ''; + width: $topLevelHeight; + height: $topLevelHeight; + top: 0; + left: 0; + mask-position: center; + mask-repeat: no-repeat; + mask-size: 18px; + } + } + + &.mx_SpaceButton_home .mx_SpaceButton_avatarPlaceholder { + background-color: #ffffff; + + &::before { + background-color: #3f3d3d; + mask-image: url('$(res)/img/element-icons/home.svg'); + } + } + + &.mx_SpaceButton_newCancel .mx_SpaceButton_avatarPlaceholder { + background-color: $icon-button-color; + + &::before { + transform: rotate(45deg); + } + } + + .mx_BaseAvatar_image { + border-radius: 8px; + } + } + + .mx_SpacePanel_badgeContainer { + height: 16px; + // don't set width so that it takes no space when there is no badge to show + margin: auto 0; // vertically align + + // Create a flexbox to make aligning dot badges easier + display: flex; + align-items: center; + + .mx_NotificationBadge { + margin: 0 2px; // centering + } + + .mx_NotificationBadge_dot { + // make the smaller dot occupy the same width for centering + margin-left: 7px; + margin-right: 7px; + } + } + + &.collapsed { + .mx_SpaceButton { + .mx_SpacePanel_badgeContainer { + position: absolute; + right: -8px; + top: -4px; + } + } + } + + &:not(.collapsed) { + .mx_SpaceButton:hover, + .mx_SpaceButton:focus-within, + .mx_SpaceButton_hasMenuOpen { + // Hide the badge container on hover because it'll be a menu button + .mx_SpacePanel_badgeContainer { + width: 0; + height: 0; + display: none; + } + } + } + + /* root space buttons are bigger and not indented */ + & > .mx_AutoHideScrollbar { + & > .mx_SpaceButton { + height: $topLevelHeight; + + &.mx_SpaceButton_active::before { + height: $topLevelHeight; + } + } + + & > ul { + padding-left: 0; + } + } +} diff --git a/res/img/element-icons/expand-space-panel.svg b/res/img/element-icons/expand-space-panel.svg new file mode 100644 index 0000000000..11232acd58 --- /dev/null +++ b/res/img/element-icons/expand-space-panel.svg @@ -0,0 +1,4 @@ + + + + diff --git a/res/themes/light/css/_mods.scss b/res/themes/light/css/_mods.scss index 30aaeedf8f..fbca58dfb1 100644 --- a/res/themes/light/css/_mods.scss +++ b/res/themes/light/css/_mods.scss @@ -16,6 +16,10 @@ backdrop-filter: blur($groupFilterPanel-background-blur-amount); } + .mx_SpacePanel { + backdrop-filter: blur($groupFilterPanel-background-blur-amount); + } + .mx_LeftPanel .mx_LeftPanel_roomListContainer { backdrop-filter: blur($roomlist-background-blur-amount); } diff --git a/src/components/structures/LeftPanel.tsx b/src/components/structures/LeftPanel.tsx index 4445ff3ff8..82dd9443cc 100644 --- a/src/components/structures/LeftPanel.tsx +++ b/src/components/structures/LeftPanel.tsx @@ -39,6 +39,7 @@ import { OwnProfileStore } from "../../stores/OwnProfileStore"; import { MatrixClientPeg } from "../../MatrixClientPeg"; import RoomListNumResults from "../views/rooms/RoomListNumResults"; import LeftPanelWidget from "./LeftPanelWidget"; +import SpacePanel from "../views/spaces/SpacePanel"; interface IProps { isMinimized: boolean; @@ -388,12 +389,19 @@ export default class LeftPanel extends React.Component { } public render(): React.ReactNode { - const groupFilterPanel = !this.state.showGroupFilterPanel ? null : ( -
- - {SettingsStore.getValue("feature_custom_tags") ? : null} -
- ); + let leftLeftPanel; + // Currently TagPanel.enableTagPanel is disabled when Legacy Communities are disabled so for now + // ignore it and force the rendering of SpacePanel if that Labs flag is enabled. + if (SettingsStore.getValue("feature_spaces")) { + leftLeftPanel = ; + } else if (this.state.showGroupFilterPanel) { + leftLeftPanel = ( +
+ + {SettingsStore.getValue("feature_custom_tags") ? : null} +
+ ); + } const roomList = { const containerClasses = classNames({ "mx_LeftPanel": true, - "mx_LeftPanel_hasGroupFilterPanel": !!groupFilterPanel, "mx_LeftPanel_minimized": this.props.isMinimized, }); @@ -417,7 +424,7 @@ export default class LeftPanel extends React.Component { return (
- {groupFilterPanel} + {leftLeftPanel}