Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
81a763f17f | ||
|
|
1ab7d27ba9 |
@@ -56,4 +56,5 @@
|
|||||||
width: 90px;
|
width: 90px;
|
||||||
height: 90px;
|
height: 90px;
|
||||||
border-radius: 90px;
|
border-radius: 90px;
|
||||||
|
font-size: 48px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -182,12 +182,21 @@ export function ClientProvider({ children }) {
|
|||||||
}, [history]);
|
}, [history]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if ("BroadcastChannel" in window) {
|
if (client) {
|
||||||
const loadTime = Date.now();
|
const loadTime = Date.now();
|
||||||
const broadcastChannel = new BroadcastChannel("matrix-video-chat");
|
|
||||||
|
|
||||||
function onMessage({ data }) {
|
const onToDeviceEvent = (event) => {
|
||||||
if (data.load !== undefined && data.load > loadTime) {
|
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) {
|
if (client) {
|
||||||
client.stopClient();
|
client.stopClient();
|
||||||
}
|
}
|
||||||
@@ -199,13 +208,18 @@ export function ClientProvider({ children }) {
|
|||||||
),
|
),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
broadcastChannel.addEventListener("message", onMessage);
|
client.on("toDeviceEvent", onToDeviceEvent);
|
||||||
broadcastChannel.postMessage({ load: loadTime });
|
|
||||||
|
client.sendToDevice("org.matrix.call_duplicate_session", {
|
||||||
|
[client.getUserId()]: {
|
||||||
|
"*": { session_id: client.getSessionId(), timestamp: loadTime },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
broadcastChannel.removeEventListener("message", onMessage);
|
client.removeListener("toDeviceEvent", onToDeviceEvent);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}, [client]);
|
}, [client]);
|
||||||
|
|||||||
@@ -43,14 +43,7 @@ export function UserMenuContainer({ preventNavigation }) {
|
|||||||
displayName || (userName ? userName.replace("@", "") : undefined)
|
displayName || (userName ? userName.replace("@", "") : undefined)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
{modalState.isOpen && (
|
{modalState.isOpen && <ProfileModal client={client} {...modalProps} />}
|
||||||
<ProfileModal
|
|
||||||
client={client}
|
|
||||||
isAuthenticated={isAuthenticated}
|
|
||||||
isPasswordlessUser={isPasswordlessUser}
|
|
||||||
{...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 { useProfile } from "./useProfile";
|
||||||
import { FieldRow, InputField, ErrorMessage } from "../input/Input";
|
import { FieldRow, InputField, ErrorMessage } from "../input/Input";
|
||||||
import { Modal, ModalContent } from "../Modal";
|
import { Modal, ModalContent } from "../Modal";
|
||||||
|
import { AvatarInputField } from "../input/AvatarInputField";
|
||||||
|
import styles from "./ProfileModal.module.css";
|
||||||
|
|
||||||
export function ProfileModal({
|
export function ProfileModal({ client, ...rest }) {
|
||||||
client,
|
|
||||||
isAuthenticated,
|
|
||||||
isPasswordlessUser,
|
|
||||||
...rest
|
|
||||||
}) {
|
|
||||||
const { onClose } = rest;
|
const { onClose } = rest;
|
||||||
const {
|
const {
|
||||||
success,
|
success,
|
||||||
error,
|
error,
|
||||||
loading,
|
loading,
|
||||||
displayName: initialDisplayName,
|
displayName: initialDisplayName,
|
||||||
|
avatarUrl,
|
||||||
saveProfile,
|
saveProfile,
|
||||||
} = useProfile(client);
|
} = useProfile(client);
|
||||||
const [displayName, setDisplayName] = useState(initialDisplayName || "");
|
const [displayName, setDisplayName] = useState(initialDisplayName || "");
|
||||||
|
const [removeAvatar, setRemoveAvatar] = useState(false);
|
||||||
|
|
||||||
|
const onRemoveAvatar = useCallback(() => {
|
||||||
|
setRemoveAvatar(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const onChangeDisplayName = useCallback(
|
const onChangeDisplayName = useCallback(
|
||||||
(e) => {
|
(e) => {
|
||||||
@@ -37,9 +40,10 @@ export function ProfileModal({
|
|||||||
saveProfile({
|
saveProfile({
|
||||||
displayName,
|
displayName,
|
||||||
avatar: avatar && avatar.size > 0 ? avatar : undefined,
|
avatar: avatar && avatar.size > 0 ? avatar : undefined,
|
||||||
|
removeAvatar: removeAvatar && (!avatar || avatar.size === 0),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[saveProfile]
|
[saveProfile, removeAvatar]
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -52,6 +56,16 @@ export function ProfileModal({
|
|||||||
<Modal title="Profile" isDismissable {...rest}>
|
<Modal title="Profile" isDismissable {...rest}>
|
||||||
<ModalContent>
|
<ModalContent>
|
||||||
<form onSubmit={onSubmit}>
|
<form onSubmit={onSubmit}>
|
||||||
|
<FieldRow className={styles.avatarFieldRow}>
|
||||||
|
<AvatarInputField
|
||||||
|
id="avatar"
|
||||||
|
name="avatar"
|
||||||
|
label="Avatar"
|
||||||
|
avatarUrl={avatarUrl}
|
||||||
|
displayName={displayName}
|
||||||
|
onRemoveAvatar={onRemoveAvatar}
|
||||||
|
/>
|
||||||
|
</FieldRow>
|
||||||
<FieldRow>
|
<FieldRow>
|
||||||
<InputField
|
<InputField
|
||||||
id="userId"
|
id="userId"
|
||||||
@@ -75,16 +89,6 @@ export function ProfileModal({
|
|||||||
onChange={onChangeDisplayName}
|
onChange={onChangeDisplayName}
|
||||||
/>
|
/>
|
||||||
</FieldRow>
|
</FieldRow>
|
||||||
{isAuthenticated && (
|
|
||||||
<FieldRow>
|
|
||||||
<InputField
|
|
||||||
type="file"
|
|
||||||
id="avatar"
|
|
||||||
name="avatar"
|
|
||||||
label="Avatar"
|
|
||||||
/>
|
|
||||||
</FieldRow>
|
|
||||||
)}
|
|
||||||
{error && (
|
{error && (
|
||||||
<FieldRow>
|
<FieldRow>
|
||||||
<ErrorMessage>{error.message}</ErrorMessage>
|
<ErrorMessage>{error.message}</ErrorMessage>
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
.avatarFieldRow {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ export function useProfile(client) {
|
|||||||
}, [client]);
|
}, [client]);
|
||||||
|
|
||||||
const saveProfile = useCallback(
|
const saveProfile = useCallback(
|
||||||
async ({ displayName, avatar }) => {
|
async ({ displayName, avatar, removeAvatar }) => {
|
||||||
if (client) {
|
if (client) {
|
||||||
setState((prev) => ({
|
setState((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
@@ -58,7 +58,9 @@ export function useProfile(client) {
|
|||||||
|
|
||||||
let mxcAvatarUrl;
|
let mxcAvatarUrl;
|
||||||
|
|
||||||
if (avatar) {
|
if (removeAvatar) {
|
||||||
|
await client.setAvatarUrl("");
|
||||||
|
} else if (avatar) {
|
||||||
mxcAvatarUrl = await client.uploadContent(avatar);
|
mxcAvatarUrl = await client.uploadContent(avatar);
|
||||||
await client.setAvatarUrl(mxcAvatarUrl);
|
await client.setAvatarUrl(mxcAvatarUrl);
|
||||||
}
|
}
|
||||||
@@ -66,7 +68,9 @@ export function useProfile(client) {
|
|||||||
setState((prev) => ({
|
setState((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
displayName,
|
displayName,
|
||||||
avatarUrl: mxcAvatarUrl
|
avatarUrl: removeAvatar
|
||||||
|
? null
|
||||||
|
: mxcAvatarUrl
|
||||||
? getAvatarUrl(client, mxcAvatarUrl)
|
? getAvatarUrl(client, mxcAvatarUrl)
|
||||||
: prev.avatarUrl,
|
: prev.avatarUrl,
|
||||||
loading: false,
|
loading: false,
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import { OverflowMenu } from "./OverflowMenu";
|
|||||||
import { UserMenuContainer } from "../UserMenuContainer";
|
import { UserMenuContainer } from "../UserMenuContainer";
|
||||||
import { Body, Link } from "../typography/Typography";
|
import { Body, Link } from "../typography/Typography";
|
||||||
import { Avatar } from "../Avatar";
|
import { Avatar } from "../Avatar";
|
||||||
import { getAvatarUrl } from "../matrix-utils";
|
|
||||||
import { useProfile } from "../profile/useProfile";
|
import { useProfile } from "../profile/useProfile";
|
||||||
import useMeasure from "react-use-measure";
|
import useMeasure from "react-use-measure";
|
||||||
import { ResizeObserver } from "@juggle/resize-observer";
|
import { ResizeObserver } from "@juggle/resize-observer";
|
||||||
@@ -86,7 +85,7 @@ export function LobbyView({
|
|||||||
borderRadius: avatarSize,
|
borderRadius: avatarSize,
|
||||||
fontSize: Math.round(avatarSize / 2),
|
fontSize: Math.round(avatarSize / 2),
|
||||||
}}
|
}}
|
||||||
src={avatarUrl && getAvatarUrl(client, avatarUrl, 96)}
|
src={avatarUrl}
|
||||||
fallback={displayName.slice(0, 1).toUpperCase()}
|
fallback={displayName.slice(0, 1).toUpperCase()}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user