Compare commits
29 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9900d661be | ||
|
|
369b59a203 | ||
|
|
6a18ba0110 | ||
|
|
0a49ddb31e | ||
|
|
25385edf12 | ||
|
|
721cccf152 | ||
|
|
3b017eb92b | ||
|
|
641b82dc45 | ||
|
|
42e2041d6f | ||
|
|
2c3ebd4c03 | ||
|
|
81a763f17f | ||
|
|
1ab7d27ba9 | ||
|
|
e76a805c8f | ||
|
|
9fc4af2bd7 | ||
|
|
0f3a7f9fd9 | ||
|
|
1cc634509b | ||
|
|
cb07ce32cb | ||
|
|
6866d662f7 | ||
|
|
51a2027d64 | ||
|
|
0f6b8f9bb1 | ||
|
|
63229ce2d7 | ||
|
|
1d620910c5 | ||
|
|
47357b3fc6 | ||
|
|
3ed35f9477 | ||
|
|
a369444b62 | ||
|
|
742d658021 | ||
|
|
681c24a0ca | ||
|
|
fc057bf988 | ||
|
|
51561e2f4e |
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -34,7 +34,7 @@ export function Avatar({
|
||||
}) {
|
||||
const backgroundColor = useMemo(() => {
|
||||
const index = hashStringToArrIndex(
|
||||
bgKey || fallback || src,
|
||||
bgKey || fallback || src || "",
|
||||
backgroundColors.length
|
||||
);
|
||||
return backgroundColors[index];
|
||||
|
||||
@@ -56,4 +56,5 @@
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
border-radius: 90px;
|
||||
font-size: 48px;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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}>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,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} />;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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"
|
||||
|
||||
74
src/room/RoomNotFoundView.jsx
Normal file
74
src/room/RoomNotFoundView.jsx
Normal 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>
|
||||
);
|
||||
}
|
||||
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%;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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("!")) {
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -6,3 +6,7 @@
|
||||
.tabContainer {
|
||||
margin: 27px 16px;
|
||||
}
|
||||
|
||||
.fieldRowText {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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