diff --git a/cypress/e2e/export-room-members/export-room-members.spec.ts b/cypress/e2e/export-room-members/export-room-members.spec.ts new file mode 100644 index 0000000000..66a6c456b9 --- /dev/null +++ b/cypress/e2e/export-room-members/export-room-members.spec.ts @@ -0,0 +1,46 @@ +/// + +import RoomUtils from "../../utils/room-utils"; +import RandomUtils from "../../utils/random-utils"; +import { normalize } from "../../../yarn-linked-dependencies/matrix-js-sdk/src/utils"; + +describe("Export room members feature", () => { + const homeserverUrl = Cypress.env("E2E_TEST_USER_HOMESERVER_URL"); + const homeserverShort = Cypress.env("E2E_TEST_USER_HOMESERVER_SHORT"); + const email = Cypress.env("E2E_TEST_USER_EMAIL"); + const password = Cypress.env("E2E_TEST_USER_PASSWORD"); + const today = new Date().toISOString().slice(0, 10).replace(/-/g, ""); + // This will fail if email has special characters. + const userId = "@" + email.replace("@", "-") + ":" + homeserverShort; + + beforeEach(() => { + cy.loginUser(homeserverUrl, email, password); + }); + + afterEach(() => {}); + + it("should display the tooltip on button hover", () => { + const roomName = "test/" + today + "/export_room_members/" + RandomUtils.generateRandom(4); + RoomUtils.createPublicRoom(roomName).then((roomId) => { + RoomUtils.openPeopleMenu(roomName); + cy.get('[data-testid="tc_exportRoomMembersButton"]') + .trigger("mouseover") + .get(".tc_exportRoomMembersTooltip"); // tooltip should show + cy.leaveRoom(roomId); + }); + }); + + it("downloads the file when button is clicked", () => { + const roomName = "test/" + today + "/export_room_members/" + RandomUtils.generateRandom(4); + const normalizedRoomName = normalize(roomName); + RoomUtils.createPublicRoom(roomName).then((roomId) => { + RoomUtils.openPeopleMenu(roomName); + cy.get('[data-testid="tc_exportRoomMembersButton"]') + .click() + .then(() => { + cy.readFile("cypress/downloads/membres_de_" + normalizedRoomName + ".txt").should("eq", userId); + }); + cy.leaveRoom(roomId); + }); + }); +}); diff --git a/cypress/utils/room-utils.ts b/cypress/utils/room-utils.ts index 2a06de8c29..b362428e2c 100644 --- a/cypress/utils/room-utils.ts +++ b/cypress/utils/room-utils.ts @@ -1,21 +1,32 @@ import Chainable = Cypress.Chainable; import TchapCreateRoom from "../../src/tchap/lib/createTchapRoom"; import { TchapRoomType } from "../../src/tchap/@types/tchap"; + export default class RoomUtils { public static createPublicRoom(roomName: string): Chainable { - return cy.createRoom(TchapCreateRoom.roomCreateOptions(roomName, TchapRoomType.Forum).createOpts); + return cy.createRoom(TchapCreateRoom.roomCreateOptions(roomName, TchapRoomType.Forum, false).createOpts); } public static createPrivateRoom(roomName: string): Chainable { - return cy.createRoom(TchapCreateRoom.roomCreateOptions(roomName, TchapRoomType.Private).createOpts); + return cy.createRoom(TchapCreateRoom.roomCreateOptions(roomName, TchapRoomType.Private, false).createOpts); } public static createPrivateWithExternalRoom(roomName: string): Chainable { - return cy.createRoom(TchapCreateRoom.roomCreateOptions(roomName, TchapRoomType.External).createOpts); + return cy.createRoom(TchapCreateRoom.roomCreateOptions(roomName, TchapRoomType.External, false).createOpts); + } + public static openRoom(roomName: string): Chainable> { + return cy.get('[aria-label="' + roomName + '"]').click(); } public static openRoomAccessSettings(roomName: string): Chainable> { - //open room - cy.get('[aria-label="' + roomName + '"]').click(); + cy.get('[aria-label="' + roomName + '"]').click(); //open room cy.get(".mx_RoomHeader_chevron").click(); cy.get('[aria-label="Paramètres"] > .mx_IconizedContextMenu_label').click(); return cy.get('[data-testid="settings-tab-ROOM_SECURITY_TAB"] > .mx_TabbedView_tabLabel_text').click(); } + public static openRoomInformation(roomName: string): Chainable> { + cy.get('[aria-label="' + roomName + '"]').click(); //open room + return cy.get('[aria-label="Information du salon"]').click(); + } + public static openPeopleMenu(roomName: string): Chainable> { + this.openRoomInformation(roomName); + return cy.get(".mx_RoomSummaryCard_icon_people").click(); + } } diff --git a/patches/export-room-members/matrix-react-sdk+3.71.1.patch b/patches/export-room-members/matrix-react-sdk+3.71.1.patch new file mode 100644 index 0000000000..6bc58b675d --- /dev/null +++ b/patches/export-room-members/matrix-react-sdk+3.71.1.patch @@ -0,0 +1,25 @@ +diff --git a/node_modules/matrix-react-sdk/src/components/views/rooms/MemberList.tsx b/node_modules/matrix-react-sdk/src/components/views/rooms/MemberList.tsx +index 44dba2e..69ffbed 100644 +--- a/node_modules/matrix-react-sdk/src/components/views/rooms/MemberList.tsx ++++ b/node_modules/matrix-react-sdk/src/components/views/rooms/MemberList.tsx +@@ -46,6 +46,7 @@ import { shouldShowComponent } from "../../../customisations/helpers/UIComponent + import { UIComponent } from "../../../settings/UIFeature"; + import PosthogTrackers from "../../../PosthogTrackers"; + import { SDKContext } from "../../../contexts/SDKContext"; ++import TchapExportMembersButton from "../../../../../../src/tchap/components/views/rooms/TchapExportMembersButton"; // TCHAP + + const INITIAL_LOAD_NUM_MEMBERS = 30; + const INITIAL_LOAD_NUM_INVITED = 5; +@@ -409,6 +410,12 @@ export default class MemberList extends React.Component { + + {scopeHeader} + {inviteButton} ++ {/** TCHAP */} ++ roomMember.userId)}> ++ ++ {/** end TCHAP */} + + } + footer={footer} diff --git a/patches/patches.json b/patches/patches.json index 67bbe4ec1d..8db552e7f4 100644 --- a/patches/patches.json +++ b/patches/patches.json @@ -210,5 +210,12 @@ "src/components/structures/RoomSearchView.tsx", "src/components/structures/RoomStatusBar.tsx" ] + }, + "export-room-members": { + "github-issue": "https://github.com/tchapgouv/tchap-web-v4/issues/593", + "package": "matrix-react-sdk", + "files": [ + "src/components/views/rooms/MemberList.tsx" + ] } } \ No newline at end of file diff --git a/res/css/views/rooms/TchapExportMembersButton.pcss b/res/css/views/rooms/TchapExportMembersButton.pcss new file mode 100644 index 0000000000..ffb6af3921 --- /dev/null +++ b/res/css/views/rooms/TchapExportMembersButton.pcss @@ -0,0 +1,31 @@ +/** +* Copyright DINUM 2023 +*/ + +.mx_AccessibleButton.mx_AccessibleButton_hasKind.tc_MemberList_export { + /* Copy the style from mx_MemberList_invite button just above. For some reason it doesn't follow the standard style. */ + display: flex; + justify-content: center; + margin: 5px 9px 9px; + padding: 0px; + border-radius: 4px; + font-weight: 600; +} + +.tc_MemberList_export span { + padding: 8px 0; + display: inline-flex; + + &::before { + content: ""; + display: inline-block; + background-color: var(--accent); + mask-image: url("../../../img/tchap/user-export.svg"); + mask-position: center; + mask-repeat: no-repeat; + mask-size: 20px; + width: 20px; + height: 20px; + margin-right: 5px; + } +} diff --git a/res/img/tchap/user-export.svg b/res/img/tchap/user-export.svg new file mode 100644 index 0000000000..6063038739 --- /dev/null +++ b/res/img/tchap/user-export.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/tchap/components/views/rooms/TchapExportMembersButton.tsx b/src/tchap/components/views/rooms/TchapExportMembersButton.tsx new file mode 100644 index 0000000000..7319f3ecbd --- /dev/null +++ b/src/tchap/components/views/rooms/TchapExportMembersButton.tsx @@ -0,0 +1,62 @@ +/** + * Copyright DINUM 2023 + */ + +import React from "react"; + +import { FileDownloader } from "matrix-react-sdk/src/utils/FileDownloader"; +import AccessibleTooltipButton from "matrix-react-sdk/src/components/views/elements/AccessibleTooltipButton"; +import { ButtonEvent } from "matrix-react-sdk/src/components/views/elements/AccessibleButton"; +import { Room } from "matrix-js-sdk/src/models/room"; +import { _t } from "matrix-react-sdk/src/languageHandler"; + +import "../../../../../res/css/views/rooms/TchapExportMembersButton.pcss"; + +interface IProps { + room: Room; + roomMembersIds: Array; +} + +interface IState { +} + +export default class MemberList extends React.Component { + private downloader = new FileDownloader(); + + public constructor(props: IProps) { + super(props); + } + + private onExportButtonClick = (ev: ButtonEvent): void => { + const blob = new Blob([this.props.roomMembersIds.join()], { type : 'plain/text' }) + + const filename = _t('members_of_%(roomName)s.txt', { + roomName: this.props.room.normalizedName, + }); + + this.downloader.download({ + blob: blob, + name: filename, + }); + + return; + }; + + public render(): React.ReactNode { + if (this.props.room?.getMyMembership() === "join" && !this.props.room.isSpaceRoom() && this.props.roomMembersIds.length > 0) { + return ( + + {_t("Export room members")} + + ); + } + return null; + } + +} \ No newline at end of file diff --git a/src/tchap/i18n/strings/tchap_translations.json b/src/tchap/i18n/strings/tchap_translations.json index 381e8bde3b..9627a04cfc 100644 --- a/src/tchap/i18n/strings/tchap_translations.json +++ b/src/tchap/i18n/strings/tchap_translations.json @@ -935,6 +935,18 @@ "fr": "Veuillez ne continuer que si vous êtes certain d’avoir perdu tous vos autres appareils et votre Code de Récupération.", "en": "Please only proceed if you're sure you've lost all of your other devices and your Recovery Code." }, + "Export room members": { + "fr": "Exporter les membres du salon", + "en": "Export room members" + }, + "Download the list of all this room's members, in a text file. Useful for adding them all to another room.": { + "fr": "Récupérer la liste des membres de ce salon, dans un fichier texte. Utile pour inviter toutes ces personnes à un autre salon.", + "en": "Download the list of all this room's members, in a text file. Useful for adding them all to another room." + }, + "members_of_%(roomName)s.txt": { + "fr": "membres_de_%(roomName)s.txt", + "en": "members_of_%(roomName)s.txt" + }, "Destroy cross-signing keys?": { "fr": "Réinitialiser les clés de signature croisée ?", "en": "Reset cross-signing keys?"