Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
81a763f17f | ||
|
|
1ab7d27ba9 |
@@ -56,4 +56,5 @@
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
border-radius: 90px;
|
||||
font-size: 48px;
|
||||
}
|
||||
|
||||
@@ -182,12 +182,21 @@ export function ClientProvider({ children }) {
|
||||
}, [history]);
|
||||
|
||||
useEffect(() => {
|
||||
if ("BroadcastChannel" in window) {
|
||||
if (client) {
|
||||
const loadTime = Date.now();
|
||||
const broadcastChannel = new BroadcastChannel("matrix-video-chat");
|
||||
|
||||
function onMessage({ data }) {
|
||||
if (data.load !== undefined && data.load > loadTime) {
|
||||
const onToDeviceEvent = (event) => {
|
||||
if (event.getType() !== "org.matrix.call_duplicate_session") {
|
||||
return;
|
||||
}
|
||||
|
||||
const content = event.getContent();
|
||||
|
||||
if (content.session_id === client.getSessionId()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (content.timestamp > loadTime) {
|
||||
if (client) {
|
||||
client.stopClient();
|
||||
}
|
||||
@@ -199,13 +208,18 @@ export function ClientProvider({ children }) {
|
||||
),
|
||||
}));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
broadcastChannel.addEventListener("message", onMessage);
|
||||
broadcastChannel.postMessage({ load: loadTime });
|
||||
client.on("toDeviceEvent", onToDeviceEvent);
|
||||
|
||||
client.sendToDevice("org.matrix.call_duplicate_session", {
|
||||
[client.getUserId()]: {
|
||||
"*": { session_id: client.getSessionId(), timestamp: loadTime },
|
||||
},
|
||||
});
|
||||
|
||||
return () => {
|
||||
broadcastChannel.removeEventListener("message", onMessage);
|
||||
client.removeListener("toDeviceEvent", onToDeviceEvent);
|
||||
};
|
||||
}
|
||||
}, [client]);
|
||||
|
||||
@@ -43,14 +43,7 @@ export function UserMenuContainer({ preventNavigation }) {
|
||||
displayName || (userName ? userName.replace("@", "") : undefined)
|
||||
}
|
||||
/>
|
||||
{modalState.isOpen && (
|
||||
<ProfileModal
|
||||
client={client}
|
||||
isAuthenticated={isAuthenticated}
|
||||
isPasswordlessUser={isPasswordlessUser}
|
||||
{...modalProps}
|
||||
/>
|
||||
)}
|
||||
{modalState.isOpen && <ProfileModal client={client} {...modalProps} />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
4
src/icons/Edit.svg
Normal file
4
src/icons/Edit.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M2.64856 7.35501C2.65473 7.31601 2.67231 7.27972 2.69908 7.25069L8.40377 1.06442C8.47865 0.983217 8.60518 0.978093 8.68638 1.05297L9.8626 2.13763C9.9438 2.21251 9.94893 2.33904 9.87405 2.42024L4.16936 8.60651C4.1426 8.63554 4.10783 8.656 4.06946 8.6653L2.66781 9.00511C2.52911 9.03873 2.40084 8.92044 2.42315 8.77948L2.64856 7.35501Z" fill="white"/>
|
||||
<path d="M1.75 9.44346C1.33579 9.44346 1 9.77925 1 10.1935C1 10.6077 1.33579 10.9435 1.75 10.9435L10.75 10.9435C11.1642 10.9435 11.5 10.6077 11.5 10.1935C11.5 9.77925 11.1642 9.44346 10.75 9.44346L1.75 9.44346Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 689 B |
78
src/input/AvatarInputField.jsx
Normal file
78
src/input/AvatarInputField.jsx
Normal file
@@ -0,0 +1,78 @@
|
||||
import { useObjectRef } from "@react-aria/utils";
|
||||
import React, { useEffect } from "react";
|
||||
import { useCallback } from "react";
|
||||
import { useState } from "react";
|
||||
import { forwardRef } from "react";
|
||||
import { Avatar } from "../Avatar";
|
||||
import { Button } from "../button";
|
||||
import classNames from "classnames";
|
||||
import { ReactComponent as EditIcon } from "../icons/Edit.svg";
|
||||
import styles from "./AvatarInputField.module.css";
|
||||
|
||||
export const AvatarInputField = forwardRef(
|
||||
(
|
||||
{ id, label, className, avatarUrl, displayName, onRemoveAvatar, ...rest },
|
||||
ref
|
||||
) => {
|
||||
const [removed, setRemoved] = useState(false);
|
||||
const [objUrl, setObjUrl] = useState(null);
|
||||
|
||||
const fileInputRef = useObjectRef(ref);
|
||||
|
||||
useEffect(() => {
|
||||
const onChange = (e) => {
|
||||
if (e.target.files.length > 0) {
|
||||
setObjUrl(URL.createObjectURL(e.target.files[0]));
|
||||
setRemoved(false);
|
||||
} else {
|
||||
setObjUrl(null);
|
||||
}
|
||||
};
|
||||
|
||||
fileInputRef.current.addEventListener("change", onChange);
|
||||
|
||||
return () => {
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.removeEventListener("change", onChange);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
const onPressRemoveAvatar = useCallback(() => {
|
||||
setRemoved(true);
|
||||
onRemoveAvatar();
|
||||
}, [onRemoveAvatar]);
|
||||
|
||||
return (
|
||||
<div className={classNames(styles.avatarInputField, className)}>
|
||||
<div className={styles.avatarContainer}>
|
||||
<Avatar
|
||||
size="xl"
|
||||
src={removed ? null : objUrl || avatarUrl}
|
||||
fallback={displayName.slice(0, 1).toUpperCase()}
|
||||
/>
|
||||
<input
|
||||
id={id}
|
||||
accept="image/png, image/jpeg"
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
className={styles.fileInput}
|
||||
role="button"
|
||||
aria-label={label}
|
||||
{...rest}
|
||||
/>
|
||||
<label htmlFor={id} className={styles.fileInputButton}>
|
||||
<EditIcon />
|
||||
</label>
|
||||
</div>
|
||||
<Button
|
||||
className={styles.removeButton}
|
||||
variant="icon"
|
||||
onPress={onPressRemoveAvatar}
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
41
src/input/AvatarInputField.module.css
Normal file
41
src/input/AvatarInputField.module.css
Normal file
@@ -0,0 +1,41 @@
|
||||
.avatarInputField {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.avatarContainer {
|
||||
position: relative;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.fileInput {
|
||||
width: 0.1px;
|
||||
height: 0.1px;
|
||||
opacity: 0;
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.fileInput:focus + .fileInputButton {
|
||||
outline: auto;
|
||||
}
|
||||
|
||||
.fileInputButton {
|
||||
position: absolute;
|
||||
bottom: 11px;
|
||||
right: -4px;
|
||||
background-color: var(--bgColor4);
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.removeButton {
|
||||
color: #0dbd8b;
|
||||
}
|
||||
@@ -3,22 +3,25 @@ import { Button } from "../button";
|
||||
import { useProfile } from "./useProfile";
|
||||
import { FieldRow, InputField, ErrorMessage } from "../input/Input";
|
||||
import { Modal, ModalContent } from "../Modal";
|
||||
import { AvatarInputField } from "../input/AvatarInputField";
|
||||
import styles from "./ProfileModal.module.css";
|
||||
|
||||
export function ProfileModal({
|
||||
client,
|
||||
isAuthenticated,
|
||||
isPasswordlessUser,
|
||||
...rest
|
||||
}) {
|
||||
export function ProfileModal({ client, ...rest }) {
|
||||
const { onClose } = rest;
|
||||
const {
|
||||
success,
|
||||
error,
|
||||
loading,
|
||||
displayName: initialDisplayName,
|
||||
avatarUrl,
|
||||
saveProfile,
|
||||
} = useProfile(client);
|
||||
const [displayName, setDisplayName] = useState(initialDisplayName || "");
|
||||
const [removeAvatar, setRemoveAvatar] = useState(false);
|
||||
|
||||
const onRemoveAvatar = useCallback(() => {
|
||||
setRemoveAvatar(true);
|
||||
}, []);
|
||||
|
||||
const onChangeDisplayName = useCallback(
|
||||
(e) => {
|
||||
@@ -37,9 +40,10 @@ export function ProfileModal({
|
||||
saveProfile({
|
||||
displayName,
|
||||
avatar: avatar && avatar.size > 0 ? avatar : undefined,
|
||||
removeAvatar: removeAvatar && (!avatar || avatar.size === 0),
|
||||
});
|
||||
},
|
||||
[saveProfile]
|
||||
[saveProfile, removeAvatar]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -52,6 +56,16 @@ export function ProfileModal({
|
||||
<Modal title="Profile" isDismissable {...rest}>
|
||||
<ModalContent>
|
||||
<form onSubmit={onSubmit}>
|
||||
<FieldRow className={styles.avatarFieldRow}>
|
||||
<AvatarInputField
|
||||
id="avatar"
|
||||
name="avatar"
|
||||
label="Avatar"
|
||||
avatarUrl={avatarUrl}
|
||||
displayName={displayName}
|
||||
onRemoveAvatar={onRemoveAvatar}
|
||||
/>
|
||||
</FieldRow>
|
||||
<FieldRow>
|
||||
<InputField
|
||||
id="userId"
|
||||
@@ -75,16 +89,6 @@ export function ProfileModal({
|
||||
onChange={onChangeDisplayName}
|
||||
/>
|
||||
</FieldRow>
|
||||
{isAuthenticated && (
|
||||
<FieldRow>
|
||||
<InputField
|
||||
type="file"
|
||||
id="avatar"
|
||||
name="avatar"
|
||||
label="Avatar"
|
||||
/>
|
||||
</FieldRow>
|
||||
)}
|
||||
{error && (
|
||||
<FieldRow>
|
||||
<ErrorMessage>{error.message}</ErrorMessage>
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
.avatarFieldRow {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ export function useProfile(client) {
|
||||
}, [client]);
|
||||
|
||||
const saveProfile = useCallback(
|
||||
async ({ displayName, avatar }) => {
|
||||
async ({ displayName, avatar, removeAvatar }) => {
|
||||
if (client) {
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
@@ -58,7 +58,9 @@ export function useProfile(client) {
|
||||
|
||||
let mxcAvatarUrl;
|
||||
|
||||
if (avatar) {
|
||||
if (removeAvatar) {
|
||||
await client.setAvatarUrl("");
|
||||
} else if (avatar) {
|
||||
mxcAvatarUrl = await client.uploadContent(avatar);
|
||||
await client.setAvatarUrl(mxcAvatarUrl);
|
||||
}
|
||||
@@ -66,7 +68,9 @@ export function useProfile(client) {
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
displayName,
|
||||
avatarUrl: mxcAvatarUrl
|
||||
avatarUrl: removeAvatar
|
||||
? null
|
||||
: mxcAvatarUrl
|
||||
? getAvatarUrl(client, mxcAvatarUrl)
|
||||
: prev.avatarUrl,
|
||||
loading: false,
|
||||
|
||||
@@ -10,7 +10,6 @@ import { OverflowMenu } from "./OverflowMenu";
|
||||
import { UserMenuContainer } from "../UserMenuContainer";
|
||||
import { Body, Link } from "../typography/Typography";
|
||||
import { Avatar } from "../Avatar";
|
||||
import { getAvatarUrl } from "../matrix-utils";
|
||||
import { useProfile } from "../profile/useProfile";
|
||||
import useMeasure from "react-use-measure";
|
||||
import { ResizeObserver } from "@juggle/resize-observer";
|
||||
@@ -86,7 +85,7 @@ export function LobbyView({
|
||||
borderRadius: avatarSize,
|
||||
fontSize: Math.round(avatarSize / 2),
|
||||
}}
|
||||
src={avatarUrl && getAvatarUrl(client, avatarUrl, 96)}
|
||||
src={avatarUrl}
|
||||
fallback={displayName.slice(0, 1).toUpperCase()}
|
||||
/>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user