Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
81a763f17f | ||
|
|
1ab7d27ba9 | ||
|
|
e76a805c8f | ||
|
|
9fc4af2bd7 | ||
|
|
0f3a7f9fd9 | ||
|
|
1cc634509b |
@@ -24,6 +24,9 @@ yarn link
|
||||
cd ..
|
||||
|
||||
cd matrix-video-chat
|
||||
|
||||
export VITE_APP_VERSION=$(git describe --tags --abbrev=0)
|
||||
|
||||
yarn link matrix-js-sdk
|
||||
yarn link matrix-react-sdk
|
||||
yarn install
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -27,6 +27,8 @@ import { InspectorContextProvider } from "./room/GroupCallInspector";
|
||||
|
||||
rageshake.init();
|
||||
|
||||
console.info(`matrix-video-chat ${import.meta.env.VITE_APP_VERSION || "dev"}`);
|
||||
|
||||
if (import.meta.env.VITE_CUSTOM_THEME) {
|
||||
const style = document.documentElement.style;
|
||||
style.setProperty("--primaryColor", import.meta.env.VITE_PRIMARY_COLOR);
|
||||
|
||||
@@ -120,12 +120,12 @@ export function getRoomUrl(roomId) {
|
||||
const [localPart, host] = roomId.replace("#", "").split(":");
|
||||
|
||||
if (host !== defaultHomeserverHost) {
|
||||
return `${window.location.host}/room/${roomId}`;
|
||||
return `${window.location.protocol}//${window.location.host}/room/${roomId}`;
|
||||
} else {
|
||||
return `${window.location.host}/${localPart}`;
|
||||
return `${window.location.protocol}//${window.location.host}/${localPart}`;
|
||||
}
|
||||
} else {
|
||||
return `${window.location.host}/room/${roomId}`;
|
||||
return `${window.location.protocol}//${window.location.host}/room/${roomId}`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -59,8 +59,8 @@ function getUserName(userId) {
|
||||
const match = userId.match(/@([^\:]+):/);
|
||||
|
||||
return match && match.length > 0
|
||||
? match[1].replace("-", " ").replace("W", "")
|
||||
: userId.replace("W", "");
|
||||
? match[1].replace("-", " ").replace(/\W/g, "")
|
||||
: userId.replace(/\W/g, "");
|
||||
}
|
||||
|
||||
function formatContent(type, content) {
|
||||
@@ -231,7 +231,7 @@ function reducer(state, action) {
|
||||
),
|
||||
};
|
||||
}
|
||||
case "receive_to_device_event": {
|
||||
case "received_voip_event": {
|
||||
const event = action.event;
|
||||
const eventsByUserId = { ...state.eventsByUserId };
|
||||
const fromId = event.getSender();
|
||||
@@ -338,8 +338,8 @@ function useGroupCallState(client, groupCall, pollCallStats) {
|
||||
// dispatch({ type: "call_hangup", call });
|
||||
// }
|
||||
|
||||
function onToDeviceEvent(event) {
|
||||
dispatch({ type: "receive_to_device_event", event });
|
||||
function onReceivedVoipEvent(event) {
|
||||
dispatch({ type: "received_voip_event", event });
|
||||
}
|
||||
|
||||
function onSendVoipEvent(event) {
|
||||
@@ -351,7 +351,7 @@ function useGroupCallState(client, groupCall, pollCallStats) {
|
||||
groupCall.on("send_voip_event", onSendVoipEvent);
|
||||
//client.on("state", onCallsChanged);
|
||||
//client.on("hangup", onCallHangup);
|
||||
client.on("toDeviceEvent", onToDeviceEvent);
|
||||
client.on("received_voip_event", onReceivedVoipEvent);
|
||||
|
||||
onUpdateRoomState();
|
||||
|
||||
@@ -361,7 +361,7 @@ function useGroupCallState(client, groupCall, pollCallStats) {
|
||||
groupCall.removeListener("send_voip_event", onSendVoipEvent);
|
||||
//client.removeListener("state", onCallsChanged);
|
||||
//client.removeListener("hangup", onCallHangup);
|
||||
client.removeListener("toDeviceEvent", onToDeviceEvent);
|
||||
client.removeListener("received_voip_event", onReceivedVoipEvent);
|
||||
};
|
||||
}, [client, groupCall]);
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ import { Avatar } from "../Avatar";
|
||||
import { UserMenuContainer } from "../UserMenuContainer";
|
||||
import { useRageshakeRequestModal } from "../settings/rageshake";
|
||||
import { RageshakeRequestModal } from "./RageshakeRequestModal";
|
||||
import { usePreventScroll } from "@react-aria/overlays";
|
||||
|
||||
const canScreenshare = "getDisplayMedia" in navigator.mediaDevices;
|
||||
// There is currently a bug in Safari our our code with cloning and sending MediaStreams
|
||||
@@ -47,6 +48,7 @@ export function InCallView({
|
||||
showInspector,
|
||||
roomId,
|
||||
}) {
|
||||
usePreventScroll();
|
||||
const [layout, setLayout] = useVideoGridLayout(screenshareFeeds.length > 0);
|
||||
|
||||
const items = useMemo(() => {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -11,6 +11,7 @@ import { useMediaHandler } from "./useMediaHandler";
|
||||
import { FieldRow, InputField } from "../input/Input";
|
||||
import { Button } from "../button";
|
||||
import { useDownloadDebugLog } from "./rageshake";
|
||||
import { Body } from "../typography/Typography";
|
||||
|
||||
export function SettingsModal({
|
||||
client,
|
||||
@@ -82,6 +83,11 @@ export function SettingsModal({
|
||||
</>
|
||||
}
|
||||
>
|
||||
<FieldRow>
|
||||
<Body className={styles.fieldRowText}>
|
||||
Version: {import.meta.env.VITE_APP_VERSION || "dev"}
|
||||
</Body>
|
||||
</FieldRow>
|
||||
<FieldRow>
|
||||
<InputField
|
||||
id="showInspector"
|
||||
|
||||
@@ -6,3 +6,7 @@
|
||||
.tabContainer {
|
||||
margin: 27px 16px;
|
||||
}
|
||||
|
||||
.fieldRowText {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ export function useSubmitRageshake() {
|
||||
opts.description || "User did not supply any additional text."
|
||||
);
|
||||
body.append("app", "matrix-video-chat");
|
||||
body.append("version", "dev");
|
||||
body.append("version", import.meta.env.VITE_APP_VERSION || "dev");
|
||||
body.append("user_agent", userAgent);
|
||||
body.append("installed_pwa", false);
|
||||
body.append("touch_input", touchInput);
|
||||
|
||||
Reference in New Issue
Block a user