Compare commits

...

2 Commits

Author SHA1 Message Date
Robert Long
81a763f17f Avoid duplicate sessions across devices/browsers 2022-02-18 16:23:37 -08:00
Robert Long
1ab7d27ba9 Finish user avatars 2022-02-18 16:02:27 -08:00
10 changed files with 179 additions and 38 deletions

View File

@@ -56,4 +56,5 @@
width: 90px;
height: 90px;
border-radius: 90px;
font-size: 48px;
}

View File

@@ -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]);

View File

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

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

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

View File

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

View File

@@ -0,0 +1,3 @@
.avatarFieldRow {
justify-content: center;
}

View File

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

View File

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