From 2e97c488e2892af760115ed187d3ee4ce50da699 Mon Sep 17 00:00:00 2001 From: Robert Long Date: Mon, 13 Dec 2021 14:54:44 -0800 Subject: [PATCH] Add profile modal --- src/ConferenceCallManagerHooks.jsx | 77 +++++++++++++++++++++++++++++- src/Input.module.css | 10 ++-- src/Modal.module.css | 2 +- src/ProfileModal.jsx | 76 +++++++++++++++++++++++++++++ src/ProfileModal.module.css | 0 src/UserMenu.jsx | 50 +++++++++++-------- src/button/Button.jsx | 1 + src/button/Button.module.css | 31 ++++++------ 8 files changed, 203 insertions(+), 44 deletions(-) create mode 100644 src/ProfileModal.jsx create mode 100644 src/ProfileModal.module.css diff --git a/src/ConferenceCallManagerHooks.jsx b/src/ConferenceCallManagerHooks.jsx index 5e841e7d..d506780f 100644 --- a/src/ConferenceCallManagerHooks.jsx +++ b/src/ConferenceCallManagerHooks.jsx @@ -104,7 +104,15 @@ export async function fetchGroupCall( export function ClientProvider({ homeserverUrl, children }) { const history = useHistory(); const [ - { loading, isAuthenticated, isPasswordlessUser, isGuest, client, userName }, + { + loading, + isAuthenticated, + isPasswordlessUser, + isGuest, + client, + userName, + displayName, + }, setState, ] = useState({ loading: true, @@ -113,6 +121,7 @@ export function ClientProvider({ homeserverUrl, children }) { isGuest: false, client: undefined, userName: null, + displayName: null, }); useEffect(() => { @@ -602,3 +611,69 @@ export function getRoomUrl(roomId) { return `${window.location.host}/room/${roomId}`; } } + +export function useDisplayName(client) { + const [{ loading, displayName, error, success }, setState] = useState(() => ({ + success: false, + loading: false, + displayName: client?.getUser(client.getUserId())?.displayName, + error: null, + })); + + useEffect(() => { + const onChangeDisplayName = (_event, { displayName }) => { + setState({ success: false, loading: false, displayName, error: null }); + }; + + let user; + + if (client) { + const userId = client.getUserId(); + user = client.getUser(userId); + user.on("User.displayName", onChangeDisplayName); + } + + return () => { + if (user) { + user.removeListener("User.displayName", onChangeDisplayName); + } + }; + }, [client]); + + const setDisplayName = useCallback( + (displayName) => { + if (client) { + setState((prev) => ({ + ...prev, + loading: true, + error: null, + success: false, + })); + + client + .setDisplayName(displayName) + .then(() => { + setState((prev) => ({ + ...prev, + displayName, + loading: false, + success: true, + })); + }) + .catch((error) => { + setState((prev) => ({ + ...prev, + loading: false, + error, + success: false, + })); + }); + } else { + console.error("Client not initialized before calling setDisplayName"); + } + }, + [client] + ); + + return { loading, error, displayName, setDisplayName, success }; +} diff --git a/src/Input.module.css b/src/Input.module.css index 234cf275..1951ebc9 100644 --- a/src/Input.module.css +++ b/src/Input.module.css @@ -15,15 +15,11 @@ justify-content: flex-end; } -.fieldRow > .field { - margin: 0 5px; +.fieldRow > * { + margin-right: 24px; } -.fieldRow > .field:first-child { - margin-left: 0; -} - -.fieldRow > .field:last-child { +.fieldRow > :last-child { margin-right: 0; } diff --git a/src/Modal.module.css b/src/Modal.module.css index c9270eb8..f83db900 100644 --- a/src/Modal.module.css +++ b/src/Modal.module.css @@ -16,7 +16,7 @@ box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.15); border-radius: 8px; max-width: 90vw; - min-width: 288px; + width: 600px; } .modalHeader { diff --git a/src/ProfileModal.jsx b/src/ProfileModal.jsx new file mode 100644 index 00000000..15176725 --- /dev/null +++ b/src/ProfileModal.jsx @@ -0,0 +1,76 @@ +import React, { useCallback, useEffect, useState } from "react"; +import { Button } from "./button"; +import { useDisplayName } from "./ConferenceCallManagerHooks"; +import { FieldRow, InputField, ErrorMessage } from "./Input"; +import { Modal, ModalContent } from "./Modal"; + +export function ProfileModal({ client, ...rest }) { + const { onClose } = rest; + const { + success, + error, + loading, + displayName: initialDisplayName, + setDisplayName: submitDisplayName, + } = useDisplayName(client); + const [displayName, setDisplayName] = useState(initialDisplayName || ""); + + const onChangeDisplayName = useCallback( + (e) => { + setDisplayName(e.target.value); + }, + [setDisplayName] + ); + + const onSubmit = useCallback( + (e) => { + e.preventDefault(); + const data = new FormData(e.target); + const displayName = data.get("displayName"); + console.log(displayName); + submitDisplayName(displayName); + }, + [setDisplayName] + ); + + useEffect(() => { + if (success) { + onClose(); + } + }, [success, onClose]); + + return ( + + +
+ + + + {error && ( + + {error.message} + + )} + + + + +
+
+
+ ); +} diff --git a/src/ProfileModal.module.css b/src/ProfileModal.module.css new file mode 100644 index 00000000..e69de29b diff --git a/src/UserMenu.jsx b/src/UserMenu.jsx index 8fd6f03d..b7d15883 100644 --- a/src/UserMenu.jsx +++ b/src/UserMenu.jsx @@ -8,17 +8,22 @@ import styles from "./UserMenu.module.css"; import { Item } from "@react-stately/collections"; import { Menu } from "./Menu"; import { useHistory, useLocation } from "react-router-dom"; -import { useClient } from "./ConferenceCallManagerHooks"; +import { useClient, useDisplayName } from "./ConferenceCallManagerHooks"; +import { useModalTriggerState } from "./Modal"; +import { ProfileModal } from "./ProfileModal"; export function UserMenu() { const location = useLocation(); const history = useHistory(); - const { isAuthenticated, isGuest, logout, userName } = useClient(); + const { isAuthenticated, isGuest, logout, userName, client } = useClient(); + const { displayName } = useDisplayName(client); + const { modalState, modalProps } = useModalTriggerState(); const onAction = useCallback( (value) => { switch (value) { case "user": + modalState.open(); break; case "logout": logout(); @@ -31,7 +36,7 @@ export function UserMenu() { break; } }, - [history, location, logout] + [history, location, logout, modalState] ); const items = useMemo(() => { @@ -41,7 +46,7 @@ export function UserMenu() { arr.push({ key: "user", icon: UserIcon, - label: userName, + label: displayName || userName, }); } @@ -67,24 +72,27 @@ export function UserMenu() { } return arr; - }, [isAuthenticated, isGuest, userName]); + }, [isAuthenticated, isGuest, userName, displayName]); return ( - - - {(props) => ( - - {items.map(({ key, icon: Icon, label }) => ( - - - {label} - - ))} - - )} - + <> + + + {(props) => ( + + {items.map(({ key, icon: Icon, label }) => ( + + + {label} + + ))} + + )} + + {modalState.isOpen && } + ); } diff --git a/src/button/Button.jsx b/src/button/Button.jsx index fcb3c90e..fe77014b 100644 --- a/src/button/Button.jsx +++ b/src/button/Button.jsx @@ -14,6 +14,7 @@ const variantToClassName = { default: [styles.button], toolbar: [styles.toolbarButton], icon: [styles.iconButton], + secondary: [styles.secondary], copy: [styles.copyButton], iconCopy: [styles.iconCopyButton], }; diff --git a/src/button/Button.module.css b/src/button/Button.module.css index 4da1be35..1d687fb7 100644 --- a/src/button/Button.module.css +++ b/src/button/Button.module.css @@ -17,7 +17,9 @@ limitations under the License. .button, .toolbarButton, .iconButton, -.iconCopyButton { +.iconCopyButton, +.secondary, +.copyButton { position: relative; display: flex; justify-content: center; @@ -28,15 +30,20 @@ limitations under the License. cursor: pointer; } -.button { - color: #fff; - background-color: var(--primaryColor); +.secondary, +.button, +.copyButton { padding: 7px 15px; border-radius: 8px; font-size: 14px; font-weight: 700; } +.button { + color: #fff; + background-color: var(--primaryColor); +} + .toolbarButton { width: 50px; height: 50px; @@ -111,21 +118,17 @@ limitations under the License. top: calc(100% + 6px); } +.secondary, .copyButton { - position: relative; - display: flex; - justify-content: center; - align-items: center; - background-color: transparent; - cursor: pointer; - border: 2px solid #0dbd8b; - border-radius: 8px; color: #0dbd8b; + border: 2px solid #0dbd8b; + background-color: transparent; +} + +.copyButton { width: 100%; height: 40px; transition: border-color 250ms, background-color 250ms; - padding: 0 20px; - flex-shrink: 0; } .copyButton span {