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?"