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 }) {
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 };
}

View file

@ -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;
}

View file

@ -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 {

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 { 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 (
<PopoverMenuTrigger placement="bottom right">
<Button variant="icon" className={styles.userButton}>
<ButtonTooltip>Profile</ButtonTooltip>
<UserIcon />
</Button>
{(props) => (
<Menu {...props} label="User menu" onAction={onAction}>
{items.map(({ key, icon: Icon, label }) => (
<Item key={key} textValue={label}>
<Icon />
<span>{label}</span>
</Item>
))}
</Menu>
)}
</PopoverMenuTrigger>
<>
<PopoverMenuTrigger placement="bottom right">
<Button variant="icon" className={styles.userButton}>
<ButtonTooltip>Profile</ButtonTooltip>
<UserIcon />
</Button>
{(props) => (
<Menu {...props} label="User menu" onAction={onAction}>
{items.map(({ key, icon: Icon, label }) => (
<Item key={key} textValue={label}>
<Icon />
<span>{label}</span>
</Item>
))}
</Menu>
)}
</PopoverMenuTrigger>
{modalState.isOpen && <ProfileModal client={client} {...modalProps} />}
</>
);
}

View file

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

View file

@ -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 {