diff --git a/linked-dependencies/matrix-react-sdk/src/components/views/settings/tabs/user/AccountUserSettingsTab.tsx b/linked-dependencies/matrix-react-sdk/src/components/views/settings/tabs/user/AccountUserSettingsTab.tsx index a928dbd05b..dd05698e98 100644 --- a/linked-dependencies/matrix-react-sdk/src/components/views/settings/tabs/user/AccountUserSettingsTab.tsx +++ b/linked-dependencies/matrix-react-sdk/src/components/views/settings/tabs/user/AccountUserSettingsTab.tsx @@ -26,6 +26,7 @@ import SettingsSubsection, { SettingsSubsectionText } from "../../shared/Setting import { SDKContext } from "../../../../../contexts/SDKContext"; import UserPersonalInfoSettings from "../../UserPersonalInfoSettings"; import { useMatrixClientContext } from "../../../../../contexts/MatrixClientContext"; +import TchapRedListSettings from "../../../../../../../../src/tchap/components/views/settings/tabs/user/TchapRedListSettings"; // :TCHAP: red-list-settings interface IProps { closeSettingsFn: () => void; @@ -194,6 +195,9 @@ const AccountUserSettingsTab: React.FC = ({ closeSettingsFn }) => { onPasswordChanged={onPasswordChanged} onPasswordChangeError={onPasswordChangeError} /> + {/* :TCHAP: red-list-settings */} + + {/* end :TCHAP: */} {accountManagementSection} diff --git a/modules/tchap-translations/tchap_translations.json b/modules/tchap-translations/tchap_translations.json index e8979673a1..f2778d11f4 100644 --- a/modules/tchap-translations/tchap_translations.json +++ b/modules/tchap-translations/tchap_translations.json @@ -881,5 +881,17 @@ "encryption|verification|verification_skip_warning": { "en": "Without verifying, you won't have access to all your messages and may appear as untrusted to others.", "fr": "Sans vérification, vous n'aurez pas accès à tous vos messages et votre compte n'apparaitra pas comme fiable pour les autres utilisateurs" + }, + "settings|general|redlist_description": { + "en": "The other users won't be able to discover my account during their search", + "fr": "Les autres utilisateurs ne pourront pas découvrir mon compte lors de leurs recherches" + }, + "settings|general|redlist_title": { + "en": "Register my account on the red list", + "fr": "Inscrire mon compte sur la liste rouge" + }, + "settings|general|redlist": { + "en": "Red List", + "fr": "Liste rouge" } } diff --git a/patches/subtree-modifications.json b/patches/subtree-modifications.json index c9c9488e23..5801e3fbba 100644 --- a/patches/subtree-modifications.json +++ b/patches/subtree-modifications.json @@ -94,5 +94,11 @@ "src/components/views/right_panel/RightPanelTabs.tsx", "src/components/views/settings/tabs/user/SecurityUserSettingsTab.tsx" ] + }, + "red-list-settings": { + "issue": "https://github.com/tchapgouv/tchap-web-v4/issues/1134", + "files": [ + "src/components/views/settings/tabs/user/AccountUserSettingsTab.tsx" + ] } } \ No newline at end of file diff --git a/src/tchap/components/views/settings/tabs/user/TchapRedListSettings.tsx b/src/tchap/components/views/settings/tabs/user/TchapRedListSettings.tsx new file mode 100644 index 0000000000..12373ec857 --- /dev/null +++ b/src/tchap/components/views/settings/tabs/user/TchapRedListSettings.tsx @@ -0,0 +1,56 @@ +import React, { useEffect, useState } from 'react'; +import LabelledToggleSwitch from 'matrix-react-sdk/src/components/views/elements/LabelledToggleSwitch'; +import SettingsSubsection from 'matrix-react-sdk/src/components/views/settings/shared/SettingsSubsection'; +import { _t } from "matrix-react-sdk/src/languageHandler"; +import TchapUtils from '../../../../../util/TchapUtils'; + +interface TchapRedListSettingsProps { +} + +/** + * A group of settings views to allow the user to set their profile information. + */ +const TchapRedListSettings: React.FC = () => { + + const [isOnRedList, setIsOnRedList] = useState(false); + const [loading, setLoading] = useState(false); + + useEffect(() => { + const fetchInitialRedListValue = async () => { + const initialValue = await TchapUtils.getUserRedListInfo(); + console.log("initialRedListValue", initialValue); + setIsOnRedList(initialValue); + } + fetchInitialRedListValue(); + }, []); + + const _onRedlistOptionChange = async (checked: boolean) => { + try { + if (!loading) { + setLoading(true) + await TchapUtils.setUserRedListInfo(checked); + setIsOnRedList(checked); + } + setLoading(false) + } catch (err) { + console.error("Error setting AccountData 'im.vector.hide_profile': " + err); + setIsOnRedList(!checked); // return to initial state because of error + setLoading(false) + } + } + + return + _onRedlistOptionChange(checked)} + label={_t('settings|general|redlist_title')} + disabled={loading} + /> +

