Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6a18ba0110 | ||
|
|
0a49ddb31e | ||
|
|
25385edf12 | ||
|
|
721cccf152 | ||
|
|
3b017eb92b | ||
|
|
641b82dc45 | ||
|
|
42e2041d6f | ||
|
|
2c3ebd4c03 | ||
|
|
81a763f17f | ||
|
|
1ab7d27ba9 | ||
|
|
e76a805c8f | ||
|
|
9fc4af2bd7 | ||
|
|
0f3a7f9fd9 | ||
|
|
1cc634509b |
@@ -24,6 +24,9 @@ yarn link
|
|||||||
cd ..
|
cd ..
|
||||||
|
|
||||||
cd matrix-video-chat
|
cd matrix-video-chat
|
||||||
|
|
||||||
|
export VITE_APP_VERSION=$(git describe --tags --abbrev=0)
|
||||||
|
|
||||||
yarn link matrix-js-sdk
|
yarn link matrix-js-sdk
|
||||||
yarn link matrix-react-sdk
|
yarn link matrix-react-sdk
|
||||||
yarn install
|
yarn install
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -38,6 +38,15 @@ export const InputField = forwardRef(
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{prefix && <span>{prefix}</span>}
|
{prefix && <span>{prefix}</span>}
|
||||||
|
{type === "textarea" ? (
|
||||||
|
<textarea
|
||||||
|
id={id}
|
||||||
|
{...rest}
|
||||||
|
ref={ref}
|
||||||
|
type={type}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
<input
|
<input
|
||||||
id={id}
|
id={id}
|
||||||
{...rest}
|
{...rest}
|
||||||
@@ -46,6 +55,8 @@ export const InputField = forwardRef(
|
|||||||
checked={checked}
|
checked={checked}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<label htmlFor={id}>
|
<label htmlFor={id}>
|
||||||
{type === "checkbox" && (
|
{type === "checkbox" && (
|
||||||
<div className={styles.checkbox}>
|
<div className={styles.checkbox}>
|
||||||
|
|||||||
@@ -29,7 +29,8 @@
|
|||||||
border: 1px solid var(--inputBorderColor);
|
border: 1px solid var(--inputBorderColor);
|
||||||
}
|
}
|
||||||
|
|
||||||
.inputField input {
|
.inputField input,
|
||||||
|
.inputField textarea {
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
border: none;
|
border: none;
|
||||||
@@ -42,6 +43,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.inputField.disabled input,
|
.inputField.disabled input,
|
||||||
|
.inputField.disabled textarea,
|
||||||
.inputField.disabled span {
|
.inputField.disabled span {
|
||||||
color: var(--textColor2);
|
color: var(--textColor2);
|
||||||
}
|
}
|
||||||
@@ -54,12 +56,14 @@
|
|||||||
padding-right: 0;
|
padding-right: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.inputField input::placeholder {
|
.inputField input::placeholder,
|
||||||
|
.inputField textarea::placeholder {
|
||||||
transition: color 0.25s ease-in 0s;
|
transition: color 0.25s ease-in 0s;
|
||||||
color: transparent;
|
color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.inputField input:placeholder-shown:focus::placeholder {
|
.inputField input:placeholder-shown:focus::placeholder,
|
||||||
|
.inputField textarea:placeholder-shown:focus::placeholder {
|
||||||
transition: color 0.25s ease-in 0.1s;
|
transition: color 0.25s ease-in 0.1s;
|
||||||
color: var(--textColor2);
|
color: var(--textColor2);
|
||||||
}
|
}
|
||||||
@@ -86,13 +90,17 @@
|
|||||||
border-color: var(--inputBorderColorFocused);
|
border-color: var(--inputBorderColorFocused);
|
||||||
}
|
}
|
||||||
|
|
||||||
.inputField input:focus {
|
.inputField input:focus,
|
||||||
|
.inputField textarea:focus {
|
||||||
outline: 0;
|
outline: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.inputField input:focus + label,
|
.inputField input:focus + label,
|
||||||
.inputField input:not(:placeholder-shown) + label,
|
.inputField input:not(:placeholder-shown) + label,
|
||||||
.inputField.prefix input + label {
|
.inputField.prefix input + label,
|
||||||
|
.inputField textarea:focus + label,
|
||||||
|
.inputField textarea:not(:placeholder-shown) + label,
|
||||||
|
.inputField.prefix textarea + label {
|
||||||
background-color: var(--bgColor2);
|
background-color: var(--bgColor2);
|
||||||
transition: font-size 0.25s ease-out 0s, color 0.25s ease-out 0s,
|
transition: font-size 0.25s ease-out 0s, color 0.25s ease-out 0s,
|
||||||
top 0.25s ease-out 0s, background-color 0.25s ease-out 0s;
|
top 0.25s ease-out 0s, background-color 0.25s ease-out 0s;
|
||||||
@@ -102,7 +110,8 @@
|
|||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.inputField input:focus + label {
|
.inputField input:focus + label,
|
||||||
|
.inputField textarea:focus + label {
|
||||||
color: var(--inputBorderColorFocused);
|
color: var(--inputBorderColorFocused);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ import { InspectorContextProvider } from "./room/GroupCallInspector";
|
|||||||
|
|
||||||
rageshake.init();
|
rageshake.init();
|
||||||
|
|
||||||
|
console.info(`matrix-video-chat ${import.meta.env.VITE_APP_VERSION || "dev"}`);
|
||||||
|
|
||||||
if (import.meta.env.VITE_CUSTOM_THEME) {
|
if (import.meta.env.VITE_CUSTOM_THEME) {
|
||||||
const style = document.documentElement.style;
|
const style = document.documentElement.style;
|
||||||
style.setProperty("--primaryColor", import.meta.env.VITE_PRIMARY_COLOR);
|
style.setProperty("--primaryColor", import.meta.env.VITE_PRIMARY_COLOR);
|
||||||
|
|||||||
@@ -120,12 +120,12 @@ export function getRoomUrl(roomId) {
|
|||||||
const [localPart, host] = roomId.replace("#", "").split(":");
|
const [localPart, host] = roomId.replace("#", "").split(":");
|
||||||
|
|
||||||
if (host !== defaultHomeserverHost) {
|
if (host !== defaultHomeserverHost) {
|
||||||
return `${window.location.host}/room/${roomId}`;
|
return `${window.location.protocol}//${window.location.host}/room/${roomId}`;
|
||||||
} else {
|
} else {
|
||||||
return `${window.location.host}/${localPart}`;
|
return `${window.location.protocol}//${window.location.host}/${localPart}`;
|
||||||
}
|
}
|
||||||
} else {
|
} 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 { 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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export function useProfile(client) {
|
|||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
displayName: user?.displayName,
|
displayName: user?.rawDisplayName,
|
||||||
avatarUrl: user && client && getAvatarUrl(client, user.avatarUrl),
|
avatarUrl: user && client && getAvatarUrl(client, user.avatarUrl),
|
||||||
error: null,
|
error: null,
|
||||||
};
|
};
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ export function FeedbackModal({ inCall, roomId, ...rest }) {
|
|||||||
description,
|
description,
|
||||||
sendLogs,
|
sendLogs,
|
||||||
rageshakeRequestId,
|
rageshakeRequestId,
|
||||||
|
roomId,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (inCall && sendLogs) {
|
if (inCall && sendLogs) {
|
||||||
@@ -47,7 +48,7 @@ export function FeedbackModal({ inCall, roomId, ...rest }) {
|
|||||||
id="description"
|
id="description"
|
||||||
name="description"
|
name="description"
|
||||||
label="Description (optional)"
|
label="Description (optional)"
|
||||||
type="text"
|
type="textarea"
|
||||||
/>
|
/>
|
||||||
</FieldRow>
|
</FieldRow>
|
||||||
<FieldRow>
|
<FieldRow>
|
||||||
|
|||||||
@@ -59,8 +59,8 @@ function getUserName(userId) {
|
|||||||
const match = userId.match(/@([^\:]+):/);
|
const match = userId.match(/@([^\:]+):/);
|
||||||
|
|
||||||
return match && match.length > 0
|
return match && match.length > 0
|
||||||
? match[1].replace("-", " ").replace("W", "")
|
? match[1].replace("-", " ").replace(/\W/g, "")
|
||||||
: userId.replace("W", "");
|
: userId.replace(/\W/g, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatContent(type, content) {
|
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 event = action.event;
|
||||||
const eventsByUserId = { ...state.eventsByUserId };
|
const eventsByUserId = { ...state.eventsByUserId };
|
||||||
const fromId = event.getSender();
|
const fromId = event.getSender();
|
||||||
@@ -338,8 +338,8 @@ function useGroupCallState(client, groupCall, pollCallStats) {
|
|||||||
// dispatch({ type: "call_hangup", call });
|
// dispatch({ type: "call_hangup", call });
|
||||||
// }
|
// }
|
||||||
|
|
||||||
function onToDeviceEvent(event) {
|
function onReceivedVoipEvent(event) {
|
||||||
dispatch({ type: "receive_to_device_event", event });
|
dispatch({ type: "received_voip_event", event });
|
||||||
}
|
}
|
||||||
|
|
||||||
function onSendVoipEvent(event) {
|
function onSendVoipEvent(event) {
|
||||||
@@ -351,7 +351,7 @@ function useGroupCallState(client, groupCall, pollCallStats) {
|
|||||||
groupCall.on("send_voip_event", onSendVoipEvent);
|
groupCall.on("send_voip_event", onSendVoipEvent);
|
||||||
//client.on("state", onCallsChanged);
|
//client.on("state", onCallsChanged);
|
||||||
//client.on("hangup", onCallHangup);
|
//client.on("hangup", onCallHangup);
|
||||||
client.on("toDeviceEvent", onToDeviceEvent);
|
client.on("received_voip_event", onReceivedVoipEvent);
|
||||||
|
|
||||||
onUpdateRoomState();
|
onUpdateRoomState();
|
||||||
|
|
||||||
@@ -361,7 +361,7 @@ function useGroupCallState(client, groupCall, pollCallStats) {
|
|||||||
groupCall.removeListener("send_voip_event", onSendVoipEvent);
|
groupCall.removeListener("send_voip_event", onSendVoipEvent);
|
||||||
//client.removeListener("state", onCallsChanged);
|
//client.removeListener("state", onCallsChanged);
|
||||||
//client.removeListener("hangup", onCallHangup);
|
//client.removeListener("hangup", onCallHangup);
|
||||||
client.removeListener("toDeviceEvent", onToDeviceEvent);
|
client.removeListener("received_voip_event", onReceivedVoipEvent);
|
||||||
};
|
};
|
||||||
}, [client, groupCall]);
|
}, [client, groupCall]);
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { isLocalRoomId } from "../matrix-utils";
|
|||||||
import { RoomNotFoundView } from "./RoomNotFoundView";
|
import { RoomNotFoundView } from "./RoomNotFoundView";
|
||||||
|
|
||||||
export function GroupCallLoader({ client, roomId, viaServers, children }) {
|
export function GroupCallLoader({ client, roomId, viaServers, children }) {
|
||||||
const { loading, error, groupCall } = useLoadGroupCall(
|
const { loading, error, groupCall, reload } = useLoadGroupCall(
|
||||||
client,
|
client,
|
||||||
roomId,
|
roomId,
|
||||||
viaServers
|
viaServers
|
||||||
@@ -29,7 +29,9 @@ export function GroupCallLoader({ client, roomId, viaServers, children }) {
|
|||||||
error.message.indexOf("Failed to fetch alias") !== -1)) &&
|
error.message.indexOf("Failed to fetch alias") !== -1)) &&
|
||||||
isLocalRoomId(roomId)
|
isLocalRoomId(roomId)
|
||||||
) {
|
) {
|
||||||
return <RoomNotFoundView client={client} roomId={roomId} />;
|
return (
|
||||||
|
<RoomNotFoundView client={client} roomId={roomId} onReload={reload} />
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ import { Avatar } from "../Avatar";
|
|||||||
import { UserMenuContainer } from "../UserMenuContainer";
|
import { UserMenuContainer } from "../UserMenuContainer";
|
||||||
import { useRageshakeRequestModal } from "../settings/rageshake";
|
import { useRageshakeRequestModal } from "../settings/rageshake";
|
||||||
import { RageshakeRequestModal } from "./RageshakeRequestModal";
|
import { RageshakeRequestModal } from "./RageshakeRequestModal";
|
||||||
|
import { usePreventScroll } from "@react-aria/overlays";
|
||||||
|
import { useMediaHandler } from "../settings/useMediaHandler";
|
||||||
|
|
||||||
const canScreenshare = "getDisplayMedia" in navigator.mediaDevices;
|
const canScreenshare = "getDisplayMedia" in navigator.mediaDevices;
|
||||||
// There is currently a bug in Safari our our code with cloning and sending MediaStreams
|
// There is currently a bug in Safari our our code with cloning and sending MediaStreams
|
||||||
@@ -47,8 +49,11 @@ export function InCallView({
|
|||||||
showInspector,
|
showInspector,
|
||||||
roomId,
|
roomId,
|
||||||
}) {
|
}) {
|
||||||
|
usePreventScroll();
|
||||||
const [layout, setLayout] = useVideoGridLayout(screenshareFeeds.length > 0);
|
const [layout, setLayout] = useVideoGridLayout(screenshareFeeds.length > 0);
|
||||||
|
|
||||||
|
const { audioOutput } = useMediaHandler();
|
||||||
|
|
||||||
const items = useMemo(() => {
|
const items = useMemo(() => {
|
||||||
const participants = [];
|
const participants = [];
|
||||||
|
|
||||||
@@ -157,6 +162,8 @@ export function InCallView({
|
|||||||
item={item}
|
item={item}
|
||||||
getAvatar={renderAvatar}
|
getAvatar={renderAvatar}
|
||||||
showName={items.length > 2 || item.focused}
|
showName={items.length > 2 || item.focused}
|
||||||
|
audioOutputDevice={audioOutput}
|
||||||
|
disableSpeakingIndicator={items.length < 3}
|
||||||
{...rest}
|
{...rest}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -187,7 +194,10 @@ export function InCallView({
|
|||||||
show={showInspector}
|
show={showInspector}
|
||||||
/>
|
/>
|
||||||
{rageshakeRequestModalState.isOpen && (
|
{rageshakeRequestModalState.isOpen && (
|
||||||
<RageshakeRequestModal {...rageshakeRequestModalProps} />
|
<RageshakeRequestModal
|
||||||
|
{...rageshakeRequestModalProps}
|
||||||
|
roomId={roomId}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -10,11 +10,11 @@ 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";
|
||||||
import { useLocationNavigation } from "../useLocationNavigation";
|
import { useLocationNavigation } from "../useLocationNavigation";
|
||||||
|
import { useMediaHandler } from "../settings/useMediaHandler";
|
||||||
|
|
||||||
export function LobbyView({
|
export function LobbyView({
|
||||||
client,
|
client,
|
||||||
@@ -32,7 +32,8 @@ export function LobbyView({
|
|||||||
roomId,
|
roomId,
|
||||||
}) {
|
}) {
|
||||||
const { stream } = useCallFeed(localCallFeed);
|
const { stream } = useCallFeed(localCallFeed);
|
||||||
const videoRef = useMediaStream(stream, true);
|
const { audioOutput } = useMediaHandler();
|
||||||
|
const videoRef = useMediaStream(stream, audioOutput, true);
|
||||||
const { displayName, avatarUrl } = useProfile(client);
|
const { displayName, avatarUrl } = useProfile(client);
|
||||||
const [previewRef, previewBounds] = useMeasure({ polyfill: ResizeObserver });
|
const [previewRef, previewBounds] = useMeasure({ polyfill: ResizeObserver });
|
||||||
const avatarSize = (previewBounds.height - 66) / 2;
|
const avatarSize = (previewBounds.height - 66) / 2;
|
||||||
@@ -86,7 +87,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>
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ export function OverflowMenu({
|
|||||||
roomId,
|
roomId,
|
||||||
setShowInspector,
|
setShowInspector,
|
||||||
showInspector,
|
showInspector,
|
||||||
client,
|
|
||||||
inCall,
|
inCall,
|
||||||
groupCall,
|
groupCall,
|
||||||
}) {
|
}) {
|
||||||
@@ -75,7 +74,6 @@ export function OverflowMenu({
|
|||||||
{...settingsModalProps}
|
{...settingsModalProps}
|
||||||
setShowInspector={setShowInspector}
|
setShowInspector={setShowInspector}
|
||||||
showInspector={showInspector}
|
showInspector={showInspector}
|
||||||
client={client}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{inviteModalState.isOpen && (
|
{inviteModalState.isOpen && (
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { FieldRow, ErrorMessage } from "../input/Input";
|
|||||||
import { useSubmitRageshake } from "../settings/rageshake";
|
import { useSubmitRageshake } from "../settings/rageshake";
|
||||||
import { Body } from "../typography/Typography";
|
import { Body } from "../typography/Typography";
|
||||||
|
|
||||||
export function RageshakeRequestModal({ rageshakeRequestId, ...rest }) {
|
export function RageshakeRequestModal({ rageshakeRequestId, roomId, ...rest }) {
|
||||||
const { submitRageshake, sending, sent, error } = useSubmitRageshake();
|
const { submitRageshake, sending, sent, error } = useSubmitRageshake();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -27,6 +27,7 @@ export function RageshakeRequestModal({ rageshakeRequestId, ...rest }) {
|
|||||||
submitRageshake({
|
submitRageshake({
|
||||||
sendLogs: true,
|
sendLogs: true,
|
||||||
rageshakeRequestId,
|
rageshakeRequestId,
|
||||||
|
roomId,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
disabled={sending}
|
disabled={sending}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { Form } from "../form/Form";
|
|||||||
import { useHistory } from "react-router-dom";
|
import { useHistory } from "react-router-dom";
|
||||||
import styles from "./RoomNotFoundView.module.css";
|
import styles from "./RoomNotFoundView.module.css";
|
||||||
|
|
||||||
export function RoomNotFoundView({ client, roomId }) {
|
export function RoomNotFoundView({ client, roomId, onReload }) {
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [error, setError] = useState();
|
const [error, setError] = useState();
|
||||||
@@ -21,11 +21,9 @@ export function RoomNotFoundView({ client, roomId }) {
|
|||||||
setError(undefined);
|
setError(undefined);
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
const roomIdOrAlias = await createRoom(client, roomName);
|
await createRoom(client, roomName);
|
||||||
|
|
||||||
if (roomIdOrAlias) {
|
onReload();
|
||||||
history.push(`/room/${roomIdOrAlias}`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
submit().catch((error) => {
|
submit().catch((error) => {
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import { ErrorView, LoadingView } from "../FullScreenView";
|
|||||||
import { RoomAuthView } from "./RoomAuthView";
|
import { RoomAuthView } from "./RoomAuthView";
|
||||||
import { GroupCallLoader } from "./GroupCallLoader";
|
import { GroupCallLoader } from "./GroupCallLoader";
|
||||||
import { GroupCallView } from "./GroupCallView";
|
import { GroupCallView } from "./GroupCallView";
|
||||||
|
import { MediaHandlerProvider } from "../settings/useMediaHandler";
|
||||||
|
|
||||||
export function RoomPage() {
|
export function RoomPage() {
|
||||||
const { loading, isAuthenticated, error, client, isPasswordlessUser } =
|
const { loading, isAuthenticated, error, client, isPasswordlessUser } =
|
||||||
@@ -47,6 +48,7 @@ export function RoomPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<MediaHandlerProvider client={client}>
|
||||||
<GroupCallLoader client={client} roomId={roomId} viaServers={viaServers}>
|
<GroupCallLoader client={client} roomId={roomId} viaServers={viaServers}>
|
||||||
{(groupCall) => (
|
{(groupCall) => (
|
||||||
<GroupCallView
|
<GroupCallView
|
||||||
@@ -58,5 +60,6 @@ export function RoomPage() {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</GroupCallLoader>
|
</GroupCallLoader>
|
||||||
|
</MediaHandlerProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect, useCallback } from "react";
|
||||||
|
|
||||||
async function fetchGroupCall(
|
async function fetchGroupCall(
|
||||||
client,
|
client,
|
||||||
@@ -41,14 +41,23 @@ export function useLoadGroupCall(client, roomId, viaServers) {
|
|||||||
loading: true,
|
loading: true,
|
||||||
error: undefined,
|
error: undefined,
|
||||||
groupCall: undefined,
|
groupCall: undefined,
|
||||||
|
reloadId: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setState({ loading: true });
|
setState({ loading: true });
|
||||||
fetchGroupCall(client, roomId, viaServers, 30000)
|
fetchGroupCall(client, roomId, viaServers, 30000)
|
||||||
.then((groupCall) => setState({ loading: false, groupCall }))
|
.then((groupCall) =>
|
||||||
.catch((error) => setState({ loading: false, error }));
|
setState((prevState) => ({ ...prevState, loading: false, groupCall }))
|
||||||
}, [client, roomId]);
|
)
|
||||||
|
.catch((error) =>
|
||||||
|
setState((prevState) => ({ ...prevState, loading: false, error }))
|
||||||
|
);
|
||||||
|
}, [client, roomId, state.reloadId]);
|
||||||
|
|
||||||
return state;
|
const reload = useCallback(() => {
|
||||||
|
setState((prevState) => ({ ...prevState, reloadId: prevState.reloadId++ }));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return { ...state, reload };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,13 +11,9 @@ import { useMediaHandler } from "./useMediaHandler";
|
|||||||
import { FieldRow, InputField } from "../input/Input";
|
import { FieldRow, InputField } from "../input/Input";
|
||||||
import { Button } from "../button";
|
import { Button } from "../button";
|
||||||
import { useDownloadDebugLog } from "./rageshake";
|
import { useDownloadDebugLog } from "./rageshake";
|
||||||
|
import { Body } from "../typography/Typography";
|
||||||
|
|
||||||
export function SettingsModal({
|
export function SettingsModal({ setShowInspector, showInspector, ...rest }) {
|
||||||
client,
|
|
||||||
setShowInspector,
|
|
||||||
showInspector,
|
|
||||||
...rest
|
|
||||||
}) {
|
|
||||||
const {
|
const {
|
||||||
audioInput,
|
audioInput,
|
||||||
audioInputs,
|
audioInputs,
|
||||||
@@ -25,7 +21,10 @@ export function SettingsModal({
|
|||||||
videoInput,
|
videoInput,
|
||||||
videoInputs,
|
videoInputs,
|
||||||
setVideoInput,
|
setVideoInput,
|
||||||
} = useMediaHandler(client);
|
audioOutput,
|
||||||
|
audioOutputs,
|
||||||
|
setAudioOutput,
|
||||||
|
} = useMediaHandler();
|
||||||
|
|
||||||
const downloadDebugLog = useDownloadDebugLog();
|
const downloadDebugLog = useDownloadDebugLog();
|
||||||
|
|
||||||
@@ -55,6 +54,17 @@ export function SettingsModal({
|
|||||||
<Item key={deviceId}>{label}</Item>
|
<Item key={deviceId}>{label}</Item>
|
||||||
))}
|
))}
|
||||||
</SelectInput>
|
</SelectInput>
|
||||||
|
{audioOutputs.length > 0 && (
|
||||||
|
<SelectInput
|
||||||
|
label="Speaker"
|
||||||
|
selectedKey={audioOutput}
|
||||||
|
onSelectionChange={setAudioOutput}
|
||||||
|
>
|
||||||
|
{audioOutputs.map(({ deviceId, label }) => (
|
||||||
|
<Item key={deviceId}>{label}</Item>
|
||||||
|
))}
|
||||||
|
</SelectInput>
|
||||||
|
)}
|
||||||
</TabItem>
|
</TabItem>
|
||||||
<TabItem
|
<TabItem
|
||||||
title={
|
title={
|
||||||
@@ -82,6 +92,11 @@ export function SettingsModal({
|
|||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
<FieldRow>
|
||||||
|
<Body className={styles.fieldRowText}>
|
||||||
|
Version: {import.meta.env.VITE_APP_VERSION || "dev"}
|
||||||
|
</Body>
|
||||||
|
</FieldRow>
|
||||||
<FieldRow>
|
<FieldRow>
|
||||||
<InputField
|
<InputField
|
||||||
id="showInspector"
|
id="showInspector"
|
||||||
|
|||||||
@@ -6,3 +6,7 @@
|
|||||||
.tabContainer {
|
.tabContainer {
|
||||||
margin: 27px 16px;
|
margin: 27px 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.fieldRowText {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|||||||
@@ -41,15 +41,22 @@ export function useSubmitRageshake() {
|
|||||||
opts.description || "User did not supply any additional text."
|
opts.description || "User did not supply any additional text."
|
||||||
);
|
);
|
||||||
body.append("app", "matrix-video-chat");
|
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("user_agent", userAgent);
|
||||||
body.append("installed_pwa", false);
|
body.append("installed_pwa", false);
|
||||||
body.append("touch_input", touchInput);
|
body.append("touch_input", touchInput);
|
||||||
|
|
||||||
if (client) {
|
if (client) {
|
||||||
|
const userId = client.getUserId();
|
||||||
|
const user = client.getUser(userId);
|
||||||
|
body.append("display_name", user?.displayName);
|
||||||
body.append("user_id", client.credentials.userId);
|
body.append("user_id", client.credentials.userId);
|
||||||
body.append("device_id", client.deviceId);
|
body.append("device_id", client.deviceId);
|
||||||
|
|
||||||
|
if (opts.roomId) {
|
||||||
|
body.append("room_id", opts.roomId);
|
||||||
|
}
|
||||||
|
|
||||||
if (client.isCryptoEnabled()) {
|
if (client.isCryptoEnabled()) {
|
||||||
const keys = [`ed25519:${client.getDeviceEd25519Key()}`];
|
const keys = [`ed25519:${client.getDeviceEd25519Key()}`];
|
||||||
if (client.getDeviceCurve25519Key) {
|
if (client.getDeviceCurve25519Key) {
|
||||||
|
|||||||
@@ -1,72 +0,0 @@
|
|||||||
import { useState, useEffect, useCallback } from "react";
|
|
||||||
|
|
||||||
export function useMediaHandler(client) {
|
|
||||||
const [{ audioInput, videoInput, audioInputs, videoInputs }, setState] =
|
|
||||||
useState(() => {
|
|
||||||
const mediaHandler = client.getMediaHandler();
|
|
||||||
|
|
||||||
return {
|
|
||||||
audioInput: mediaHandler.audioInput,
|
|
||||||
videoInput: mediaHandler.videoInput,
|
|
||||||
audioInputs: [],
|
|
||||||
videoInputs: [],
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const mediaHandler = client.getMediaHandler();
|
|
||||||
|
|
||||||
function updateDevices() {
|
|
||||||
navigator.mediaDevices.enumerateDevices().then((devices) => {
|
|
||||||
const audioInputs = devices.filter(
|
|
||||||
(device) => device.kind === "audioinput"
|
|
||||||
);
|
|
||||||
const videoInputs = devices.filter(
|
|
||||||
(device) => device.kind === "videoinput"
|
|
||||||
);
|
|
||||||
|
|
||||||
setState(() => ({
|
|
||||||
audioInput: mediaHandler.audioInput,
|
|
||||||
videoInput: mediaHandler.videoInput,
|
|
||||||
audioInputs,
|
|
||||||
videoInputs,
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
updateDevices();
|
|
||||||
|
|
||||||
mediaHandler.on("local_streams_changed", updateDevices);
|
|
||||||
navigator.mediaDevices.addEventListener("devicechange", updateDevices);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
mediaHandler.removeListener("local_streams_changed", updateDevices);
|
|
||||||
navigator.mediaDevices.removeEventListener("devicechange", updateDevices);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const setAudioInput = useCallback(
|
|
||||||
(deviceId) => {
|
|
||||||
setState((prevState) => ({ ...prevState, audioInput: deviceId }));
|
|
||||||
client.getMediaHandler().setAudioInput(deviceId);
|
|
||||||
},
|
|
||||||
[client]
|
|
||||||
);
|
|
||||||
|
|
||||||
const setVideoInput = useCallback(
|
|
||||||
(deviceId) => {
|
|
||||||
setState((prevState) => ({ ...prevState, videoInput: deviceId }));
|
|
||||||
client.getMediaHandler().setVideoInput(deviceId);
|
|
||||||
},
|
|
||||||
[client]
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
audioInput,
|
|
||||||
audioInputs,
|
|
||||||
setAudioInput,
|
|
||||||
videoInput,
|
|
||||||
videoInputs,
|
|
||||||
setVideoInput,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
203
src/settings/useMediaHandler.jsx
Normal file
203
src/settings/useMediaHandler.jsx
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
import React, {
|
||||||
|
useState,
|
||||||
|
useEffect,
|
||||||
|
useCallback,
|
||||||
|
useMemo,
|
||||||
|
useContext,
|
||||||
|
createContext,
|
||||||
|
} from "react";
|
||||||
|
|
||||||
|
const MediaHandlerContext = createContext();
|
||||||
|
|
||||||
|
function getMediaPreferences() {
|
||||||
|
const mediaPreferences = localStorage.getItem("matrix-media-preferences");
|
||||||
|
|
||||||
|
if (mediaPreferences) {
|
||||||
|
try {
|
||||||
|
return JSON.parse(mediaPreferences);
|
||||||
|
} catch (e) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateMediaPreferences(newPreferences) {
|
||||||
|
const oldPreferences = getMediaPreferences(newPreferences);
|
||||||
|
|
||||||
|
localStorage.setItem(
|
||||||
|
"matrix-media-preferences",
|
||||||
|
JSON.stringify({
|
||||||
|
...oldPreferences,
|
||||||
|
...newPreferences,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MediaHandlerProvider({ client, children }) {
|
||||||
|
const [
|
||||||
|
{
|
||||||
|
audioInput,
|
||||||
|
videoInput,
|
||||||
|
audioInputs,
|
||||||
|
videoInputs,
|
||||||
|
audioOutput,
|
||||||
|
audioOutputs,
|
||||||
|
},
|
||||||
|
setState,
|
||||||
|
] = useState(() => {
|
||||||
|
const mediaPreferences = getMediaPreferences();
|
||||||
|
const mediaHandler = client.getMediaHandler();
|
||||||
|
|
||||||
|
mediaHandler.restoreMediaSettings(
|
||||||
|
mediaPreferences?.audioInput,
|
||||||
|
mediaPreferences?.videoInput
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
audioInput: mediaHandler.audioInput,
|
||||||
|
videoInput: mediaHandler.videoInput,
|
||||||
|
audioOutput: undefined,
|
||||||
|
audioInputs: [],
|
||||||
|
videoInputs: [],
|
||||||
|
audioOutputs: [],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const mediaHandler = client.getMediaHandler();
|
||||||
|
|
||||||
|
function updateDevices() {
|
||||||
|
navigator.mediaDevices.enumerateDevices().then((devices) => {
|
||||||
|
const mediaPreferences = getMediaPreferences();
|
||||||
|
|
||||||
|
const audioInputs = devices.filter(
|
||||||
|
(device) => device.kind === "audioinput"
|
||||||
|
);
|
||||||
|
const audioConnected = audioInputs.some(
|
||||||
|
(device) => device.deviceId === mediaHandler.audioInput
|
||||||
|
);
|
||||||
|
|
||||||
|
let audioInput = mediaHandler.audioInput;
|
||||||
|
|
||||||
|
if (!audioConnected && audioInputs.length > 0) {
|
||||||
|
audioInput = audioInputs[0].deviceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
const videoInputs = devices.filter(
|
||||||
|
(device) => device.kind === "videoinput"
|
||||||
|
);
|
||||||
|
const videoConnected = videoInputs.some(
|
||||||
|
(device) => device.deviceId === mediaHandler.videoInput
|
||||||
|
);
|
||||||
|
|
||||||
|
let videoInput = mediaHandler.videoInput;
|
||||||
|
|
||||||
|
if (!videoConnected && videoInputs.length > 0) {
|
||||||
|
videoInput = videoInputs[0].deviceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
const audioOutputs = devices.filter(
|
||||||
|
(device) => device.kind === "audiooutput"
|
||||||
|
);
|
||||||
|
let audioOutput = undefined;
|
||||||
|
|
||||||
|
if (
|
||||||
|
mediaPreferences &&
|
||||||
|
audioOutputs.some(
|
||||||
|
(device) => device.deviceId === mediaPreferences.audioOutput
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
audioOutput = mediaPreferences.audioOutput;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
mediaHandler.videoInput !== videoInput ||
|
||||||
|
mediaHandler.audioInput !== audioInput
|
||||||
|
) {
|
||||||
|
mediaHandler.setMediaInputs(audioInput, videoInput);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateMediaPreferences({ audioInput, videoInput, audioOutput });
|
||||||
|
|
||||||
|
setState({
|
||||||
|
audioInput,
|
||||||
|
videoInput,
|
||||||
|
audioOutput,
|
||||||
|
audioInputs,
|
||||||
|
videoInputs,
|
||||||
|
audioOutputs,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
updateDevices();
|
||||||
|
|
||||||
|
mediaHandler.on("local_streams_changed", updateDevices);
|
||||||
|
navigator.mediaDevices.addEventListener("devicechange", updateDevices);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
mediaHandler.removeListener("local_streams_changed", updateDevices);
|
||||||
|
navigator.mediaDevices.removeEventListener("devicechange", updateDevices);
|
||||||
|
mediaHandler.stopAllStreams();
|
||||||
|
};
|
||||||
|
}, [client]);
|
||||||
|
|
||||||
|
const setAudioInput = useCallback(
|
||||||
|
(deviceId) => {
|
||||||
|
updateMediaPreferences({ audioInput: deviceId });
|
||||||
|
setState((prevState) => ({ ...prevState, audioInput: deviceId }));
|
||||||
|
client.getMediaHandler().setAudioInput(deviceId);
|
||||||
|
},
|
||||||
|
[client]
|
||||||
|
);
|
||||||
|
|
||||||
|
const setVideoInput = useCallback(
|
||||||
|
(deviceId) => {
|
||||||
|
updateMediaPreferences({ videoInput: deviceId });
|
||||||
|
setState((prevState) => ({ ...prevState, videoInput: deviceId }));
|
||||||
|
client.getMediaHandler().setVideoInput(deviceId);
|
||||||
|
},
|
||||||
|
[client]
|
||||||
|
);
|
||||||
|
|
||||||
|
const setAudioOutput = useCallback((deviceId) => {
|
||||||
|
updateMediaPreferences({ audioOutput: deviceId });
|
||||||
|
setState((prevState) => ({ ...prevState, audioOutput: deviceId }));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const context = useMemo(
|
||||||
|
() => ({
|
||||||
|
audioInput,
|
||||||
|
audioInputs,
|
||||||
|
setAudioInput,
|
||||||
|
videoInput,
|
||||||
|
videoInputs,
|
||||||
|
setVideoInput,
|
||||||
|
audioOutput,
|
||||||
|
audioOutputs,
|
||||||
|
setAudioOutput,
|
||||||
|
}),
|
||||||
|
[
|
||||||
|
audioInput,
|
||||||
|
audioInputs,
|
||||||
|
setAudioInput,
|
||||||
|
videoInput,
|
||||||
|
videoInputs,
|
||||||
|
setVideoInput,
|
||||||
|
audioOutput,
|
||||||
|
audioOutputs,
|
||||||
|
setAudioOutput,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MediaHandlerContext.Provider value={context}>
|
||||||
|
{children}
|
||||||
|
</MediaHandlerContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useMediaHandler() {
|
||||||
|
return useContext(MediaHandlerContext);
|
||||||
|
}
|
||||||
@@ -64,6 +64,7 @@ export const ParticipantsTest = () => {
|
|||||||
key={item.id}
|
key={item.id}
|
||||||
name={`User ${item.id}`}
|
name={`User ${item.id}`}
|
||||||
showName={items.length > 2 || item.focused}
|
showName={items.length > 2 || item.focused}
|
||||||
|
disableSpeakingIndicator={items.length < 3}
|
||||||
{...rest}
|
{...rest}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user