From 6b414824d32de413fee41221ada2e89b77fb9eec Mon Sep 17 00:00:00 2001 From: Marcel Date: Wed, 23 Aug 2023 15:49:01 +0200 Subject: [PATCH 01/83] add icon and rudimentary component for inviting users --- assets/icons/user-add.svg | 1 + components/UI/InviteUserToMatrixRoom.js | 14 ++++++++++++++ pages/etherpad/[[...roomId]].js | 2 ++ 3 files changed, 17 insertions(+) create mode 100644 assets/icons/user-add.svg create mode 100644 components/UI/InviteUserToMatrixRoom.js diff --git a/assets/icons/user-add.svg b/assets/icons/user-add.svg new file mode 100644 index 00000000..fcc3d414 --- /dev/null +++ b/assets/icons/user-add.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/components/UI/InviteUserToMatrixRoom.js b/components/UI/InviteUserToMatrixRoom.js new file mode 100644 index 00000000..2997cb8b --- /dev/null +++ b/components/UI/InviteUserToMatrixRoom.js @@ -0,0 +1,14 @@ +import { useTranslation } from 'react-i18next'; + +import UserAddIcon from '../../assets/icons/user-add.svg'; + +export default function InviteUserToMatrixRoom({ roomId }) { + const { t } = useTranslation(); + const handleClick = () => { + + }; + + return ; +} diff --git a/pages/etherpad/[[...roomId]].js b/pages/etherpad/[[...roomId]].js index a742aff0..003666bf 100644 --- a/pages/etherpad/[[...roomId]].js +++ b/pages/etherpad/[[...roomId]].js @@ -20,6 +20,7 @@ import CreateAnonymousPad from './actions/CreateAnonymousPad'; import AddExistingPad from './actions/AddExistingPad'; import CreateAuthoredPad from './actions/CreateAuthoredPad'; import CreatePasswordPad from './actions/CreatePasswordPad'; +import InviteUserToMatrixRoom from '../../components/UI/InviteUserToMatrixRoom'; export default function Etherpad() { const auth = useAuth(); @@ -234,6 +235,7 @@ export default function Etherpad() { + From d0403931bd8c2eff1084cf2533208f48bd402aad Mon Sep 17 00:00:00 2001 From: Marcel Date: Wed, 23 Aug 2023 17:29:58 +0200 Subject: [PATCH 02/83] add logic to search for matrix users --- components/UI/InviteUserToMatrixRoom.js | 114 +++++++++++++++++++++++- 1 file changed, 110 insertions(+), 4 deletions(-) diff --git a/components/UI/InviteUserToMatrixRoom.js b/components/UI/InviteUserToMatrixRoom.js index 2997cb8b..bd5eda7b 100644 --- a/components/UI/InviteUserToMatrixRoom.js +++ b/components/UI/InviteUserToMatrixRoom.js @@ -1,14 +1,120 @@ +import React, { useCallback, useState } from 'react'; import { useTranslation } from 'react-i18next'; +import { debounce } from 'lodash'; +import Modal from 'react-modal'; +import styled from 'styled-components'; +import TextButton from '../UI/TextButton'; import UserAddIcon from '../../assets/icons/user-add.svg'; +import Form from './Form'; +import { useAuth } from '../../lib/Auth'; +import LoadingSpinnerInline from './LoadingSpinnerInline'; +import CloseIcon from '../../assets/icons/close.svg'; + +const Header = styled.header` + display: grid; + grid-template-columns: 1fr auto; + margin-bottom: calc(var(--margin) * 2); + +`; + +const CloseButton = styled(TextButton)` + /* unset globally defined button styles; set height to line-height */ + width: unset; + height: calc(var(--margin) * 1.3); + padding: unset; + background-color: unset; + border: unset; +`; + +export default function InviteUserToMatrixRoom({ roomId, name }) { + const auth = useAuth(); + const matrixClient = auth.getAuthenticationProvider('matrix').getMatrixClient(); + const [isInviteDialogueOpen, setIsInviteDialogueOpen] = useState(false); + const [searchInput, setSearchInput] = useState(''); + const [searchResults, setSearchResults] = useState([]); + const [isInviting, setIsInviting] = useState(false); + const [isFetchingSearchResults, setIsFetchingSearchResults] = useState(false); + const customStyles = { + content: { + top: '50%', + right: 'auto', + bottom: 'auto', + left: '50%', + padding: 'calc(var(--margin) * 2)', + marginRight: '-50%', + transform: 'translate(-50%, -50%)', + }, + }; -export default function InviteUserToMatrixRoom({ roomId }) { const { t } = useTranslation(); + const handleClick = () => { + setIsInviteDialogueOpen(prevState => !prevState); + }; + + const onContributorInputValueChanged = (event) => { + setSearchInput(event.target.value); + debouncedFetchUsersForContributorSearch(event.target.value); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + const debouncedFetchUsersForContributorSearch = useCallback(debounce((val) => fetchUsersForContributorSearch(val), 300), []); + + const fetchUsersForContributorSearch = useCallback(async (a) => { + setIsFetchingSearchResults(true); + try { + const users = await matrixClient.searchUserDirectory({ term: a }); + // we only update the state if the returned array has entries, to be able to check if users a matrix users or not further down in the code (otherwise the array gets set to [] as soon as you selected an option from the datalist) + users.results.length > 0 && setSearchResults(users.results); + } catch (err) { + console.error('Error whhile trying to fetch users: ' + err); + } finally { + setIsFetchingSearchResults(false); + } + }, [matrixClient]); + + const handleInvite = (e) => { + alert('invited!'); }; - return ; + return <> + + { isInviteDialogueOpen && ( + setIsInviteDialogueOpen(false)} + contentLabel="Invite Users" + style={customStyles} + shouldCloseOnOverlayClick={true}> + +
+ { t('Invite user to') } { name } setIsInviteDialogueOpen(false)}> + + +
+
+ + + { searchResults.map((user, i) => { + return ; + }) } + + +
+
+ ) } + ; } From 43ba9b97b96cb3771adc41f8da9c499993fc36b6 Mon Sep 17 00:00:00 2001 From: Marcel Date: Wed, 23 Aug 2023 17:30:10 +0200 Subject: [PATCH 03/83] add invite user button --- pages/etherpad/[[...roomId]].js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/etherpad/[[...roomId]].js b/pages/etherpad/[[...roomId]].js index 003666bf..4ec5f876 100644 --- a/pages/etherpad/[[...roomId]].js +++ b/pages/etherpad/[[...roomId]].js @@ -235,7 +235,7 @@ export default function Etherpad() { - + From 58515897cefaebb1df00c883c177540d77bee085 Mon Sep 17 00:00:00 2001 From: Marcel Date: Wed, 23 Aug 2023 17:30:23 +0200 Subject: [PATCH 04/83] use react-modal package --- package-lock.json | 42 ++++++++++++++++++++++++++++++++++++++---- package.json | 1 + 2 files changed, 39 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6a44a7a6..6246c7f8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "react-i18next": "^12.2.2", + "react-modal": "^3.16.1", "styled-components": "^5.3.3" }, "devDependencies": { @@ -3054,6 +3055,11 @@ "node": ">=8" } }, + "node_modules/exenv": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/exenv/-/exenv-1.2.2.tgz", + "integrity": "sha512-Z+ktTxTwv9ILfgKCk32OX3n/doe+OcLTRtqK9pcL+JsP3J1/VW8Uvl4ZjLlKqeW4rzK4oesDOGMEMRIZqtP4Iw==" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -4680,7 +4686,6 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -5118,7 +5123,6 @@ "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "dev": true, "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", @@ -5128,8 +5132,7 @@ "node_modules/prop-types/node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "dev": true + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, "node_modules/punycode": { "version": "2.1.1", @@ -5234,6 +5237,29 @@ "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "peer": true }, + "node_modules/react-lifecycles-compat": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", + "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" + }, + "node_modules/react-modal": { + "version": "3.16.1", + "resolved": "https://registry.npmjs.org/react-modal/-/react-modal-3.16.1.tgz", + "integrity": "sha512-VStHgI3BVcGo7OXczvnJN7yT2TWHJPDXZWyI/a0ssFNhGZWsPmB8cF0z33ewDXq4VfYMO1vXgiv/g8Nj9NDyWg==", + "dependencies": { + "exenv": "^1.2.0", + "prop-types": "^15.7.2", + "react-lifecycles-compat": "^3.0.0", + "warning": "^4.0.3" + }, + "engines": { + "node": ">=8" + }, + "peerDependencies": { + "react": "^0.14.0 || ^15.0.0 || ^16 || ^17 || ^18", + "react-dom": "^0.14.0 || ^15.0.0 || ^16 || ^17 || ^18" + } + }, "node_modules/read-pkg": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", @@ -6701,6 +6727,14 @@ "node": ">=0.10.0" } }, + "node_modules/warning": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", + "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, "node_modules/watchpack": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", diff --git a/package.json b/package.json index b65f0e51..c8bebd0f 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "react-i18next": "^12.2.2", + "react-modal": "^3.16.1", "styled-components": "^5.3.3" }, "devDependencies": { From 5018104b5586f12a98c1f742a929348163d1a434 Mon Sep 17 00:00:00 2001 From: Marcel Date: Thu, 24 Aug 2023 13:23:26 +0200 Subject: [PATCH 05/83] add invitation logic and user feedback, close modal after sucessfull invitation. add `setAppElement` for screen readers. --- components/UI/InviteUserToMatrixRoom.js | 112 +++++++++++++++++++----- lib/Matrix.js | 17 ++++ 2 files changed, 105 insertions(+), 24 deletions(-) diff --git a/components/UI/InviteUserToMatrixRoom.js b/components/UI/InviteUserToMatrixRoom.js index bd5eda7b..5523c043 100644 --- a/components/UI/InviteUserToMatrixRoom.js +++ b/components/UI/InviteUserToMatrixRoom.js @@ -1,4 +1,14 @@ -import React, { useCallback, useState } from 'react'; +/** + * This component renders a button whoch onClick opens a Modal. + * `activeContexts` is the array of room IDs for the currently set context spaces. + * + * @param {string} roomId (valid matrix roomId) + * @param {string} name (name of the matrix room) + * + * @return {React.ReactElement} + */ + +import React, { useCallback, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { debounce } from 'lodash'; import Modal from 'react-modal'; @@ -10,6 +20,10 @@ import Form from './Form'; import { useAuth } from '../../lib/Auth'; import LoadingSpinnerInline from './LoadingSpinnerInline'; import CloseIcon from '../../assets/icons/close.svg'; +import { useMatrix } from '../../lib/Matrix'; +import ErrorMessage from './ErrorMessage'; + +Modal.setAppElement(document.body); const Header = styled.header` display: grid; @@ -29,18 +43,23 @@ const CloseButton = styled(TextButton)` export default function InviteUserToMatrixRoom({ roomId, name }) { const auth = useAuth(); + const matrix = useMatrix(auth.getAuthenticationProvider('matrix')); const matrixClient = auth.getAuthenticationProvider('matrix').getMatrixClient(); const [isInviteDialogueOpen, setIsInviteDialogueOpen] = useState(false); const [searchInput, setSearchInput] = useState(''); const [searchResults, setSearchResults] = useState([]); const [isInviting, setIsInviting] = useState(false); const [isFetchingSearchResults, setIsFetchingSearchResults] = useState(false); + const [userFeedback, setUserFeedback] = useState(''); + const [validUserObject, setValidUserObject] = useState(false); + const customStyles = { content: { top: '50%', right: 'auto', bottom: 'auto', left: '50%', + minWidth: '60%', padding: 'calc(var(--margin) * 2)', marginRight: '-50%', transform: 'translate(-50%, -50%)', @@ -53,7 +72,7 @@ export default function InviteUserToMatrixRoom({ roomId, name }) { setIsInviteDialogueOpen(prevState => !prevState); }; - const onContributorInputValueChanged = (event) => { + const handleChange = (event) => { setSearchInput(event.target.value); debouncedFetchUsersForContributorSearch(event.target.value); }; @@ -74,12 +93,54 @@ export default function InviteUserToMatrixRoom({ roomId, name }) { } }, [matrixClient]); - const handleInvite = (e) => { - alert('invited!'); + const handleInvite = async (e) => { + setIsInviting(true); + e?.preventDefault(); + + function clearInputs() { + setUserFeedback(''); + setSearchInput(''); + setIsInviting(false); + } + await matrix.inviteUserToMatrixRoom(roomId, validUserObject.userId) + .catch(async err => { + setUserFeedback({ err.data?.error }); + await new Promise(() => setTimeout(() => { + clearInputs(); + }, 3000)); + + return; + }); + + setUserFeedback('✓ ' + validUserObject.displayName + t(' was invited and needs to accept your invitation')); + await new Promise(() => setTimeout(() => { + clearInputs(); + }, 3000)); }; + useEffect(() => { + let cancelled = false; + if (cancelled || searchInput === '') return; + verifyUser(searchInput); + + return () => { + cancelled = true; + }; + }, [searchInput, verifyUser]); + + // eslint-disable-next-line react-hooks/exhaustive-deps + const verifyUser = useCallback( + debounce(user => { + const id = user.substring(user.lastIndexOf(' ') + 1); + const getUser = matrixClient.getUser(id); + if (getUser) setValidUserObject(getUser); + else setValidUserObject(false); + }, 200), + [], + ); + return <> - { isInviteDialogueOpen && ( @@ -95,26 +156,29 @@ export default function InviteUserToMatrixRoom({ roomId, name }) { -
- - - { searchResults.map((user, i) => { - return ; - }) } - - -
+ { userFeedback ?
{ userFeedback }
: +
+ + + { searchResults.map((user, i) => { + return ; + }) } + + +
+ } ) } ; } + diff --git a/lib/Matrix.js b/lib/Matrix.js index a859bd11..f9839ce5 100644 --- a/lib/Matrix.js +++ b/lib/Matrix.js @@ -302,6 +302,22 @@ function useMatrixProvider(activeMatrixAuthenticationProviders) { }); }; + /** + * This function is used to invite a userId to a matrix roomId + * + * @param {string} roomId + * @param {string} userId + * @returns {Promise} + * + * @TODO: + * add ability to define power level of invited user. + */ + const inviteUserToMatrixRoom = async (roomId, userId) => { + // If for some reason this function was called without a valid roomId we just cancel right away + if (!roomId) return; + await matrixClient.invite(roomId, userId); + }; + return { rooms, spaces, @@ -314,6 +330,7 @@ function useMatrixProvider(activeMatrixAuthenticationProviders) { leaveRoom, createRoom, hydrateRoomContent, + inviteUserToMatrixRoom, }; })(authenticationProvider)); } From 81f677aec57cde9b5d77f62bbc59f26b03a93dbe Mon Sep 17 00:00:00 2001 From: Marcel Date: Thu, 24 Aug 2023 13:41:07 +0200 Subject: [PATCH 06/83] add comments and TODO --- components/UI/InviteUserToMatrixRoom.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/components/UI/InviteUserToMatrixRoom.js b/components/UI/InviteUserToMatrixRoom.js index 5523c043..4c7992e2 100644 --- a/components/UI/InviteUserToMatrixRoom.js +++ b/components/UI/InviteUserToMatrixRoom.js @@ -6,6 +6,12 @@ * @param {string} name (name of the matrix room) * * @return {React.ReactElement} + * + * @TODO + * - create seperate component for the invitation dialogue so it can be used without the button and maybe wthout the modal view. + * - maybe swap datalist for a different UI element. datalist handleing is far from optimal, since we have to manually get the userId and displayName after a user has selected the user to invite. + * Even though we already have it from the `matrixClient.searchUserDirectory` call. The problem is that afaik there is no way to parse the object from the