Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
81a763f17f | ||
|
|
1ab7d27ba9 | ||
|
|
e76a805c8f | ||
|
|
9fc4af2bd7 | ||
|
|
0f3a7f9fd9 | ||
|
|
1cc634509b | ||
|
|
cb07ce32cb | ||
|
|
6866d662f7 | ||
|
|
51a2027d64 | ||
|
|
0f6b8f9bb1 | ||
|
|
63229ce2d7 | ||
|
|
1d620910c5 | ||
|
|
47357b3fc6 | ||
|
|
3ed35f9477 | ||
|
|
a369444b62 | ||
|
|
742d658021 | ||
|
|
681c24a0ca |
@@ -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",
|
||||
|
||||
@@ -24,6 +24,9 @@ yarn link
|
||||
cd ..
|
||||
|
||||
cd matrix-video-chat
|
||||
|
||||
export VITE_APP_VERSION=$(git describe --tags --abbrev=0)
|
||||
|
||||
yarn link matrix-js-sdk
|
||||
yarn link matrix-react-sdk
|
||||
yarn install
|
||||
|
||||
@@ -56,4 +56,5 @@
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
border-radius: 90px;
|
||||
font-size: 48px;
|
||||
}
|
||||
|
||||
@@ -145,29 +145,36 @@ 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");
|
||||
@@ -175,12 +182,21 @@ export function ClientProvider({ children }) {
|
||||
}, [history]);
|
||||
|
||||
useEffect(() => {
|
||||
if ("BroadcastChannel" in window) {
|
||||
if (client) {
|
||||
const loadTime = Date.now();
|
||||
const broadcastChannel = new BroadcastChannel("matrix-video-chat");
|
||||
|
||||
function onMessage({ data }) {
|
||||
if (data.load !== undefined && data.load > loadTime) {
|
||||
const onToDeviceEvent = (event) => {
|
||||
if (event.getType() !== "org.matrix.call_duplicate_session") {
|
||||
return;
|
||||
}
|
||||
|
||||
const content = event.getContent();
|
||||
|
||||
if (content.session_id === client.getSessionId()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (content.timestamp > loadTime) {
|
||||
if (client) {
|
||||
client.stopClient();
|
||||
}
|
||||
@@ -192,13 +208,18 @@ export function ClientProvider({ children }) {
|
||||
),
|
||||
}));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
broadcastChannel.addEventListener("message", onMessage);
|
||||
broadcastChannel.postMessage({ load: loadTime });
|
||||
client.on("toDeviceEvent", onToDeviceEvent);
|
||||
|
||||
client.sendToDevice("org.matrix.call_duplicate_session", {
|
||||
[client.getUserId()]: {
|
||||
"*": { session_id: client.getSessionId(), timestamp: loadTime },
|
||||
},
|
||||
});
|
||||
|
||||
return () => {
|
||||
broadcastChannel.removeEventListener("message", onMessage);
|
||||
client.removeListener("toDeviceEvent", onToDeviceEvent);
|
||||
};
|
||||
}
|
||||
}, [client]);
|
||||
@@ -226,6 +247,10 @@ export function ClientProvider({ children }) {
|
||||
]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
window.matrixclient = client;
|
||||
}, [client]);
|
||||
|
||||
if (error) {
|
||||
return <ErrorView error={error} />;
|
||||
}
|
||||
|
||||
@@ -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} />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
137
src/auth/generateRandomName.js
Normal file
137
src/auth/generateRandomName.js
Normal 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,
|
||||
});
|
||||
}
|
||||
@@ -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 };
|
||||
|
||||
|
||||
@@ -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")}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -22,6 +22,8 @@
|
||||
display: flex;
|
||||
text-decoration: none;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.avatar,
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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
4
src/icons/Edit.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M2.64856 7.35501C2.65473 7.31601 2.67231 7.27972 2.69908 7.25069L8.40377 1.06442C8.47865 0.983217 8.60518 0.978093 8.68638 1.05297L9.8626 2.13763C9.9438 2.21251 9.94893 2.33904 9.87405 2.42024L4.16936 8.60651C4.1426 8.63554 4.10783 8.656 4.06946 8.6653L2.66781 9.00511C2.52911 9.03873 2.40084 8.92044 2.42315 8.77948L2.64856 7.35501Z" fill="white"/>
|
||||
<path d="M1.75 9.44346C1.33579 9.44346 1 9.77925 1 10.1935C1 10.6077 1.33579 10.9435 1.75 10.9435L10.75 10.9435C11.1642 10.9435 11.5 10.6077 11.5 10.1935C11.5 9.77925 11.1642 9.44346 10.75 9.44346L1.75 9.44346Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 689 B |
78
src/input/AvatarInputField.jsx
Normal file
78
src/input/AvatarInputField.jsx
Normal file
@@ -0,0 +1,78 @@
|
||||
import { useObjectRef } from "@react-aria/utils";
|
||||
import React, { useEffect } from "react";
|
||||
import { useCallback } from "react";
|
||||
import { useState } from "react";
|
||||
import { forwardRef } from "react";
|
||||
import { Avatar } from "../Avatar";
|
||||
import { Button } from "../button";
|
||||
import classNames from "classnames";
|
||||
import { ReactComponent as EditIcon } from "../icons/Edit.svg";
|
||||
import styles from "./AvatarInputField.module.css";
|
||||
|
||||
export const AvatarInputField = forwardRef(
|
||||
(
|
||||
{ id, label, className, avatarUrl, displayName, onRemoveAvatar, ...rest },
|
||||
ref
|
||||
) => {
|
||||
const [removed, setRemoved] = useState(false);
|
||||
const [objUrl, setObjUrl] = useState(null);
|
||||
|
||||
const fileInputRef = useObjectRef(ref);
|
||||
|
||||
useEffect(() => {
|
||||
const onChange = (e) => {
|
||||
if (e.target.files.length > 0) {
|
||||
setObjUrl(URL.createObjectURL(e.target.files[0]));
|
||||
setRemoved(false);
|
||||
} else {
|
||||
setObjUrl(null);
|
||||
}
|
||||
};
|
||||
|
||||
fileInputRef.current.addEventListener("change", onChange);
|
||||
|
||||
return () => {
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.removeEventListener("change", onChange);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
const onPressRemoveAvatar = useCallback(() => {
|
||||
setRemoved(true);
|
||||
onRemoveAvatar();
|
||||
}, [onRemoveAvatar]);
|
||||
|
||||
return (
|
||||
<div className={classNames(styles.avatarInputField, className)}>
|
||||
<div className={styles.avatarContainer}>
|
||||
<Avatar
|
||||
size="xl"
|
||||
src={removed ? null : objUrl || avatarUrl}
|
||||
fallback={displayName.slice(0, 1).toUpperCase()}
|
||||
/>
|
||||
<input
|
||||
id={id}
|
||||
accept="image/png, image/jpeg"
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
className={styles.fileInput}
|
||||
role="button"
|
||||
aria-label={label}
|
||||
{...rest}
|
||||
/>
|
||||
<label htmlFor={id} className={styles.fileInputButton}>
|
||||
<EditIcon />
|
||||
</label>
|
||||
</div>
|
||||
<Button
|
||||
className={styles.removeButton}
|
||||
variant="icon"
|
||||
onPress={onPressRemoveAvatar}
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
41
src/input/AvatarInputField.module.css
Normal file
41
src/input/AvatarInputField.module.css
Normal file
@@ -0,0 +1,41 @@
|
||||
.avatarInputField {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.avatarContainer {
|
||||
position: relative;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.fileInput {
|
||||
width: 0.1px;
|
||||
height: 0.1px;
|
||||
opacity: 0;
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.fileInput:focus + .fileInputButton {
|
||||
outline: auto;
|
||||
}
|
||||
|
||||
.fileInputButton {
|
||||
position: absolute;
|
||||
bottom: 11px;
|
||||
right: -4px;
|
||||
background-color: var(--bgColor4);
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.removeButton {
|
||||
color: #0dbd8b;
|
||||
}
|
||||
@@ -27,6 +27,8 @@ import { InspectorContextProvider } from "./room/GroupCallInspector";
|
||||
|
||||
rageshake.init();
|
||||
|
||||
console.info(`matrix-video-chat ${import.meta.env.VITE_APP_VERSION || "dev"}`);
|
||||
|
||||
if (import.meta.env.VITE_CUSTOM_THEME) {
|
||||
const style = document.documentElement.style;
|
||||
style.setProperty("--primaryColor", import.meta.env.VITE_PRIMARY_COLOR);
|
||||
|
||||
@@ -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}`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,22 +3,25 @@ import { Button } from "../button";
|
||||
import { useProfile } from "./useProfile";
|
||||
import { FieldRow, InputField, ErrorMessage } from "../input/Input";
|
||||
import { Modal, ModalContent } from "../Modal";
|
||||
import { AvatarInputField } from "../input/AvatarInputField";
|
||||
import styles from "./ProfileModal.module.css";
|
||||
|
||||
export function ProfileModal({
|
||||
client,
|
||||
isAuthenticated,
|
||||
isPasswordlessUser,
|
||||
...rest
|
||||
}) {
|
||||
export function ProfileModal({ client, ...rest }) {
|
||||
const { onClose } = rest;
|
||||
const {
|
||||
success,
|
||||
error,
|
||||
loading,
|
||||
displayName: initialDisplayName,
|
||||
avatarUrl,
|
||||
saveProfile,
|
||||
} = useProfile(client);
|
||||
const [displayName, setDisplayName] = useState(initialDisplayName || "");
|
||||
const [removeAvatar, setRemoveAvatar] = useState(false);
|
||||
|
||||
const onRemoveAvatar = useCallback(() => {
|
||||
setRemoveAvatar(true);
|
||||
}, []);
|
||||
|
||||
const onChangeDisplayName = useCallback(
|
||||
(e) => {
|
||||
@@ -37,9 +40,10 @@ export function ProfileModal({
|
||||
saveProfile({
|
||||
displayName,
|
||||
avatar: avatar && avatar.size > 0 ? avatar : undefined,
|
||||
removeAvatar: removeAvatar && (!avatar || avatar.size === 0),
|
||||
});
|
||||
},
|
||||
[saveProfile]
|
||||
[saveProfile, removeAvatar]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -52,6 +56,16 @@ export function ProfileModal({
|
||||
<Modal title="Profile" isDismissable {...rest}>
|
||||
<ModalContent>
|
||||
<form onSubmit={onSubmit}>
|
||||
<FieldRow className={styles.avatarFieldRow}>
|
||||
<AvatarInputField
|
||||
id="avatar"
|
||||
name="avatar"
|
||||
label="Avatar"
|
||||
avatarUrl={avatarUrl}
|
||||
displayName={displayName}
|
||||
onRemoveAvatar={onRemoveAvatar}
|
||||
/>
|
||||
</FieldRow>
|
||||
<FieldRow>
|
||||
<InputField
|
||||
id="userId"
|
||||
@@ -75,16 +89,6 @@ export function ProfileModal({
|
||||
onChange={onChangeDisplayName}
|
||||
/>
|
||||
</FieldRow>
|
||||
{isAuthenticated && (
|
||||
<FieldRow>
|
||||
<InputField
|
||||
type="file"
|
||||
id="avatar"
|
||||
name="avatar"
|
||||
label="Avatar"
|
||||
/>
|
||||
</FieldRow>
|
||||
)}
|
||||
{error && (
|
||||
<FieldRow>
|
||||
<ErrorMessage>{error.message}</ErrorMessage>
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
.avatarFieldRow {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ export function useProfile(client) {
|
||||
}, [client]);
|
||||
|
||||
const saveProfile = useCallback(
|
||||
async ({ displayName, avatar }) => {
|
||||
async ({ displayName, avatar, removeAvatar }) => {
|
||||
if (client) {
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
@@ -58,7 +58,9 @@ export function useProfile(client) {
|
||||
|
||||
let mxcAvatarUrl;
|
||||
|
||||
if (avatar) {
|
||||
if (removeAvatar) {
|
||||
await client.setAvatarUrl("");
|
||||
} else if (avatar) {
|
||||
mxcAvatarUrl = await client.uploadContent(avatar);
|
||||
await client.setAvatarUrl(mxcAvatarUrl);
|
||||
}
|
||||
@@ -66,7 +68,9 @@ export function useProfile(client) {
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
displayName,
|
||||
avatarUrl: mxcAvatarUrl
|
||||
avatarUrl: removeAvatar
|
||||
? null
|
||||
: mxcAvatarUrl
|
||||
? getAvatarUrl(client, mxcAvatarUrl)
|
||||
: prev.avatarUrl,
|
||||
loading: false,
|
||||
|
||||
@@ -59,8 +59,8 @@ function getUserName(userId) {
|
||||
const match = userId.match(/@([^\:]+):/);
|
||||
|
||||
return match && match.length > 0
|
||||
? match[1].replace("-", " ").replace("W", "")
|
||||
: userId.replace("W", "");
|
||||
? match[1].replace("-", " ").replace(/\W/g, "")
|
||||
: userId.replace(/\W/g, "");
|
||||
}
|
||||
|
||||
function formatContent(type, content) {
|
||||
@@ -231,7 +231,7 @@ function reducer(state, action) {
|
||||
),
|
||||
};
|
||||
}
|
||||
case "receive_to_device_event": {
|
||||
case "received_voip_event": {
|
||||
const event = action.event;
|
||||
const eventsByUserId = { ...state.eventsByUserId };
|
||||
const fromId = event.getSender();
|
||||
@@ -338,8 +338,8 @@ function useGroupCallState(client, groupCall, pollCallStats) {
|
||||
// dispatch({ type: "call_hangup", call });
|
||||
// }
|
||||
|
||||
function onToDeviceEvent(event) {
|
||||
dispatch({ type: "receive_to_device_event", event });
|
||||
function onReceivedVoipEvent(event) {
|
||||
dispatch({ type: "received_voip_event", event });
|
||||
}
|
||||
|
||||
function onSendVoipEvent(event) {
|
||||
@@ -351,7 +351,7 @@ function useGroupCallState(client, groupCall, pollCallStats) {
|
||||
groupCall.on("send_voip_event", onSendVoipEvent);
|
||||
//client.on("state", onCallsChanged);
|
||||
//client.on("hangup", onCallHangup);
|
||||
client.on("toDeviceEvent", onToDeviceEvent);
|
||||
client.on("received_voip_event", onReceivedVoipEvent);
|
||||
|
||||
onUpdateRoomState();
|
||||
|
||||
@@ -361,7 +361,7 @@ function useGroupCallState(client, groupCall, pollCallStats) {
|
||||
groupCall.removeListener("send_voip_event", onSendVoipEvent);
|
||||
//client.removeListener("state", onCallsChanged);
|
||||
//client.removeListener("hangup", onCallHangup);
|
||||
client.removeListener("toDeviceEvent", onToDeviceEvent);
|
||||
client.removeListener("received_voip_event", onReceivedVoipEvent);
|
||||
};
|
||||
}, [client, groupCall]);
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@ 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(
|
||||
@@ -20,6 +22,16 @@ 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} />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <ErrorView error={error} />;
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import { Avatar } from "../Avatar";
|
||||
import { UserMenuContainer } from "../UserMenuContainer";
|
||||
import { useRageshakeRequestModal } from "../settings/rageshake";
|
||||
import { RageshakeRequestModal } from "./RageshakeRequestModal";
|
||||
import { usePreventScroll } from "@react-aria/overlays";
|
||||
|
||||
const canScreenshare = "getDisplayMedia" in navigator.mediaDevices;
|
||||
// There is currently a bug in Safari our our code with cloning and sending MediaStreams
|
||||
@@ -47,7 +48,8 @@ export function InCallView({
|
||||
showInspector,
|
||||
roomId,
|
||||
}) {
|
||||
const [layout, setLayout] = useVideoGridLayout();
|
||||
usePreventScroll();
|
||||
const [layout, setLayout] = useVideoGridLayout(screenshareFeeds.length > 0);
|
||||
|
||||
const items = useMemo(() => {
|
||||
const participants = [];
|
||||
@@ -57,7 +59,7 @@ export function InCallView({
|
||||
id: callFeed.stream.id,
|
||||
callFeed,
|
||||
focused:
|
||||
screenshareFeeds.length === 0
|
||||
screenshareFeeds.length === 0 && layout === "spotlight"
|
||||
? callFeed.userId === activeSpeaker
|
||||
: false,
|
||||
});
|
||||
@@ -80,14 +82,14 @@ export function InCallView({
|
||||
}
|
||||
|
||||
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 +158,7 @@ export function InCallView({
|
||||
key={item.id}
|
||||
item={item}
|
||||
getAvatar={renderAvatar}
|
||||
showName={items.length > 2 || item.focused}
|
||||
{...rest}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -10,7 +10,6 @@ import { OverflowMenu } from "./OverflowMenu";
|
||||
import { UserMenuContainer } from "../UserMenuContainer";
|
||||
import { Body, Link } from "../typography/Typography";
|
||||
import { Avatar } from "../Avatar";
|
||||
import { getAvatarUrl } from "../matrix-utils";
|
||||
import { useProfile } from "../profile/useProfile";
|
||||
import useMeasure from "react-use-measure";
|
||||
import { ResizeObserver } from "@juggle/resize-observer";
|
||||
@@ -86,7 +85,7 @@ export function LobbyView({
|
||||
borderRadius: avatarSize,
|
||||
fontSize: Math.round(avatarSize / 2),
|
||||
}}
|
||||
src={avatarUrl && getAvatarUrl(client, avatarUrl, 96)}
|
||||
src={avatarUrl}
|
||||
fallback={displayName.slice(0, 1).toUpperCase()}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -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"
|
||||
|
||||
76
src/room/RoomNotFoundView.jsx
Normal file
76
src/room/RoomNotFoundView.jsx
Normal file
@@ -0,0 +1,76 @@
|
||||
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 }) {
|
||||
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);
|
||||
|
||||
const roomIdOrAlias = await createRoom(client, roomName);
|
||||
|
||||
if (roomIdOrAlias) {
|
||||
history.push(`/room/${roomIdOrAlias}`);
|
||||
}
|
||||
}
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
11
src/room/RoomNotFoundView.module.css
Normal file
11
src/room/RoomNotFoundView.module.css
Normal 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%;
|
||||
}
|
||||
@@ -32,7 +32,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 />;
|
||||
|
||||
@@ -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("!")) {
|
||||
|
||||
@@ -11,6 +11,7 @@ import { useMediaHandler } from "./useMediaHandler";
|
||||
import { FieldRow, InputField } from "../input/Input";
|
||||
import { Button } from "../button";
|
||||
import { useDownloadDebugLog } from "./rageshake";
|
||||
import { Body } from "../typography/Typography";
|
||||
|
||||
export function SettingsModal({
|
||||
client,
|
||||
@@ -82,6 +83,11 @@ export function SettingsModal({
|
||||
</>
|
||||
}
|
||||
>
|
||||
<FieldRow>
|
||||
<Body className={styles.fieldRowText}>
|
||||
Version: {import.meta.env.VITE_APP_VERSION || "dev"}
|
||||
</Body>
|
||||
</FieldRow>
|
||||
<FieldRow>
|
||||
<InputField
|
||||
id="showInspector"
|
||||
|
||||
@@ -6,3 +6,7 @@
|
||||
.tabContainer {
|
||||
margin: 27px 16px;
|
||||
}
|
||||
|
||||
.fieldRowText {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ export function useSubmitRageshake() {
|
||||
opts.description || "User did not supply any additional text."
|
||||
);
|
||||
body.append("app", "matrix-video-chat");
|
||||
body.append("version", "dev");
|
||||
body.append("version", import.meta.env.VITE_APP_VERSION || "dev");
|
||||
body.append("user_agent", userAgent);
|
||||
body.append("installed_pwa", false);
|
||||
body.append("touch_input", touchInput);
|
||||
|
||||
@@ -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,12 @@ 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}
|
||||
{...rest}
|
||||
/>
|
||||
)}
|
||||
</VideoGrid>
|
||||
</div>
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user