+ ({_t("settings|general|redlist_description")}) +

+
+} + +export default TchapRedListSettings; \ No newline at end of file diff --git a/src/tchap/util/TchapUtils.ts b/src/tchap/util/TchapUtils.ts index daa6500bfe..f1c822c69c 100644 --- a/src/tchap/util/TchapUtils.ts +++ b/src/tchap/util/TchapUtils.ts @@ -282,4 +282,16 @@ export default class TchapUtils { } } + static async getUserRedListInfo() : Promise { + const client = MatrixClientPeg.safeGet(); + const accountData = await client.getAccountData('im.vector.hide_profile'); + console.log('accountData', accountData?.getContent()); + return !!accountData?.getContent().hide_profile; + } + + static async setUserRedListInfo(isOnRedList: boolean) : Promise { + const client = MatrixClientPeg.safeGet(); + client.setAccountData('im.vector.hide_profile', {hide_profile: isOnRedList}); + } + } diff --git a/test/unit-tests/tchap/components/views/settings/AccountUserSettingsTab-test.tsx b/test/unit-tests/tchap/components/views/settings/AccountUserSettingsTab-test.tsx new file mode 100644 index 0000000000..62c46737aa --- /dev/null +++ b/test/unit-tests/tchap/components/views/settings/AccountUserSettingsTab-test.tsx @@ -0,0 +1,95 @@ +import { render } from "@testing-library/react"; +import React from "react"; +import { MatrixClient } from "matrix-js-sdk/src/matrix"; +import { logger } from "matrix-js-sdk/src/logger"; +import { MockedObject } from "jest-mock"; +import AccountUserSettingsTab from "matrix-react-sdk/src/components/views/settings/tabs/user/AccountUserSettingsTab"; +import { SdkContextClass, SDKContext } from "matrix-react-sdk/src/contexts/SDKContext"; +import SettingsStore from "matrix-react-sdk/src/settings/SettingsStore"; +import { UIFeature } from "matrix-react-sdk/src/settings/UIFeature"; +import { OidcClientStore } from "matrix-react-sdk/src/stores/oidc/OidcClientStore"; +import MatrixClientContext from "matrix-react-sdk/src/contexts/MatrixClientContext"; +import { + getMockClientWithEventEmitter, + mockClientMethodsServer, + mockClientMethodsUser, + mockPlatformPeg, +} from "matrix-react-sdk/test/test-utils"; + + +jest.mock( + "matrix-react-sdk/src/components/views/settings/ChangePassword", + () => + ({ onError, onFinished }: { onError: (e: Error) => void; onFinished: () => void }) => { + return ; + }, +); + +describe("", () => { + const defaultProps = { + closeSettingsFn: jest.fn(), + }; + + const userId = "@alice:server.org"; + let mockClient: MockedObject; + + let stores: SdkContextClass; + + const getComponent = () => ( + + + + + + ); + + beforeEach(() => { + jest.spyOn(SettingsStore, "getValue").mockReturnValue(false); + mockPlatformPeg(); + jest.clearAllMocks(); + jest.spyOn(SettingsStore, "getValue").mockRestore(); + jest.spyOn(logger, "error").mockRestore(); + + mockClient = getMockClientWithEventEmitter({ + ...mockClientMethodsUser(userId), + ...mockClientMethodsServer(), + getCapabilities: jest.fn(), + getThreePids: jest.fn(), + getIdentityServerUrl: jest.fn(), + deleteThreePid: jest.fn(), + }); + + mockClient.getCapabilities.mockResolvedValue({}); + mockClient.getThreePids.mockResolvedValue({ + threepids: [], + }); + mockClient.deleteThreePid.mockResolvedValue({ + id_server_unbind_result: "success", + }); + + stores = new SdkContextClass(); + stores.client = mockClient; + // stub out this store completely to avoid mocking initialisation + const mockOidcClientStore = {} as unknown as OidcClientStore; + jest.spyOn(stores, "oidcClientStore", "get").mockReturnValue(mockOidcClientStore); + jest.spyOn(SettingsStore, "getValue").mockImplementation( + (settingName: string) => settingName === UIFeature.Deactivate, + ); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + + describe("common view snapshot", () => { + + it("should render section when account deactivation feature is enabled", () => { + const { container } = render(getComponent()); + + expect(container).toMatchSnapshot(); + }); + }); + + +}); diff --git a/test/unit-tests/tchap/components/views/settings/TchapRedListSettings-test.tsx b/test/unit-tests/tchap/components/views/settings/TchapRedListSettings-test.tsx new file mode 100644 index 0000000000..b6eabafb06 --- /dev/null +++ b/test/unit-tests/tchap/components/views/settings/TchapRedListSettings-test.tsx @@ -0,0 +1,109 @@ +import { act, logRoles, render, screen, waitFor } from '@testing-library/react'; +import React from 'react'; +import { flushPromises, stubClient } from 'matrix-react-sdk/test/test-utils'; +import { Mocked, mocked } from 'jest-mock'; + +import TchapRedListSettings from '~tchap-web/src/tchap/components/views/settings/tabs/user/TchapRedListSettings'; +import { MatrixClient } from 'matrix-js-sdk/src/matrix'; +import TchapUtils from '~tchap-web/src/tchap/util/TchapUtils'; +import userEvent from '@testing-library/user-event'; + +jest.mock("~tchap-web/src/tchap/util/TchapUtils"); + +describe("TchapRedListSettings", () => { + let client: Mocked; + + const renderComponent = () => render(); + + beforeEach(() => { + client = mocked(stubClient()); + }); + + + it("should red list should be activated when initial value is true", async () => { + mocked(TchapUtils.getUserRedListInfo).mockResolvedValue(true); + mocked(TchapUtils.setUserRedListInfo).mockResolvedValue(); + + renderComponent(); + + await flushPromises(); + + const switchElm = screen.getByRole("switch"); + + expect(switchElm).toHaveAttribute('aria-checked', "true"); + + }); + + it("should red list should be desactivated when initial value is false", async () => { + mocked(TchapUtils.getUserRedListInfo).mockResolvedValue(false); + mocked(TchapUtils.setUserRedListInfo).mockResolvedValue(); + + renderComponent(); + + await flushPromises(); + + const switchElm = screen.getByRole("switch"); + + expect(switchElm).toHaveAttribute('aria-checked', "false"); + }); + + + it("should remove user from red list", async () => { + // initial value + mocked(TchapUtils.getUserRedListInfo).mockResolvedValue(true); + mocked(TchapUtils.setUserRedListInfo).mockResolvedValue(); + + renderComponent(); + + await flushPromises(); + + const switchElm = screen.getByRole("switch"); + + act(() => { + userEvent.click(switchElm); + }) + + waitFor(() => { + expect(switchElm).toHaveAttribute('aria-checked', "false"); + }) + }); + + it("should put user on red list", async () => { + // initial value + mocked(TchapUtils.getUserRedListInfo).mockResolvedValue(false); + mocked(TchapUtils.setUserRedListInfo).mockResolvedValue(); + + renderComponent(); + + await flushPromises(); + + const switchElm = screen.getByRole("switch"); + + act(() => { + userEvent.click(switchElm); + }) + + waitFor(() => { + expect(switchElm).toHaveAttribute('aria-checked', "true"); + }) + }); + + it("should get back to initial value if throws an error", async () => { + // initial value + mocked(TchapUtils.getUserRedListInfo).mockResolvedValue(false); + mocked(TchapUtils.setUserRedListInfo).mockRejectedValue(new Error("error test")); + + renderComponent(); + + await flushPromises(); + + const switchElm = screen.getByRole("switch"); + + act(() => { + userEvent.click(switchElm); + }) + waitFor(() => { + expect(switchElm).toHaveAttribute('aria-checked', "false"); + }); + }); +}); diff --git a/test/unit-tests/tchap/components/views/settings/__snapshots__/AccountUserSettingsTab-test.tsx.snap b/test/unit-tests/tchap/components/views/settings/__snapshots__/AccountUserSettingsTab-test.tsx.snap new file mode 100644 index 0000000000..abc2b85199 --- /dev/null +++ b/test/unit-tests/tchap/components/views/settings/__snapshots__/AccountUserSettingsTab-test.tsx.snap @@ -0,0 +1,222 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` common view snapshot should render section when account deactivation feature is enabled 1`] = ` +
+
+
+
+
+
+

+ Profile +

+
+ Your account is managed separately by an identity provider and so some of your personal information can’t be changed here. +
+
+
+
+ + + +
+
+
+
+ +
+ +
+
+
+
+
+
+ Username +
+
+ @alice:server.org +
+
+
+
+
+
+ Sign out +
+
+
+
+
+

+ settings +

+
+
+
+ +
+ settings +
+
+
+
+
+
+

+ ( + settings + ) +

+
+
+
+
+
+

+ Deactivate Account +

+
+
+
+

+ Account management +

+
+
+
+ Deactivating your account is a permanent action — be careful! +
+
+
+
+ Deactivate Account +
+
+
+
+
+
+
+
+`;