Compare commits

...

29 Commits

Author SHA1 Message Date
Robert Long
9900d661be Fix avatar background when bgKey undefined 2022-03-01 11:37:45 -08:00
Robert Long
369b59a203 Fix ordering in 1-1 calls 2022-02-25 16:26:21 -08:00
Robert Long
6a18ba0110 Use raw display names for members 2022-02-23 16:41:12 -08:00
Robert Long
0a49ddb31e Fix input prefix 2022-02-23 16:34:11 -08:00
Robert Long
25385edf12 Use a textarea in the feedback modal 2022-02-23 16:07:14 -08:00
Robert Long
721cccf152 Disable speaking indicator in 1-1 calls 2022-02-23 15:59:16 -08:00
Robert Long
3b017eb92b Add room_id and display_name to rageshakes 2022-02-23 15:52:53 -08:00
Robert Long
641b82dc45 Fix creating rooms from not found screen 2022-02-23 15:36:38 -08:00
Robert Long
42e2041d6f Fix media handler device changes 2022-02-23 15:07:51 -08:00
Robert Long
2c3ebd4c03 Add audio output setting when available 2022-02-22 18:32:51 -08:00
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
Robert Long
e76a805c8f Better logging of to device events / usernames 2022-02-17 14:08:53 -08:00
Robert Long
9fc4af2bd7 Add version to console, rageshake, and settings modal 2022-02-16 11:29:43 -08:00
Robert Long
0f3a7f9fd9 Prevent scroll in call view 2022-02-16 11:17:33 -08:00
Robert Long
1cc634509b Add protocol to copied room url 2022-02-16 10:52:07 -08:00
Robert Long
cb07ce32cb Fix room not found view 2022-02-15 15:00:06 -08:00
Robert Long
6866d662f7 Automatically switch to spotlight layout on screenshare 2022-02-15 14:49:50 -08:00
Robert Long
51a2027d64 Fix screenshare button styling 2022-02-15 12:58:55 -08:00
Robert Long
0f6b8f9bb1 New incremental auth 2022-02-15 12:46:58 -08:00
Robert Long
63229ce2d7 Fix video grid story 2022-02-14 14:49:19 -08:00
Robert Long
1d620910c5 Only show name when focused or more than 2 participants 2022-02-14 14:48:12 -08:00
Robert Long
47357b3fc6 Add room not found view 2022-02-14 13:53:19 -08:00
Robert Long
3ed35f9477 Fix deprecated usage of substr 2022-02-14 12:35:39 -08:00
Robert Long
a369444b62 Convert room id to lowercase 2022-02-14 12:35:07 -08:00
Robert Long
742d658021 Center align call tile contents 2022-02-14 12:19:54 -08:00
Robert Long
681c24a0ca Fix focusing in freedom layout 2022-02-14 11:14:09 -08:00
Robert Long
fc057bf988 Prevent opening multiple tabs of the same account 2022-02-10 17:10:36 -08:00
Robert Long
51561e2f4e Set rageshake submit url for prod 2022-02-07 16:16:51 -08:00
45 changed files with 952 additions and 254 deletions

View File

