Merge pull request #5452 from SimonBrandner/feature-multi-language-spell-check
Add multi language spell checkpull/21833/head
commit
a5ee029c62
|
@ -213,6 +213,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";
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com>
|
||||
|
||||
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;
|
||||
}
|
|
@ -131,6 +131,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.
|
||||
|
@ -240,6 +248,16 @@ export default abstract class BasePlatform {
|
|||
|
||||
setLanguage(preferredLangs: string[]) {}
|
||||
|
||||
setSpellCheckLanguages(preferredLangs: string[]) {}
|
||||
|
||||
getSpellCheckLanguages(): Promise<string[]> | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
getAvailableSpellCheckLanguages(): Promise<string[]> | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
protected getSSOCallbackUrl(fragmentAfterLogin: string): URL {
|
||||
const url = new URL(window.location.href);
|
||||
url.hash = fragmentAfterLogin || "";
|
||||
|
|
|
@ -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 <Dropdown
|
||||
|
|
|
@ -0,0 +1,126 @@
|
|||
/*
|
||||
Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com>
|
||||
|
||||
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 Dropdown from "../../views/elements/Dropdown"
|
||||
import * as sdk from '../../../index';
|
||||
import PlatformPeg from "../../../PlatformPeg";
|
||||
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<SpellCheckLanguagesDropdownIProps,
|
||||
SpellCheckLanguagesDropdownIState> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this._onSearchChange = this._onSearchChange.bind(this);
|
||||
|
||||
this.state = {
|
||||
searchQuery: '',
|
||||
languages: null,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
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']});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_onSearchChange(search) {
|
||||
this.setState({
|
||||
searchQuery: search,
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.languages === null) {
|
||||
const Spinner = sdk.getComponent('elements.Spinner');
|
||||
return <Spinner />;
|
||||
}
|
||||
|
||||
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 <div key={language.value}>
|
||||
{ language.label }
|
||||
</div>;
|
||||
});
|
||||
|
||||
// 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 <Dropdown
|
||||
id="mx_LanguageDropdown"
|
||||
className={this.props.className}
|
||||
onOptionChange={this.props.onOptionChange}
|
||||
onSearchChange={this._onSearchChange}
|
||||
searchEnabled={true}
|
||||
value={value}
|
||||
label={_t("Language Dropdown")}>
|
||||
{ options }
|
||||
</Dropdown>;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,111 @@
|
|||
/*
|
||||
Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com>
|
||||
|
||||
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 SpellCheckLanguagesDropdown from "../../../components/views/elements/SpellCheckLanguagesDropdown";
|
||||
import AccessibleButton from "../../../components/views/elements/AccessibleButton";
|
||||
import {_t} from "../../../languageHandler";
|
||||
|
||||
interface ExistingSpellCheckLanguageIProps {
|
||||
language: string,
|
||||
onRemoved(language: string),
|
||||
}
|
||||
|
||||
interface SpellCheckLanguagesIProps {
|
||||
languages: Array<string>,
|
||||
onLanguagesChange(languages: Array<string>),
|
||||
}
|
||||
|
||||
interface SpellCheckLanguagesIState {
|
||||
newLanguage: string,
|
||||
}
|
||||
|
||||
export class ExistingSpellCheckLanguage extends React.Component<ExistingSpellCheckLanguageIProps> {
|
||||
_onRemove = (e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
return this.props.onRemoved(this.props.language);
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="mx_ExistingSpellCheckLanguage">
|
||||
<span className="mx_ExistingSpellCheckLanguage_language">{this.props.language}</span>
|
||||
<AccessibleButton onClick={this._onRemove} kind="danger_sm">
|
||||
{_t("Remove")}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default class SpellCheckLanguages extends React.Component<SpellCheckLanguagesIProps, SpellCheckLanguagesIState> {
|
||||
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 <ExistingSpellCheckLanguage language={e} onRemoved={this._onRemoved} key={e} />;
|
||||
});
|
||||
|
||||
const addButton = (
|
||||
<AccessibleButton onClick={this._onAddClick} kind="primary">
|
||||
{_t("Add")}
|
||||
</AccessibleButton>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="mx_SpellCheckLanguages">
|
||||
{existingSpellCheckLanguages}
|
||||
<form onSubmit={this._onAddClick} noValidate={true}>
|
||||
<SpellCheckLanguagesDropdown
|
||||
className="mx_GeneralUserSettingsTab_spellCheckLanguageInput"
|
||||
value={this.state.newLanguage}
|
||||
onOptionChange={this._onNewLanguageChange} />
|
||||
{addButton}
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
|
@ -85,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);
|
||||
}
|
||||
|
@ -182,6 +193,15 @@ export default class GeneralUserSettingsTab extends React.Component {
|
|||
PlatformPeg.get().reload();
|
||||
};
|
||||
|
||||
_onSpellCheckLanguagesChange = (languages) => {
|
||||
this.setState({spellCheckLanguages: languages});
|
||||
|
||||
const plaf = PlatformPeg.get();
|
||||
if (plaf) {
|
||||
plaf.setSpellCheckLanguages(languages);
|
||||
}
|
||||
};
|
||||
|
||||
_onPasswordChangeError = (err) => {
|
||||
// TODO: Figure out a design that doesn't involve replacing the current dialog
|
||||
let errMsg = err.error || "";
|
||||
|
@ -303,6 +323,16 @@ export default class GeneralUserSettingsTab extends React.Component {
|
|||
);
|
||||
}
|
||||
|
||||
_renderSpellCheckSection() {
|
||||
return (
|
||||
<div className="mx_SettingsTab_section">
|
||||
<span className="mx_SettingsTab_subheading">{_t("Spell check dictionaries")}</span>
|
||||
<SpellCheckSettings languages={this.state.spellCheckLanguages}
|
||||
onLanguagesChange={this._onSpellCheckLanguagesChange} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
_renderDiscoverySection() {
|
||||
const SetIdServer = sdk.getComponent("views.settings.SetIdServer");
|
||||
|
||||
|
@ -381,6 +411,9 @@ export default class GeneralUserSettingsTab extends React.Component {
|
|||
}
|
||||
|
||||
render() {
|
||||
const plaf = PlatformPeg.get();
|
||||
const supportsMultiLanguageSpellCheck = plaf.supportsMultiLanguageSpellCheck();
|
||||
|
||||
const discoWarning = this.state.requiredPolicyInfo.hasTerms
|
||||
? <img className='mx_GeneralUserSettingsTab_warningIcon'
|
||||
src={require("../../../../../../res/img/feather-customised/warning-triangle.svg")}
|
||||
|
@ -409,6 +442,7 @@ export default class GeneralUserSettingsTab extends React.Component {
|
|||
{this._renderProfileSection()}
|
||||
{this._renderAccountSection()}
|
||||
{this._renderLanguageSection()}
|
||||
{supportsMultiLanguageSpellCheck ? this._renderSpellCheckSection() : null}
|
||||
{ discoverySection }
|
||||
{this._renderIntegrationManagerSection() /* Has its own title */}
|
||||
{ accountManagementSection }
|
||||
|
|
|
@ -1151,6 +1151,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.",
|
||||
|
@ -1182,6 +1183,7 @@
|
|||
"Set a new account password...": "Set a new account password...",
|
||||
"Account": "Account",
|
||||
"Language and region": "Language and region",
|
||||
"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!",
|
||||
|
@ -1379,7 +1381,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?",
|
||||
|
|
Loading…
Reference in New Issue