diff --git a/playwright/e2e/settings/general-user-settings-tab.spec.ts b/playwright/e2e/settings/general-user-settings-tab.spec.ts index 32946053d8..050cd76d00 100644 --- a/playwright/e2e/settings/general-user-settings-tab.spec.ts +++ b/playwright/e2e/settings/general-user-settings-tab.spec.ts @@ -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( diff --git a/playwright/e2e/settings/preferences-user-settings-tab.spec.ts b/playwright/e2e/settings/preferences-user-settings-tab.spec.ts index 2dbd267162..22baa19a8a 100644 --- a/playwright/e2e/settings/preferences-user-settings-tab.spec.ts +++ b/playwright/e2e/settings/preferences-user-settings-tab.spec.ts @@ -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. @@ -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 }) => { @@ -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(); + }); }); diff --git a/playwright/snapshots/settings/preferences-user-settings-tab.spec.ts/Preferences-user-settings-tab-should-be-rendered-properly-1-linux.png b/playwright/snapshots/settings/preferences-user-settings-tab.spec.ts/Preferences-user-settings-tab-should-be-rendered-properly-1-linux.png index d0bc1288ec..ca2e75dfbb 100644 Binary files a/playwright/snapshots/settings/preferences-user-settings-tab.spec.ts/Preferences-user-settings-tab-should-be-rendered-properly-1-linux.png and b/playwright/snapshots/settings/preferences-user-settings-tab.spec.ts/Preferences-user-settings-tab-should-be-rendered-properly-1-linux.png differ diff --git a/res/css/views/settings/tabs/user/_GeneralUserSettingsTab.pcss b/res/css/views/settings/tabs/user/_GeneralUserSettingsTab.pcss index 76c5834fa8..a59f64b391 100644 --- a/res/css/views/settings/tabs/user/_GeneralUserSettingsTab.pcss +++ b/res/css/views/settings/tabs/user/_GeneralUserSettingsTab.pcss @@ -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); +} diff --git a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.tsx b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.tsx index 46c13430d3..5925e389ec 100644 --- a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.tsx @@ -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"; @@ -49,9 +41,6 @@ interface IProps { } interface IState { - language: string; - spellCheckEnabled?: boolean; - spellCheckLanguages: string[]; canChangePassword: boolean; idServerName?: string; externalAccountManagementUrl?: string; @@ -69,9 +58,6 @@ export default class GeneralUserSettingsTab extends React.Component { - const plat = PlatformPeg.get(); - const [spellCheckEnabled, spellCheckLanguages] = await Promise.all([ - plat?.getSpellCheckEnabled(), - plat?.getSpellCheckLanguages(), - ]); - - if (spellCheckLanguages) { - this.setState({ - spellCheckEnabled, - spellCheckLanguages, - }); - } - } - private async getCapabilities(): Promise { const cli = this.context.client!; @@ -127,28 +98,6 @@ export default class GeneralUserSettingsTab extends React.Component { - 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); @@ -228,37 +177,6 @@ export default class GeneralUserSettingsTab extends React.Component - - - ); - } - - private renderSpellCheckSection(): JSX.Element { - const heading = ( - - - - ); - return ( - - {this.state.spellCheckEnabled && !IS_MAC && ( - - )} - - ); - } - private renderManagementSection(): JSX.Element { // TODO: Improve warning text for account deactivation return ( @@ -283,9 +201,6 @@ export default class GeneralUserSettingsTab extends React.Component {this.renderAccountSection()} - {this.renderLanguageSection()} - {supportsMultiLanguageSpellCheck ? this.renderSpellCheckSection() : null} {this.renderIntegrationManagerSection()} {accountManagementSection} diff --git a/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx b/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx index 6df2a1a03c..439cc2122f 100644 --- a/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx @@ -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"; @@ -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; @@ -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 ( +
+ {_t("settings|general|application_language")} + +
+ {_t("settings|general|application_language_reload_hint")} +
+
+ ); +}; + +const SpellCheckSection: React.FC = () => { + const [spellCheckEnabled, setSpellCheckEnabled] = useState(); + const [spellCheckLanguages, setSpellCheckLanguages] = useState(); + + 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 ( + <> + + {spellCheckEnabled && spellCheckLanguages !== undefined && !IS_MAC && ( + + )} + + ); +}; + export default class PreferencesUserSettingsTab extends React.Component { private static ROOM_LIST_SETTINGS = ["breadcrumbs", "FTUE.userOnboardingButton"]; @@ -146,6 +224,12 @@ export default class PreferencesUserSettingsTab extends React.Component + {/* The heading string is still 'general' from where it was moved, but this section should become 'general' */} + + + + + {roomListSettings.length > 0 && ( {this.renderGroup(roomListSettings)} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 5272e22c85..6d0f97130d 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2463,6 +2463,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.", @@ -2516,7 +2519,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", @@ -2532,7 +2535,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" diff --git a/test/components/views/settings/tabs/user/PreferencesUserSettingsTab-test.tsx b/test/components/views/settings/tabs/user/PreferencesUserSettingsTab-test.tsx index 82abc8fed6..4541720159 100644 --- a/test/components/views/settings/tabs/user/PreferencesUserSettingsTab-test.tsx +++ b/test/components/views/settings/tabs/user/PreferencesUserSettingsTab-test.tsx @@ -15,7 +15,8 @@ limitations under the License. */ import React from "react"; -import { fireEvent, render, RenderResult, waitFor } from "@testing-library/react"; +import { fireEvent, render, RenderResult, screen, waitFor } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; import PreferencesUserSettingsTab from "../../../../../../src/components/views/settings/tabs/user/PreferencesUserSettingsTab"; import { MatrixClientPeg } from "../../../../../../src/MatrixClientPeg"; @@ -23,6 +24,7 @@ import { mockPlatformPeg, stubClient } from "../../../../../test-utils"; import SettingsStore from "../../../../../../src/settings/SettingsStore"; import { SettingLevel } from "../../../../../../src/settings/SettingLevel"; import MatrixClientBackedController from "../../../../../../src/settings/controllers/MatrixClientBackedController"; +import PlatformPeg from "../../../../../../src/PlatformPeg"; describe("PreferencesUserSettingsTab", () => { beforeEach(() => { @@ -38,6 +40,43 @@ describe("PreferencesUserSettingsTab", () => { expect(asFragment()).toMatchSnapshot(); }); + it("should reload when changing language", async () => { + const reloadStub = jest.fn(); + PlatformPeg.get()!.reload = reloadStub; + + renderTab(); + const languageDropdown = await screen.findByRole("button", { name: "Language Dropdown" }); + expect(languageDropdown).toBeInTheDocument(); + + await userEvent.click(languageDropdown); + + const germanOption = await screen.findByText("Deutsch"); + await userEvent.click(germanOption); + expect(reloadStub).toHaveBeenCalled(); + }); + + it("should not show spell check setting if unsupported", async () => { + PlatformPeg.get()!.supportsSpellCheckSettings = jest.fn().mockReturnValue(false); + + renderTab(); + expect(screen.queryByRole("switch", { name: "Allow spell check" })).not.toBeInTheDocument(); + }); + + it("should enable spell check", async () => { + const spellCheckEnableFn = jest.fn(); + PlatformPeg.get()!.supportsSpellCheckSettings = jest.fn().mockReturnValue(true); + PlatformPeg.get()!.getSpellCheckEnabled = jest.fn().mockReturnValue(false); + PlatformPeg.get()!.setSpellCheckEnabled = spellCheckEnableFn; + + renderTab(); + const toggle = await screen.findByRole("switch", { name: "Allow spell check" }); + expect(toggle).toHaveAttribute("aria-checked", "false"); + + await userEvent.click(toggle); + + expect(spellCheckEnableFn).toHaveBeenCalledWith(true); + }); + describe("send read receipts", () => { beforeEach(() => { stubClient(); diff --git a/test/components/views/settings/tabs/user/__snapshots__/PreferencesUserSettingsTab-test.tsx.snap b/test/components/views/settings/tabs/user/__snapshots__/PreferencesUserSettingsTab-test.tsx.snap index e9050a94da..7c535aae44 100644 --- a/test/components/views/settings/tabs/user/__snapshots__/PreferencesUserSettingsTab-test.tsx.snap +++ b/test/components/views/settings/tabs/user/__snapshots__/PreferencesUserSettingsTab-test.tsx.snap @@ -15,6 +15,44 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
+
+
+

+ Language +

+
+
+
+ Application language +
+
+
+
+ The app will reload after selecting another language +
+
+
+