@@ -41,7 +41,8 @@
"react-router": "6",
"react-router-dom": "^5.2.0",
"react-use-clipboard": "^1.0.7",
"react-use-measure": "^2.1.1"
"react-use-measure": "^2.1.1",
"unique-names-generator": "^4.6.0"
},
"devDependencies": {
"@babel/core": "^7.16.5",

View File

@@ -4,6 +4,7 @@ set -ex
export VITE_DEFAULT_HOMESERVER=https://call.ems.host
export VITE_SENTRY_DSN=https://b1e328d49be3402ba96101338989fb35@sentry.matrix.org/41
export VITE_RAGESHAKE_SUBMIT_URL=https://element.io/bugreports/submit
git clone https://github.com/matrix-org/matrix-js-sdk.git
cd matrix-js-sdk
@@ -23,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

View File

@@ -34,7 +34,7 @@ export function Avatar({
}) {
const backgroundColor = useMemo(() => {
const index = hashStringToArrIndex(
bgKey || fallback || src,
bgKey || fallback || src || "",
backgroundColors.length
);
return backgroundColors[index];

View File

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

View File

@@ -23,6 +23,7 @@ import React, {
useContext,
} from "react";
import { useHistory } from "react-router-dom";
import { ErrorView } from "./FullScreenView";
import { initClient, defaultHomeserver } from "./matrix-utils";
const ClientContext = createContext();
@@ -30,7 +31,7 @@ const ClientContext = createContext();
export function ClientProvider({ children }) {
const history = useHistory();
const [
{ loading, isAuthenticated, isPasswordlessUser, client, userName },
{ loading, isAuthenticated, isPasswordlessUser, client, userName, error },
setState,
] = useState({
loading: true,
@@ -38,6 +39,7 @@ export function ClientProvider({ children }) {
isPasswordlessUser: false,
client: undefined,
userName: null,
error: undefined,
});
useEffect(() => {
@@ -143,35 +145,85 @@ export function ClientProvider({ children }) {
[client]
);
const setClient = useCallback((client, session) => {
if (client) {
localStorage.setItem("matrix-auth-store", JSON.stringify(session));
const setClient = useCallback(
(newClient, session) => {
if (client && client !== newClient) {
client.stopClient();
}
setState({
client,
loading: false,
isAuthenticated: true,
isPasswordlessUser: !!session.passwordlessUser,
userName: client.getUserIdLocalpart(),
});
} else {
localStorage.removeItem("matrix-auth-store");
if (newClient) {
localStorage.setItem("matrix-auth-store", JSON.stringify(session));
setState({
client: undefined,
loading: false,
isAuthenticated: false,
isPasswordlessUser: false,
userName: null,
});
}
}, []);
setState({
client: newClient,
loading: false,
isAuthenticated: true,
isPasswordlessUser: !!session.passwordlessUser,
userName: newClient.getUserIdLocalpart(),
});
} else {
localStorage.removeItem("matrix-auth-store");
setState({
client: undefined,
loading: false,
isAuthenticated: false,
isPasswordlessUser: false,
userName: null,
});
}
},
[client]
);
const logout = useCallback(() => {
localStorage.removeItem("matrix-auth-store");
window.location = "/";
}, [history]);
useEffect(() => {
if (client) {
const loadTime = Date.now();
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();
}
setState((prev) => ({
...prev,
error: new Error(
"This application has been opened in another tab."
),
}));
}
};
client.on("toDeviceEvent", onToDeviceEvent);
client.sendToDevice("org.matrix.call_duplicate_session", {
[client.getUserId()]: {
"*": { session_id: client.getSessionId(), timestamp: loadTime },
},
});
return () => {
client.removeListener("toDeviceEvent", onToDeviceEvent);
};
}
}, [client]);
const context = useMemo(
() => ({
loading,
@@ -195,6 +247,14 @@ export function ClientProvider({ children }) {
]
);
useEffect(() => {
window.matrixclient = client;
}, [client]);
if (error) {
return <ErrorView error={error} />;
}
return (
<ClientContext.Provider value={context}>{children}</ClientContext.Provider>
);

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

View File

@@ -31,13 +31,7 @@ import { usePageTitle } from "../usePageTitle";
export function RegisterPage() {
usePageTitle("Register");
const {
loading,
client,
changePassword,
isAuthenticated,
isPasswordlessUser,
} = useClient();
const { loading, isAuthenticated, isPasswordlessUser, client } = useClient();
const confirmPasswordRef = useRef();
const history = useHistory();
const location = useLocation();
@@ -64,11 +58,31 @@ export function RegisterPage() {
async function submit() {
setRegistering(true);
if (isPasswordlessUser) {
await changePassword(password);
} else {
const recaptchaResponse = await execute();
await register(userName, password, recaptchaResponse);
let roomIds;
if (client && isPasswordlessUser) {
const groupCalls = client.groupCallEventHandler.groupCalls.values();
roomIds = Array.from(groupCalls).map(
(groupCall) => groupCall.room.roomId
);
}
const recaptchaResponse = await execute();
const newClient = await register(
userName,
password,
userName,
recaptchaResponse
);
if (roomIds) {
for (const roomId of roomIds) {
try {
await newClient.joinRoom(roomId);
} catch (error) {
console.warn(`Couldn't join room ${roomId}`, error);
}
}
}
}
@@ -86,15 +100,7 @@ export function RegisterPage() {
reset();
});
},
[
register,
changePassword,
location,
history,
isPasswordlessUser,
reset,
execute,
]
[register, location, history, isPasswordlessUser, reset, execute, client]
);
useEffect(() => {
@@ -110,10 +116,10 @@ export function RegisterPage() {
}, [password, passwordConfirmation]);
useEffect(() => {
if (!loading && isAuthenticated && !isPasswordlessUser) {
if (!loading && isAuthenticated && !isPasswordlessUser && !registering) {
history.push("/");
}
}, [history, isAuthenticated, isPasswordlessUser]);
}, [history, isAuthenticated, isPasswordlessUser, registering]);
if (loading) {
return <LoadingView />;
@@ -137,12 +143,6 @@ export function RegisterPage() {
autoCapitalize="none"
prefix="@"
suffix={`:${defaultHomeserverHost}`}
value={
isAuthenticated && isPasswordlessUser
? client.getUserIdLocalpart()
: undefined
}
disabled={isAuthenticated && isPasswordlessUser}
/>
</FieldRow>
<FieldRow>
@@ -168,22 +168,20 @@ export function RegisterPage() {
ref={confirmPasswordRef}
/>
</FieldRow>
{!isPasswordlessUser && (
<Caption>
This site is protected by ReCAPTCHA and the Google{" "}
<Link href="https://www.google.com/policies/privacy/">
Privacy Policy
</Link>{" "}
and{" "}
<Link href="https://policies.google.com/terms">
Terms of Service
</Link>{" "}
apply.
<br />
By clicking "Log in", you agree to our{" "}
<Link href={privacyPolicyUrl}>Terms and conditions</Link>
</Caption>
)}
<Caption>
This site is protected by ReCAPTCHA and the Google{" "}
<Link href="https://www.google.com/policies/privacy/">
Privacy Policy
</Link>{" "}
and{" "}
<Link href="https://policies.google.com/terms">
Terms of Service
</Link>{" "}
apply.
<br />
By clicking "Register", you agree to our{" "}
<Link href={privacyPolicyUrl}>Terms and conditions</Link>
</Caption>
{error && (
<FieldRow>
<ErrorMessage>{error.message}</ErrorMessage>

View File

@@ -0,0 +1,137 @@
import {
uniqueNamesGenerator,
adjectives,
colors,
animals,
} from "unique-names-generator";
const elements = [
"hydrogen",
"helium",
"lithium",
"beryllium",
"boron",
"carbon",
"nitrogen",
"oxygen",
"fluorine",
"neon",
"sodium",
"magnesium",
"aluminum",
"silicon",
"phosphorus",
"sulfur",
"chlorine",
"argon",
"potassium",
"calcium",
"scandium",
"titanium",
"vanadium",
"chromium",
"manganese",
"iron",
"cobalt",
"nickel",
"copper",
"zinc",
"gallium",
"germanium",
"arsenic",
"selenium",
"bromine",
"krypton",
"rubidium",
"strontium",
"yttrium",
"zirconium",
"niobium",
"molybdenum",
"technetium",
"ruthenium",
"rhodium",
"palladium",
"silver",
"cadmium",
"indium",
"tin",
"antimony",
"tellurium",
"iodine",
"xenon",
"cesium",
"barium",
"lanthanum",
"cerium",
"praseodymium",
"neodymium",
"promethium",
"samarium",
"europium",
"gadolinium",
"terbium",
"dysprosium",
"holmium",
"erbium",
"thulium",
"ytterbium",
"lutetium",
"hafnium",
"tantalum",
"wolfram",
"rhenium",
"osmium",
"iridium",
"platinum",
"gold",
"mercury",
"thallium",
"lead",
"bismuth",
"polonium",
"astatine",
"radon",
"francium",
"radium",
"actinium",
"thorium",
"protactinium",
"uranium",
"neptunium",
"plutonium",
"americium",
"curium",
"berkelium",
"californium",
"einsteinium",
"fermium",
"mendelevium",
"nobelium",
"lawrencium",
"rutherfordium",
"dubnium",
"seaborgium",
"bohrium",
"hassium",
"meitnerium",
"darmstadtium",
"roentgenium",
"copernicium",
"nihonium",
"flerovium",
"moscovium",
"livermorium",
"tennessine",
"oganesson",
];
export function generateRandomName(config) {
return uniqueNamesGenerator({
dictionaries: [colors, adjectives, animals, elements],
style: "lowerCase",
length: 3,
separator: "-",
...config,
});
}

View File

@@ -25,7 +25,13 @@ export function useInteractiveRegistration() {
}, []);
const register = useCallback(
async (username, password, recaptchaResponse, passwordlessUser) => {
async (
username,
password,
displayName,
recaptchaResponse,
passwordlessUser
) => {
const interactiveAuth = new InteractiveAuth({
matrixClient: authClientRef.current,
busyChanged(loading) {
@@ -66,7 +72,7 @@ export function useInteractiveRegistration() {
deviceId: device_id,
});
await client.setDisplayName(username);
await client.setDisplayName(displayName);
const session = { user_id, device_id, access_token, passwordlessUser };

View File

@@ -14,6 +14,7 @@ import { TooltipTrigger } from "../Tooltip";
export const variantToClassName = {
default: [styles.button],
toolbar: [styles.toolbarButton],
toolbarSecondary: [styles.toolbarButtonSecondary],
icon: [styles.iconButton],
secondary: [styles.secondary],
copy: [styles.copyButton],
@@ -103,7 +104,7 @@ export function VideoButton({ muted, ...rest }) {
export function ScreenshareButton({ enabled, className, ...rest }) {
return (
<TooltipTrigger>
<Button variant="toolbar" {...rest} on={enabled}>
<Button variant="toolbarSecondary" {...rest} on={enabled}>
<ScreenshareIcon />
</Button>
{() => (enabled ? "Stop sharing screen" : "Share screen")}

View File

@@ -16,6 +16,7 @@ limitations under the License.
.button,
.toolbarButton,
.toolbarButtonSecondary,
.iconButton,
.iconCopyButton,
.secondary,
@@ -48,6 +49,7 @@ limitations under the License.
.button:focus,
.toolbarButton:focus,
.toolbarButtonSecondary:focus,
.iconButton:focus,
.iconCopyButton:focus,
.secondary:focus,
@@ -55,14 +57,16 @@ limitations under the License.
outline: auto;
}
.toolbarButton {
.toolbarButton,
.toolbarButtonSecondary {
width: 50px;
height: 50px;
border-radius: 50px;
background-color: var(--bgColor2);
}
.toolbarButton:hover {
.toolbarButton:hover,
.toolbarButtonSecondary:hover {
background-color: var(--bgColor4);
}
@@ -71,6 +75,10 @@ limitations under the License.
background-color: #ffffff;
}
.toolbarButtonSecondary.on {
background-color: #0dbd8b;
}
.iconButton:not(.stroke) svg * {
fill: #ffffff;
}
@@ -100,6 +108,10 @@ limitations under the License.
fill: #21262c;
}
.toolbarButtonSecondary.on svg * {
fill: #ffffff;
}
.secondary,
.copyButton {
color: #0dbd8b;

View File

@@ -3,7 +3,6 @@ import { Link } from "react-router-dom";
import { CopyButton } from "../button";
import { Facepile } from "../Facepile";
import { Avatar } from "../Avatar";
import { ReactComponent as VideoIcon } from "../icons/Video.svg";
import styles from "./CallList.module.css";
import { getRoomUrl } from "../matrix-utils";
import { Body, Caption } from "../typography/Typography";

View File

@@ -22,6 +22,8 @@
display: flex;
text-decoration: none;
width: 100%;
height: 100%;
align-items: center;
}
.avatar,

View File

@@ -17,6 +17,7 @@ import { Form } from "../form/Form";
export function RegisteredView({ client }) {
const [loading, setLoading] = useState(false);
const [error, setError] = useState();
const history = useHistory();
const onSubmit = useCallback(
(e) => {
e.preventDefault();
@@ -55,7 +56,6 @@ export function RegisteredView({ client }) {
const { modalState, modalProps } = useModalTriggerState();
const [existingRoomId, setExistingRoomId] = useState();
const history = useHistory();
const onJoinExistingRoom = useCallback(() => {
history.push(`/${existingRoomId}`);
}, [history, existingRoomId]);

View File

@@ -14,6 +14,7 @@ import { Body, Caption, Link, Headline } from "../typography/Typography";
import { Form } from "../form/Form";
import styles from "./UnauthenticatedView.module.css";
import commonStyles from "./common.module.css";
import { generateRandomName } from "../auth/generateRandomName";
export function UnauthenticatedView() {
const [loading, setLoading] = useState(false);
@@ -26,19 +27,20 @@ export function UnauthenticatedView() {
e.preventDefault();
const data = new FormData(e.target);
const roomName = data.get("callName");
const userName = data.get("userName");
const displayName = data.get("displayName");
async function submit() {
setError(undefined);
setLoading(true);
const recaptchaResponse = await execute();
const userName = generateRandomName();
const client = await register(
userName,
randomString(16),
displayName,
recaptchaResponse,
true
);
const roomIdOrAlias = await createRoom(client, roomName);
if (roomIdOrAlias) {
@@ -100,10 +102,10 @@ export function UnauthenticatedView() {
</FieldRow>
<FieldRow>
<InputField
id="userName"
name="userName"
label="Username"
placeholder="Username"
id="displayName"
name="displayName"
label="Display Name"
placeholder="Display Name"
type="text"
required
autoComplete="off"

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

@@ -38,14 +38,25 @@ export const InputField = forwardRef(
)}
>
{prefix && <span>{prefix}</span>}
<input
id={id}
{...rest}
ref={ref}
type={type}
checked={checked}
disabled={disabled}
/>
{type === "textarea" ? (
<textarea
id={id}
{...rest}
ref={ref}
type={type}
disabled={disabled}
/>
) : (
<input
id={id}
{...rest}
ref={ref}
type={type}
checked={checked}
disabled={disabled}
/>
)}
<label htmlFor={id}>
{type === "checkbox" && (
<div className={styles.checkbox}>

View File

@@ -29,7 +29,8 @@
border: 1px solid var(--inputBorderColor);
}
.inputField input {
.inputField input,
.inputField textarea {
font-weight: 400;
font-size: 15px;
border: none;
@@ -42,6 +43,7 @@
}
.inputField.disabled input,
.inputField.disabled textarea,
.inputField.disabled span {
color: var(--textColor2);
}
@@ -54,12 +56,14 @@
padding-right: 0;
}
.inputField input::placeholder {
.inputField input::placeholder,
.inputField textarea::placeholder {
transition: color 0.25s ease-in 0s;
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;
color: var(--textColor2);
}
@@ -86,13 +90,17 @@
border-color: var(--inputBorderColorFocused);
}
.inputField input:focus {
.inputField input:focus,
.inputField textarea:focus {
outline: 0;
}
.inputField input:focus + 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);
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;
@@ -102,7 +110,8 @@
pointer-events: auto;
}
.inputField input:focus + label {
.inputField input:focus + label,
.inputField textarea:focus + label {
color: var(--inputBorderColorFocused);
}

View File

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

View File

@@ -50,6 +50,31 @@ export function roomAliasFromRoomName(roomName) {
.toLowerCase();
}
export function roomNameFromRoomId(roomId) {
return roomId
.match(/([^:]+):.*$/)[1]
.substring(1)
.split("-")
.map((part) =>
part.length > 0 ? part.charAt(0).toUpperCase() + part.slice(1) : part
)
.join(" ");
}
export function isLocalRoomId(roomId) {
if (!roomId) {
return false;
}
const parts = roomId.match(/[^:]+:(.*)$/);
if (parts.length < 2) {
return false;
}
return parts[1] === defaultHomeserverHost;
}
export async function createRoom(client, name) {
const { room_id, room_alias } = await client.createRoom({
visibility: "private",
@@ -95,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}`;
}
}

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

@@ -9,7 +9,7 @@ export function useProfile(client) {
return {
success: false,
loading: false,
displayName: user?.displayName,
displayName: user?.rawDisplayName,
avatarUrl: user && client && getAvatarUrl(client, user.avatarUrl),
error: null,
};
@@ -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

@@ -22,6 +22,7 @@ export function FeedbackModal({ inCall, roomId, ...rest }) {
description,
sendLogs,
rageshakeRequestId,
roomId,
});
if (inCall && sendLogs) {
@@ -47,7 +48,7 @@ export function FeedbackModal({ inCall, roomId, ...rest }) {
id="description"
name="description"
label="Description (optional)"
type="text"
type="textarea"
/>
</FieldRow>
<FieldRow>

View File

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

View File

@@ -2,9 +2,11 @@ import React from "react";
import { useLoadGroupCall } from "./useLoadGroupCall";
import { ErrorView, FullScreenView } from "../FullScreenView";
import { usePageTitle } from "../usePageTitle";
import { isLocalRoomId } from "../matrix-utils";
import { RoomNotFoundView } from "./RoomNotFoundView";
export function GroupCallLoader({ client, roomId, viaServers, children }) {
const { loading, error, groupCall } = useLoadGroupCall(
const { loading, error, groupCall, reload } = useLoadGroupCall(
client,
roomId,
viaServers
@@ -20,6 +22,18 @@ export function GroupCallLoader({ client, roomId, viaServers, children }) {
);
}
if (
error &&
(error.errcode === "M_NOT_FOUND" ||
(error.message &&
error.message.indexOf("Failed to fetch alias") !== -1)) &&
isLocalRoomId(roomId)
) {
return (
<RoomNotFoundView client={client} roomId={roomId} onReload={reload} />
);
}
if (error) {
return <ErrorView error={error} />;
}

View File

@@ -21,6 +21,8 @@ import { Avatar } from "../Avatar";
import { UserMenuContainer } from "../UserMenuContainer";
import { useRageshakeRequestModal } from "../settings/rageshake";
import { RageshakeRequestModal } from "./RageshakeRequestModal";
import { usePreventScroll } from "@react-aria/overlays";
import { useMediaHandler } from "../settings/useMediaHandler";
const canScreenshare = "getDisplayMedia" in navigator.mediaDevices;
// There is currently a bug in Safari our our code with cloning and sending MediaStreams
@@ -47,7 +49,10 @@ export function InCallView({
showInspector,
roomId,
}) {
const [layout, setLayout] = useVideoGridLayout();
usePreventScroll();
const [layout, setLayout] = useVideoGridLayout(screenshareFeeds.length > 0);
const { audioOutput } = useMediaHandler();
const items = useMemo(() => {
const participants = [];
@@ -57,9 +62,10 @@ export function InCallView({
id: callFeed.stream.id,
callFeed,
focused:
screenshareFeeds.length === 0
screenshareFeeds.length === 0 && layout === "spotlight"
? callFeed.userId === activeSpeaker
: false,
isLocal: callFeed.isLocal(),
});
}
@@ -76,18 +82,19 @@ export function InCallView({
id: callFeed.stream.id,
callFeed,
focused: true,
isLocal: callFeed.isLocal(),
});
}
return participants;
}, [userMediaFeeds, activeSpeaker, screenshareFeeds]);
}, [userMediaFeeds, activeSpeaker, screenshareFeeds, layout]);
const onFocusTile = useCallback(
(tiles, focusedTile) => {
if (layout === "freedom") {
return tiles.map((tile) => {
if (tile === focusedTile) {
return { ...tile, presenter: !tile.presenter };
return { ...tile, focused: !tile.focused };
}
return tile;
@@ -156,6 +163,9 @@ export function InCallView({
key={item.id}
item={item}
getAvatar={renderAvatar}
showName={items.length > 2 || item.focused}
audioOutputDevice={audioOutput}
disableSpeakingIndicator={items.length < 3}
{...rest}
/>
)}
@@ -186,7 +196,10 @@ export function InCallView({
show={showInspector}
/>
{rageshakeRequestModalState.isOpen && (
<RageshakeRequestModal {...rageshakeRequestModalProps} />
<RageshakeRequestModal
{...rageshakeRequestModalProps}
roomId={roomId}
/>
)}
</div>
);

View File

@@ -10,11 +10,11 @@ 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";
import { useLocationNavigation } from "../useLocationNavigation";
import { useMediaHandler } from "../settings/useMediaHandler";
export function LobbyView({
client,
@@ -32,7 +32,8 @@ export function LobbyView({
roomId,
}) {
const { stream } = useCallFeed(localCallFeed);
const videoRef = useMediaStream(stream, true);
const { audioOutput } = useMediaHandler();
const videoRef = useMediaStream(stream, audioOutput, true);
const { displayName, avatarUrl } = useProfile(client);
const [previewRef, previewBounds] = useMeasure({ polyfill: ResizeObserver });
const avatarSize = (previewBounds.height - 66) / 2;
@@ -86,7 +87,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>

View File

@@ -17,7 +17,6 @@ export function OverflowMenu({
roomId,
setShowInspector,
showInspector,
client,
inCall,
groupCall,
}) {
@@ -75,7 +74,6 @@ export function OverflowMenu({
{...settingsModalProps}
setShowInspector={setShowInspector}
showInspector={showInspector}
client={client}
/>
)}
{inviteModalState.isOpen && (

View File

@@ -5,7 +5,7 @@ import { FieldRow, ErrorMessage } from "../input/Input";
import { useSubmitRageshake } from "../settings/rageshake";
import { Body } from "../typography/Typography";
export function RageshakeRequestModal({ rageshakeRequestId, ...rest }) {
export function RageshakeRequestModal({ rageshakeRequestId, roomId, ...rest }) {
const { submitRageshake, sending, sent, error } = useSubmitRageshake();
useEffect(() => {
@@ -27,6 +27,7 @@ export function RageshakeRequestModal({ rageshakeRequestId, ...rest }) {
submitRageshake({
sendLogs: true,
rageshakeRequestId,
roomId,
})
}
disabled={sending}

View File

@@ -10,6 +10,7 @@ import { randomString } from "matrix-js-sdk/src/randomstring";
import { useInteractiveRegistration } from "../auth/useInteractiveRegistration";
import { Form } from "../form/Form";
import { UserMenuContainer } from "../UserMenuContainer";
import { generateRandomName } from "../auth/generateRandomName";
export function RoomAuthView() {
const [loading, setLoading] = useState(false);
@@ -21,13 +22,20 @@ export function RoomAuthView() {
(e) => {
e.preventDefault();
const data = new FormData(e.target);
const userName = data.get("userName");
const displayName = data.get("displayName");
async function submit() {
setError(undefined);
setLoading(true);
const recaptchaResponse = await execute();
await register(userName, randomString(16), recaptchaResponse, true);
const userName = generateRandomName();
await register(
userName,
randomString(16),
displayName,
recaptchaResponse,
true
);
}
submit().catch((error) => {
@@ -58,10 +66,10 @@ export function RoomAuthView() {
<Form className={styles.form} onSubmit={onSubmit}>
<FieldRow>
<InputField
id="userName"
name="userName"
label="Pick a user name"
placeholder="Pick a user name"
id="displayName"
name="displayName"
label="Display Name"
placeholder="Display Name"
type="text"
required
autoComplete="off"

View File

@@ -0,0 +1,74 @@
import React, { useState, useCallback } from "react";
import { FullScreenView } from "../FullScreenView";
import { Headline, Subtitle } from "../typography/Typography";
import { createRoom, roomNameFromRoomId } from "../matrix-utils";
import { FieldRow, ErrorMessage, InputField } from "../input/Input";
import { Button } from "../button";
import { Form } from "../form/Form";
import { useHistory } from "react-router-dom";
import styles from "./RoomNotFoundView.module.css";
export function RoomNotFoundView({ client, roomId, onReload }) {
const history = useHistory();
const [loading, setLoading] = useState(false);
const [error, setError] = useState();
const roomName = roomNameFromRoomId(roomId);
const onSubmit = useCallback(
(e) => {
e.preventDefault();
async function submit() {
setError(undefined);
setLoading(true);
await createRoom(client, roomName);
onReload();
}
submit().catch((error) => {
console.error(error);
setLoading(false);
setError(error);
});
},
[client, roomName]
);
return (
<FullScreenView>
<Headline>Call Not Found</Headline>
<Subtitle>Would you like to create this call?</Subtitle>
<Form onSubmit={onSubmit} className={styles.form}>
<FieldRow>
<InputField
id="callName"
name="callName"
label="Call name"
placeholder="Call name"
type="text"
required
autoComplete="off"
value={roomName}
disabled
/>
</FieldRow>
<FieldRow>
<Button
type="submit"
size="lg"
disabled={loading}
className={styles.button}
>
{loading ? "Loading..." : "Create Room"}
</Button>
</FieldRow>
{error && (
<FieldRow>
<ErrorMessage>{error.message}</ErrorMessage>
</FieldRow>
)}
</Form>
</FullScreenView>
);
}

View File

@@ -0,0 +1,11 @@
.form {
padding: 0 24px;
justify-content: center;
max-width: 409px;
width: calc(100% - 48px);
margin-bottom: 72px;
}
.button {
width: 100%;
}

View File

@@ -21,6 +21,7 @@ import { ErrorView, LoadingView } from "../FullScreenView";
import { RoomAuthView } from "./RoomAuthView";
import { GroupCallLoader } from "./GroupCallLoader";
import { GroupCallView } from "./GroupCallView";
import { MediaHandlerProvider } from "../settings/useMediaHandler";
export function RoomPage() {
const { loading, isAuthenticated, error, client, isPasswordlessUser } =
@@ -32,7 +33,7 @@ export function RoomPage() {
const params = new URLSearchParams(search);
return [params.has("simple"), params.getAll("via")];
}, [search]);
const roomId = maybeRoomId || hash;
const roomId = (maybeRoomId || hash || "").toLowerCase();
if (loading) {
return <LoadingView />;
@@ -47,16 +48,18 @@ export function RoomPage() {
}
return (
<GroupCallLoader client={client} roomId={roomId} viaServers={viaServers}>
{(groupCall) => (
<GroupCallView
client={client}
roomId={roomId}
groupCall={groupCall}
isPasswordlessUser={isPasswordlessUser}
simpleGrid={simpleGrid}
/>
)}
</GroupCallLoader>
<MediaHandlerProvider client={client}>
<GroupCallLoader client={client} roomId={roomId} viaServers={viaServers}>
{(groupCall) => (
<GroupCallView
client={client}
roomId={roomId}
groupCall={groupCall}
isPasswordlessUser={isPasswordlessUser}
simpleGrid={simpleGrid}
/>
)}
</GroupCallLoader>
</MediaHandlerProvider>
);
}

View File

@@ -11,7 +11,7 @@ export function RoomRedirect() {
let roomId = pathname;
if (pathname.startsWith("/")) {
roomId = roomId.substr(1, roomId.length);
roomId = roomId.substring(1, roomId.length);
}
if (!roomId.startsWith("#") && !roomId.startsWith("!")) {

View File

@@ -1,4 +1,4 @@
import { useState, useEffect } from "react";
import { useState, useEffect, useCallback } from "react";
async function fetchGroupCall(
client,
@@ -41,14 +41,23 @@ export function useLoadGroupCall(client, roomId, viaServers) {
loading: true,
error: undefined,
groupCall: undefined,
reloadId: 0,
});
useEffect(() => {
setState({ loading: true });
fetchGroupCall(client, roomId, viaServers, 30000)
.then((groupCall) => setState({ loading: false, groupCall }))
.catch((error) => setState({ loading: false, error }));
}, [client, roomId]);
.then((groupCall) =>
setState((prevState) => ({ ...prevState, loading: false, groupCall }))
)
.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 };
}

View File

@@ -11,13 +11,9 @@ 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,
setShowInspector,
showInspector,
...rest
}) {
export function SettingsModal({ setShowInspector, showInspector, ...rest }) {
const {
audioInput,
audioInputs,
@@ -25,7 +21,10 @@ export function SettingsModal({
videoInput,
videoInputs,
setVideoInput,
} = useMediaHandler(client);
audioOutput,
audioOutputs,
setAudioOutput,
} = useMediaHandler();
const downloadDebugLog = useDownloadDebugLog();
@@ -55,6 +54,17 @@ export function SettingsModal({
<Item key={deviceId}>{label}</Item>
))}
</SelectInput>
{audioOutputs.length > 0 && (
<SelectInput
label="Speaker"
selectedKey={audioOutput}
onSelectionChange={setAudioOutput}
>
{audioOutputs.map(({ deviceId, label }) => (
<Item key={deviceId}>{label}</Item>
))}
</SelectInput>
)}
</TabItem>
<TabItem
title={
@@ -82,6 +92,11 @@ export function SettingsModal({
</>
}
>
<FieldRow>
<Body className={styles.fieldRowText}>
Version: {import.meta.env.VITE_APP_VERSION || "dev"}
</Body>
</FieldRow>
<FieldRow>
<InputField
id="showInspector"

View File

@@ -6,3 +6,7 @@
.tabContainer {
margin: 27px 16px;
}
.fieldRowText {
margin-bottom: 0;
}

View File

@@ -41,15 +41,22 @@ 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);
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("device_id", client.deviceId);
if (opts.roomId) {
body.append("room_id", opts.roomId);
}
if (client.isCryptoEnabled()) {
const keys = [`ed25519:${client.getDeviceEd25519Key()}`];
if (client.getDeviceCurve25519Key) {

View File

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

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

View File

@@ -15,7 +15,7 @@ export default {
};
export const ParticipantsTest = () => {
const [layout, setLayout] = useVideoGridLayout();
const [layout, setLayout] = useVideoGridLayout(false);
const [participantCount, setParticipantCount] = useState(1);
const items = useMemo(
@@ -60,7 +60,13 @@ export const ParticipantsTest = () => {
>
<VideoGrid layout={layout} items={items}>
{({ item, ...rest }) => (
<VideoTile key={item.id} name={`User ${item.id}`} {...rest} />
<VideoTile
key={item.id}
name={`User ${item.id}`}
showName={items.length > 2 || item.focused}
disableSpeakingIndicator={items.length < 3}
{...rest}
/>
)}
</VideoGrid>
</div>

View File

@@ -12039,6 +12039,11 @@ unique-filename@^1.1.1:
dependencies:
unique-slug "^2.0.0"
unique-names-generator@^4.6.0:
version "4.6.0"
resolved "https://registry.yarnpkg.com/unique-names-generator/-/unique-names-generator-4.6.0.tgz#852c1db8149815d6cf665a601820fe80ec2fbc37"
integrity sha512-m0fke1emBeT96UYn2psPQYwljooDWRTKt9oUZ5vlt88ZFMBGxqwPyLHXwCfkbgdm8jzioCp7oIpo6KdM+fnUlQ==
unique-slug@^2.0.0:
version "2.0.2"
resolved "https://registry.yarnpkg.com/unique-slug/-/unique-slug-2.0.2.tgz#baabce91083fc64e945b0f3ad613e264f7cd4e6c"