Skip to content
This repository has been archived by the owner on Sep 11, 2024. It is now read-only.

Move language settings to 'preferences' #12723

Merged
merged 6 commits into from
Jul 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 0 additions & 18 deletions playwright/e2e/settings/general-user-settings-tab.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,24 +73,6 @@ test.describe("General user settings tab", () => {
// Assert that the add button is rendered
await expect(phoneNumbers.getByRole("button", { name: "Add" })).toBeVisible();

// Check language and region setting dropdown
const languageInput = uut.locator(".mx_GeneralUserSettingsTab_section_languageInput");
await languageInput.scrollIntoViewIfNeeded();
// Check the default value
await expect(languageInput.getByText("English")).toBeVisible();
// Click the button to display the dropdown menu
await languageInput.getByRole("button", { name: "Language Dropdown" }).click();
// Assert that the default option is rendered and highlighted
languageInput.getByRole("option", { name: /Albanian/ });
await expect(languageInput.getByRole("option", { name: /Albanian/ })).toHaveClass(
/mx_Dropdown_option_highlight/,
);
await expect(languageInput.getByRole("option", { name: /Deutsch/ })).toBeVisible();
// Click again to close the dropdown
await languageInput.getByRole("button", { name: "Language Dropdown" }).click();
// Assert that the default value is rendered again
await expect(languageInput.getByText("English")).toBeVisible();

const setIntegrationManager = uut.locator(".mx_SetIntegrationManager");
await setIntegrationManager.scrollIntoViewIfNeeded();
await expect(
Expand Down
25 changes: 25 additions & 0 deletions playwright/e2e/settings/preferences-user-settings-tab.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/*
Copyright 2023 Suguru Hirahara
Copyright 2024 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.
Expand All @@ -19,6 +20,10 @@ import { test, expect } from "../../element-web-test";
test.describe("Preferences user settings tab", () => {
test.use({
displayName: "Bob",
uut: async ({ app, user }, use) => {
const locator = await app.settings.openUserSettings("Preferences");
await use(locator);
},
});

test("should be rendered properly", async ({ app, user }) => {
Expand All @@ -28,4 +33,24 @@ test.describe("Preferences user settings tab", () => {
await expect(tab.getByRole("heading", { name: "Preferences" })).toBeVisible();
await expect(tab).toMatchScreenshot();
});

test("should be able to change the app language", async ({ uut, user }) => {
// Check language and region setting dropdown
const languageInput = uut.locator(".mx_GeneralUserSettingsTab_section_languageInput");
await languageInput.scrollIntoViewIfNeeded();
// Check the default value
await expect(languageInput.getByText("English")).toBeVisible();
// Click the button to display the dropdown menu
await languageInput.getByRole("button", { name: "Language Dropdown" }).click();
// Assert that the default option is rendered and highlighted
languageInput.getByRole("option", { name: /Albanian/ });
await expect(languageInput.getByRole("option", { name: /Albanian/ })).toHaveClass(
/mx_Dropdown_option_highlight/,
);
await expect(languageInput.getByRole("option", { name: /Deutsch/ })).toBeVisible();
// Click again to close the dropdown
await languageInput.getByRole("button", { name: "Language Dropdown" }).click();
// Assert that the default value is rendered again
await expect(languageInput.getByText("English")).toBeVisible();
});
});
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,8 @@ limitations under the License.
margin-right: $spacing-8;
margin-bottom: 2px;
}

.mx_GeneralUserSettingsTab_section_hint {
font: var(--cpd-font-body-sm-regular);
color: var(--cpd-color-text-secondary);
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,25 +22,17 @@ import { logger } from "matrix-js-sdk/src/logger";

import { UserFriendlyError, _t } from "../../../../../languageHandler";
import UserProfileSettings from "../../UserProfileSettings";
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 PlatformPeg from "../../../../../PlatformPeg";
import Modal from "../../../../../Modal";
import { SettingLevel } from "../../../../../settings/SettingLevel";
import { UIFeature } from "../../../../../settings/UIFeature";
import ErrorDialog, { extractErrorMessageFromError } from "../../../dialogs/ErrorDialog";
import ChangePassword from "../../ChangePassword";
import SetIntegrationManager from "../../SetIntegrationManager";
import ToggleSwitch from "../../../elements/ToggleSwitch";
import { IS_MAC } from "../../../../../Keyboard";
import SettingsTab from "../SettingsTab";
import { SettingsSection } from "../../shared/SettingsSection";
import SettingsSubsection, { SettingsSubsectionText } from "../../shared/SettingsSubsection";
import { SettingsSubsectionHeading } from "../../shared/SettingsSubsectionHeading";
import { SDKContext } from "../../../../../contexts/SDKContext";
import UserPersonalInfoSettings from "../../UserPersonalInfoSettings";

Expand All @@ -49,9 +41,6 @@ interface IProps {
}

interface IState {
language: string;
spellCheckEnabled?: boolean;
spellCheckLanguages: string[];
canChangePassword: boolean;
idServerName?: string;
externalAccountManagementUrl?: string;
Expand All @@ -69,9 +58,6 @@ export default class GeneralUserSettingsTab extends React.Component<IProps, ISta
this.context = context;

this.state = {
language: languageHandler.getCurrentLanguage(),
spellCheckEnabled: false,
spellCheckLanguages: [],
canChangePassword: false,
canMake3pidChanges: false,
canSetDisplayName: false,
Expand All @@ -81,21 +67,6 @@ export default class GeneralUserSettingsTab extends React.Component<IProps, ISta
this.getCapabilities();
}

public async componentDidMount(): Promise<void> {
const plat = PlatformPeg.get();
const [spellCheckEnabled, spellCheckLanguages] = await Promise.all([
plat?.getSpellCheckEnabled(),
plat?.getSpellCheckLanguages(),
]);

if (spellCheckLanguages) {
this.setState({
spellCheckEnabled,
spellCheckLanguages,
});
}
}

private async getCapabilities(): Promise<void> {
const cli = this.context.client!;

Expand Down Expand Up @@ -127,28 +98,6 @@ export default class GeneralUserSettingsTab extends React.Component<IProps, ISta
});
}

private onLanguageChange = (newLanguage: string): void => {
if (this.state.language === newLanguage) return;

SettingsStore.setValue("language", null, SettingLevel.DEVICE, newLanguage);
this.setState({ language: newLanguage });
const platform = PlatformPeg.get();
if (platform) {
platform.setLanguage([newLanguage]);
platform.reload();
}
};

private onSpellCheckLanguagesChange = (languages: string[]): void => {
this.setState({ spellCheckLanguages: languages });
PlatformPeg.get()?.setSpellCheckLanguages(languages);
};

private onSpellCheckEnabledChange = (spellCheckEnabled: boolean): void => {
this.setState({ spellCheckEnabled });
PlatformPeg.get()?.setSpellCheckEnabled(spellCheckEnabled);
};

private onPasswordChangeError = (err: Error): void => {
logger.error("Failed to change password: " + err);

Expand Down Expand Up @@ -256,37 +205,6 @@ export default class GeneralUserSettingsTab extends React.Component<IProps, ISta
);
}

private renderLanguageSection(): JSX.Element {
// TODO: Convert to new-styled Field
return (
<SettingsSubsection heading={_t("settings|general|language_section")} stretchContent>
<LanguageDropdown
className="mx_GeneralUserSettingsTab_section_languageInput"
onOptionChange={this.onLanguageChange}
value={this.state.language}
/>
</SettingsSubsection>
);
}

private renderSpellCheckSection(): JSX.Element {
const heading = (
<SettingsSubsectionHeading heading={_t("settings|general|spell_check_section")}>
<ToggleSwitch checked={!!this.state.spellCheckEnabled} onChange={this.onSpellCheckEnabledChange} />
</SettingsSubsectionHeading>
);
return (
<SettingsSubsection heading={heading} stretchContent>
{this.state.spellCheckEnabled && !IS_MAC && (
<SpellCheckSettings
languages={this.state.spellCheckLanguages}
onLanguagesChange={this.onSpellCheckLanguagesChange}
/>
)}
</SettingsSubsection>
);
}

private renderManagementSection(): JSX.Element {
// TODO: Improve warning text for account deactivation
return (
Expand All @@ -311,9 +229,6 @@ export default class GeneralUserSettingsTab extends React.Component<IProps, ISta
}

public render(): React.ReactNode {
const plaf = PlatformPeg.get();
const supportsMultiLanguageSpellCheck = plaf?.supportsSpellCheckSettings();

let accountManagementSection: JSX.Element | undefined;
const isAccountManagedExternally = !!this.state.externalAccountManagementUrl;
if (SettingsStore.getValue(UIFeature.Deactivate) && !isAccountManagedExternally) {
Expand All @@ -329,8 +244,6 @@ export default class GeneralUserSettingsTab extends React.Component<IProps, ISta
/>
<UserPersonalInfoSettings canMake3pidChanges={this.state.canMake3pidChanges} />
{this.renderAccountSection()}
{this.renderLanguageSection()}
{supportsMultiLanguageSpellCheck ? this.renderSpellCheckSection() : null}
</SettingsSection>
{this.renderIntegrationManagerSection()}
{accountManagementSection}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ See the License for the specific language governing permissions and
limitations under the License.
*/

import React from "react";
import React, { useCallback, useEffect, useState } from "react";

import { _t } from "../../../../../languageHandler";
import { _t, getCurrentLanguage } from "../../../../../languageHandler";
import { UseCase } from "../../../../../settings/enums/UseCase";
import SettingsStore from "../../../../../settings/SettingsStore";
import Field from "../../../elements/Field";
Expand All @@ -33,6 +33,11 @@ import { showUserOnboardingPage } from "../../../user-onboarding/UserOnboardingP
import SettingsSubsection from "../../shared/SettingsSubsection";
import SettingsTab from "../SettingsTab";
import { SettingsSection } from "../../shared/SettingsSection";
import LanguageDropdown from "../../../elements/LanguageDropdown";
import PlatformPeg from "../../../../../PlatformPeg";
import { IS_MAC } from "../../../../../Keyboard";
import SpellCheckSettings from "../../SpellCheckSettings";
import LabelledToggleSwitch from "../../../elements/LabelledToggleSwitch";

interface IProps {
closeSettingsFn(success: boolean): void;
Expand All @@ -44,6 +49,79 @@ interface IState {
readMarkerOutOfViewThresholdMs: string;
}

const LanguageSection: React.FC = () => {
const [language, setLanguage] = useState(getCurrentLanguage());

const onLanguageChange = useCallback(
(newLanguage: string) => {
if (language === newLanguage) return;

SettingsStore.setValue("language", null, SettingLevel.DEVICE, newLanguage);
setLanguage(newLanguage);
const platform = PlatformPeg.get();
if (platform) {
platform.setLanguage([newLanguage]);
platform.reload();
}
},
[language],
);

return (
<div className="mx_SettingsSubsection_contentStretch">
{_t("settings|general|application_language")}
<LanguageDropdown
className="mx_GeneralUserSettingsTab_section_languageInput"
onOptionChange={onLanguageChange}
value={language}
/>
<div className="mx_GeneralUserSettingsTab_section_hint">
{_t("settings|general|application_language_reload_hint")}
</div>
</div>
);
};

const SpellCheckSection: React.FC = () => {
const [spellCheckEnabled, setSpellCheckEnabled] = useState<boolean | undefined>();
const [spellCheckLanguages, setSpellCheckLanguages] = useState<string[] | undefined>();

useEffect(() => {
(async () => {
const plaf = PlatformPeg.get();
const [enabled, langs] = await Promise.all([plaf?.getSpellCheckEnabled(), plaf?.getSpellCheckLanguages()]);

setSpellCheckEnabled(enabled);
setSpellCheckLanguages(langs || undefined);
})();
}, []);

const onSpellCheckEnabledChange = useCallback((enabled: boolean) => {
setSpellCheckEnabled(enabled);
PlatformPeg.get()?.setSpellCheckEnabled(enabled);
}, []);

const onSpellCheckLanguagesChange = useCallback((languages: string[]): void => {
setSpellCheckLanguages(languages);
PlatformPeg.get()?.setSpellCheckLanguages(languages);
}, []);

if (!PlatformPeg.get()?.supportsSpellCheckSettings()) return null;

return (
<>
<LabelledToggleSwitch
label={_t("settings|general|allow_spellcheck")}
value={Boolean(spellCheckEnabled)}
onChange={onSpellCheckEnabledChange}
/>
{spellCheckEnabled && spellCheckLanguages !== undefined && !IS_MAC && (
<SpellCheckSettings languages={spellCheckLanguages} onLanguagesChange={onSpellCheckLanguagesChange} />
)}
</>
);
};

export default class PreferencesUserSettingsTab extends React.Component<IProps, IState> {
private static ROOM_LIST_SETTINGS = ["breadcrumbs", "FTUE.userOnboardingButton"];

Expand Down Expand Up @@ -146,6 +224,12 @@ export default class PreferencesUserSettingsTab extends React.Component<IProps,
return (
<SettingsTab data-testid="mx_PreferencesUserSettingsTab">
<SettingsSection>
{/* The heading string is still 'general' from where it was moved, but this section should become 'general' */}
<SettingsSubsection heading={_t("settings|general|language_section")}>
<LanguageSection />
<SpellCheckSection />
</SettingsSubsection>

{roomListSettings.length > 0 && (
<SettingsSubsection heading={_t("settings|preferences|room_list_heading")}>
{this.renderGroup(roomListSettings)}
Expand Down
6 changes: 4 additions & 2 deletions src/i18n/strings/en_EN.json
Original file line number Diff line number Diff line change
Expand Up @@ -2461,6 +2461,9 @@
"add_msisdn_dialog_title": "Add Phone Number",
"add_msisdn_instructions": "A text message has been sent to +%(msisdn)s. Please enter the verification code it contains.",
"add_msisdn_misconfigured": "The add / bind with MSISDN flow is misconfigured",
"allow_spellcheck": "Allow spell check",
"application_language": "Application language",
"application_language_reload_hint": "The app will reload after selecting another language",
"avatar_remove_progress": "Removing image...",
"avatar_save_progress": "Uploading image...",
"avatar_upload_error_text": "The file format is not supported or the image is larger than %(size)s.",
Expand Down Expand Up @@ -2515,7 +2518,7 @@
"identity_server_no_token": "No identity access token found",
"identity_server_not_set": "Identity server not set",
"incorrect_msisdn_verification": "Incorrect verification code",
"language_section": "Language and region",
"language_section": "Language",
"msisdn_in_use": "This phone number is already in use",
"msisdn_label": "Phone Number",
"msisdn_verification_field_label": "Verification code",
Expand All @@ -2531,7 +2534,6 @@
"remove_email_prompt": "Remove %(email)s?",
"remove_msisdn_prompt": "Remove %(phone)s?",
"spell_check_locale_placeholder": "Choose a locale",
"spell_check_section": "Spell check",
"unable_to_load_emails": "Unable to load email addresses",
"unable_to_load_msisdns": "Unable to load phone numbers",
"username": "Username"
Expand Down
Loading
Loading