Add profile modal

This commit is contained in:
Robert Long 2021-12-13 14:54:44 -08:00
parent e08a24ade7
commit 2e97c488e2
8 changed files with 203 additions and 44 deletions

View file

@ -104,7 +104,15 @@ export async function fetchGroupCall(
export function ClientProvider({ homeserverUrl, children }) { export function ClientProvider({ homeserverUrl, children }) {
const history = useHistory(); const history = useHistory();
const [ const [
{ loading, isAuthenticated, isPasswordlessUser, isGuest, client, userName }, {
loading,
isAuthenticated,
isPasswordlessUser,
isGuest,
client,
userName,
displayName,
},
setState, setState,
] = useState({ ] = useState({
loading: true, loading: true,
@ -113,6 +121,7 @@ export function ClientProvider({ homeserverUrl, children }) {
isGuest: false, isGuest: false,
client: undefined, client: undefined,
userName: null, userName: null,
displayName: null,
}); });
useEffect(() => { useEffect(() => {
@ -602,3 +611,69 @@ export function getRoomUrl(roomId) {
return `${window.location.host}/room/${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 };
}

View file

@ -15,15 +15,11 @@
justify-content: flex-end; justify-content: flex-end;
} }
.fieldRow > .field { .fieldRow > * {
margin: 0 5px; margin-right: 24px;
} }
.fieldRow > .field:first-child { .fieldRow > :last-child {
margin-left: 0;
}
.fieldRow > .field:last-child {
margin-right: 0; margin-right: 0;
} }

View file

@ -16,7 +16,7 @@
box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.15); box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.15);
border-radius: 8px; border-radius: 8px;
max-width: 90vw; max-width: 90vw;
min-width: 288px; width: 600px;
} }
.modalHeader { .modalHeader {

76
src/ProfileModal.jsx Normal file
View file

@ -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 (
<Modal title="Profile" isDismissable {...rest}>
<ModalContent>
<form onSubmit={onSubmit}>
<FieldRow>
<InputField
id="displayName"
name="displayName"
label="Display Name"
type="text"
required
autoComplete="off"
placeholder="Display Name"
value={displayName}
onChange={onChangeDisplayName}
/>
</FieldRow>
{error && (
<FieldRow>
<ErrorMessage>{error.message}</ErrorMessage>
</FieldRow>
)}
<FieldRow rightAlign>
<Button type="button" variant="secondary" onPress={onClose}>
Cancel
</Button>
<Button type="submit" disabled={loading}>
{loading ? "Saving..." : "Save"}
</Button>
</FieldRow>
</form>
</ModalContent>
</Modal>
);
}

View file

View file

@ -8,17 +8,22 @@ import styles from "./UserMenu.module.css";
import { Item } from "@react-stately/collections"; import { Item } from "@react-stately/collections";
import { Menu } from "./Menu"; import { Menu } from "./Menu";
import { useHistory, useLocation } from "react-router-dom"; 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() { export function UserMenu() {
const location = useLocation(); const location = useLocation();
const history = useHistory(); 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( const onAction = useCallback(
(value) => { (value) => {
switch (value) { switch (value) {
case "user": case "user":
modalState.open();
break; break;
case "logout": case "logout":
logout(); logout();
@ -31,7 +36,7 @@ export function UserMenu() {
break; break;
} }
}, },
[history, location, logout] [history, location, logout, modalState]
); );
const items = useMemo(() => { const items = useMemo(() => {
@ -41,7 +46,7 @@ export function UserMenu() {
arr.push({ arr.push({
key: "user", key: "user",
icon: UserIcon, icon: UserIcon,
label: userName, label: displayName || userName,
}); });
} }
@ -67,24 +72,27 @@ export function UserMenu() {
} }
return arr; return arr;
}, [isAuthenticated, isGuest, userName]); }, [isAuthenticated, isGuest, userName, displayName]);
return ( return (
<PopoverMenuTrigger placement="bottom right"> <>
<Button variant="icon" className={styles.userButton}> <PopoverMenuTrigger placement="bottom right">
<ButtonTooltip>Profile</ButtonTooltip> <Button variant="icon" className={styles.userButton}>
<UserIcon /> <ButtonTooltip>Profile</ButtonTooltip>
</Button> <UserIcon />
{(props) => ( </Button>
<Menu {...props} label="User menu" onAction={onAction}> {(props) => (
{items.map(({ key, icon: Icon, label }) => ( <Menu {...props} label="User menu" onAction={onAction}>
<Item key={key} textValue={label}> {items.map(({ key, icon: Icon, label }) => (
<Icon /> <Item key={key} textValue={label}>
<span>{label}</span> <Icon />
</Item> <span>{label}</span>
))} </Item>
</Menu> ))}
)} </Menu>
</PopoverMenuTrigger> )}
</PopoverMenuTrigger>
{modalState.isOpen && <ProfileModal client={client} {...modalProps} />}
</>
); );
} }

View file

@ -14,6 +14,7 @@ const variantToClassName = {
default: [styles.button], default: [styles.button],
toolbar: [styles.toolbarButton], toolbar: [styles.toolbarButton],
icon: [styles.iconButton], icon: [styles.iconButton],
secondary: [styles.secondary],
copy: [styles.copyButton], copy: [styles.copyButton],
iconCopy: [styles.iconCopyButton], iconCopy: [styles.iconCopyButton],
}; };

View file

@ -17,7 +17,9 @@ limitations under the License.
.button, .button,
.toolbarButton, .toolbarButton,
.iconButton, .iconButton,
.iconCopyButton { .iconCopyButton,
.secondary,
.copyButton {
position: relative; position: relative;
display: flex; display: flex;
justify-content: center; justify-content: center;
@ -28,15 +30,20 @@ limitations under the License.
cursor: pointer; cursor: pointer;
} }
.button { .secondary,
color: #fff; .button,
background-color: var(--primaryColor); .copyButton {
padding: 7px 15px; padding: 7px 15px;
border-radius: 8px; border-radius: 8px;
font-size: 14px; font-size: 14px;
font-weight: 700; font-weight: 700;
} }
.button {
color: #fff;
background-color: var(--primaryColor);
}
.toolbarButton { .toolbarButton {
width: 50px; width: 50px;
height: 50px; height: 50px;
@ -111,21 +118,17 @@ limitations under the License.
top: calc(100% + 6px); top: calc(100% + 6px);
} }
.secondary,
.copyButton { .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; color: #0dbd8b;
border: 2px solid #0dbd8b;
background-color: transparent;
}
.copyButton {
width: 100%; width: 100%;
height: 40px; height: 40px;
transition: border-color 250ms, background-color 250ms; transition: border-color 250ms, background-color 250ms;
padding: 0 20px;
flex-shrink: 0;
} }
.copyButton span { .copyButton span {