Compare commits

...

26 Commits

Author SHA1 Message Date
Robert Long
81a763f17f Avoid duplicate sessions across devices/browsers 2022-02-18 16:23:37 -08:00
Robert Long
1ab7d27ba9 Finish user avatars 2022-02-18 16:02:27 -08:00
Robert Long
e76a805c8f Better logging of to device events / usernames 2022-02-17 14:08:53 -08:00
Robert Long
9fc4af2bd7 Add version to console, rageshake, and settings modal 2022-02-16 11:29:43 -08:00
Robert Long
0f3a7f9fd9 Prevent scroll in call view 2022-02-16 11:17:33 -08:00
Robert Long
1cc634509b Add protocol to copied room url 2022-02-16 10:52:07 -08:00
Robert Long
cb07ce32cb Fix room not found view 2022-02-15 15:00:06 -08:00
Robert Long
6866d662f7 Automatically switch to spotlight layout on screenshare 2022-02-15 14:49:50 -08:00
Robert Long
51a2027d64 Fix screenshare button styling 2022-02-15 12:58:55 -08:00
Robert Long
0f6b8f9bb1 New incremental auth 2022-02-15 12:46:58 -08:00
Robert Long
63229ce2d7 Fix video grid story 2022-02-14 14:49:19 -08:00
Robert Long
1d620910c5 Only show name when focused or more than 2 participants 2022-02-14 14:48:12 -08:00
Robert Long
47357b3fc6 Add room not found view 2022-02-14 13:53:19 -08:00
Robert Long
3ed35f9477 Fix deprecated usage of substr 2022-02-14 12:35:39 -08:00
Robert Long
a369444b62 Convert room id to lowercase 2022-02-14 12:35:07 -08:00
Robert Long
742d658021 Center align call tile contents 2022-02-14 12:19:54 -08:00
Robert Long
681c24a0ca Fix focusing in freedom layout 2022-02-14 11:14:09 -08:00
Robert Long
fc057bf988 Prevent opening multiple tabs of the same account 2022-02-10 17:10:36 -08:00
Robert Long
51561e2f4e Set rageshake submit url for prod 2022-02-07 16:16:51 -08:00
Robert Long
4168540017 Added group_call_rageshake_request_id for rageshake grouping 2022-02-07 15:24:43 -08:00
Robert Long
942630c2fc Merge pull request #207 from vector-im/revert-206-michaelk/rename_groupcall.txt
Revert "Rename groupcall.txt -> groupcall.json."
2022-02-07 15:23:14 -08:00
Robert Long
9251cd9964 Revert "Rename groupcall.txt -> groupcall.json." 2022-02-07 15:23:00 -08:00
Robert Long
145826d1f3 Merge pull request #206 from michaelkaye/michaelk/rename_groupcall.txt
Rename groupcall.txt -> groupcall.json.
2022-02-07 15:09:13 -08:00
Michael Kaye
5e42881c5c Rename groupcall.txt -> groupcall.json.
This will stop groupcall.txt being handled as a 'log file' and instead
indicate it's an artifact to be stored alongside the rageshake.

The file will still be stored on the rageshake server but the extension
will indicate it's not a log file.
2022-02-07 15:28:46 +00:00
Robert Long
0824bfb4ed Update copy and feedback icon 2022-02-04 17:00:58 -08:00
Robert Long
6ec9e4b666 Add rageshake request modal 2022-02-04 16:55:57 -08:00
40 changed files with 882 additions and 184 deletions

View File

@@ -41,7 +41,8 @@
"react-router": "6", "react-router": "6",
"react-router-dom": "^5.2.0", "react-router-dom": "^5.2.0",
"react-use-clipboard": "^1.0.7", "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": { "devDependencies": {
"@babel/core": "^7.16.5", "@babel/core": "^7.16.5",

View File

@@ -4,6 +4,7 @@ set -ex
export VITE_DEFAULT_HOMESERVER=https://call.ems.host export VITE_DEFAULT_HOMESERVER=https://call.ems.host
export VITE_SENTRY_DSN=https://b1e328d49be3402ba96101338989fb35@sentry.matrix.org/41 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 git clone https://github.com/matrix-org/matrix-js-sdk.git
cd matrix-js-sdk cd matrix-js-sdk
@@ -23,6 +24,9 @@ yarn link
cd .. cd ..
cd matrix-video-chat cd matrix-video-chat
export VITE_APP_VERSION=$(git describe --tags --abbrev=0)
yarn link matrix-js-sdk yarn link matrix-js-sdk
yarn link matrix-react-sdk yarn link matrix-react-sdk
yarn install yarn install

View File

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

View File

@@ -23,6 +23,7 @@ import React, {
useContext, useContext,
} from "react"; } from "react";
import { useHistory } from "react-router-dom"; import { useHistory } from "react-router-dom";
import { ErrorView } from "./FullScreenView";
import { initClient, defaultHomeserver } from "./matrix-utils"; import { initClient, defaultHomeserver } from "./matrix-utils";
const ClientContext = createContext(); const ClientContext = createContext();
@@ -30,7 +31,7 @@ const ClientContext = createContext();
export function ClientProvider({ children }) { export function ClientProvider({ children }) {
const history = useHistory(); const history = useHistory();
const [ const [
{ loading, isAuthenticated, isPasswordlessUser, client, userName }, { loading, isAuthenticated, isPasswordlessUser, client, userName, error },
setState, setState,
] = useState({ ] = useState({
loading: true, loading: true,
@@ -38,6 +39,7 @@ export function ClientProvider({ children }) {
isPasswordlessUser: false, isPasswordlessUser: false,
client: undefined, client: undefined,
userName: null, userName: null,
error: undefined,
}); });
useEffect(() => { useEffect(() => {
@@ -143,16 +145,21 @@ export function ClientProvider({ children }) {
[client] [client]
); );
const setClient = useCallback((client, session) => { const setClient = useCallback(
if (client) { (newClient, session) => {
if (client && client !== newClient) {
client.stopClient();
}
if (newClient) {
localStorage.setItem("matrix-auth-store", JSON.stringify(session)); localStorage.setItem("matrix-auth-store", JSON.stringify(session));
setState({ setState({
client, client: newClient,
loading: false, loading: false,
isAuthenticated: true, isAuthenticated: true,
isPasswordlessUser: !!session.passwordlessUser, isPasswordlessUser: !!session.passwordlessUser,
userName: client.getUserIdLocalpart(), userName: newClient.getUserIdLocalpart(),
}); });
} else { } else {
localStorage.removeItem("matrix-auth-store"); localStorage.removeItem("matrix-auth-store");
@@ -165,13 +172,58 @@ export function ClientProvider({ children }) {
userName: null, userName: null,
}); });
} }
}, []); },
[client]
);
const logout = useCallback(() => { const logout = useCallback(() => {
localStorage.removeItem("matrix-auth-store"); localStorage.removeItem("matrix-auth-store");
window.location = "/"; window.location = "/";
}, [history]); }, [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( const context = useMemo(
() => ({ () => ({
loading, loading,
@@ -195,6 +247,14 @@ export function ClientProvider({ children }) {
] ]
); );
useEffect(() => {
window.matrixclient = client;
}, [client]);
if (error) {
return <ErrorView error={error} />;
}
return ( return (
<ClientContext.Provider value={context}>{children}</ClientContext.Provider> <ClientContext.Provider value={context}>{children}</ClientContext.Provider>
); );

View File

@@ -43,14 +43,7 @@ export function UserMenuContainer({ preventNavigation }) {
displayName || (userName ? userName.replace("@", "") : undefined) displayName || (userName ? userName.replace("@", "") : undefined)
} }
/> />
{modalState.isOpen && ( {modalState.isOpen && <ProfileModal client={client} {...modalProps} />}
<ProfileModal
client={client}
isAuthenticated={isAuthenticated}
isPasswordlessUser={isPasswordlessUser}
{...modalProps}
/>
)}
</> </>
); );
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

4
src/icons/Edit.svg Normal file
View File

@@ -0,0 +1,4 @@
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2.64856 7.35501C2.65473 7.31601 2.67231 7.27972 2.69908 7.25069L8.40377 1.06442C8.47865 0.983217 8.60518 0.978093 8.68638 1.05297L9.8626 2.13763C9.9438 2.21251 9.94893 2.33904 9.87405 2.42024L4.16936 8.60651C4.1426 8.63554 4.10783 8.656 4.06946 8.6653L2.66781 9.00511C2.52911 9.03873 2.40084 8.92044 2.42315 8.77948L2.64856 7.35501Z" fill="white"/>
<path d="M1.75 9.44346C1.33579 9.44346 1 9.77925 1 10.1935C1 10.6077 1.33579 10.9435 1.75 10.9435L10.75 10.9435C11.1642 10.9435 11.5 10.6077 11.5 10.1935C11.5 9.77925 11.1642 9.44346 10.75 9.44346L1.75 9.44346Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 689 B

3
src/icons/Feedback.svg Normal file
View File

@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.283 21.4401C17.6495 21.4401 21.9999 17.0881 21.9999 11.7196C21.9999 6.3511 17.6495 1.99908 12.283 1.99908C6.91643 1.99908 2.566 6.3511 2.566 11.7196C2.566 13.2234 2.90739 14.6476 3.51687 15.9186L2.04468 20.7049C1.80806 21.4742 2.5308 22.1936 3.29898 21.9535L8.04564 20.4696C9.32625 21.0914 10.7639 21.4401 12.283 21.4401Z" fill="#ffffff"/>
</svg>

After

Width:  |  Height:  |  Size: 496 B

View File

@@ -0,0 +1,78 @@
import { useObjectRef } from "@react-aria/utils";
import React, { useEffect } from "react";
import { useCallback } from "react";
import { useState } from "react";
import { forwardRef } from "react";
import { Avatar } from "../Avatar";
import { Button } from "../button";
import classNames from "classnames";
import { ReactComponent as EditIcon } from "../icons/Edit.svg";
import styles from "./AvatarInputField.module.css";
export const AvatarInputField = forwardRef(
(
{ id, label, className, avatarUrl, displayName, onRemoveAvatar, ...rest },
ref
) => {
const [removed, setRemoved] = useState(false);
const [objUrl, setObjUrl] = useState(null);
const fileInputRef = useObjectRef(ref);
useEffect(() => {
const onChange = (e) => {
if (e.target.files.length > 0) {
setObjUrl(URL.createObjectURL(e.target.files[0]));
setRemoved(false);
} else {
setObjUrl(null);
}
};
fileInputRef.current.addEventListener("change", onChange);
return () => {
if (fileInputRef.current) {
fileInputRef.current.removeEventListener("change", onChange);
}
};
});
const onPressRemoveAvatar = useCallback(() => {
setRemoved(true);
onRemoveAvatar();
}, [onRemoveAvatar]);
return (
<div className={classNames(styles.avatarInputField, className)}>
<div className={styles.avatarContainer}>
<Avatar
size="xl"
src={removed ? null : objUrl || avatarUrl}
fallback={displayName.slice(0, 1).toUpperCase()}
/>
<input
id={id}
accept="image/png, image/jpeg"
ref={fileInputRef}
type="file"
className={styles.fileInput}
role="button"
aria-label={label}
{...rest}
/>
<label htmlFor={id} className={styles.fileInputButton}>
<EditIcon />
</label>
</div>
<Button
className={styles.removeButton}
variant="icon"
onPress={onPressRemoveAvatar}
>
Remove
</Button>
</div>
);
}
);

View File

@@ -0,0 +1,41 @@
.avatarInputField {
display: flex;
flex-direction: column;
justify-content: center;
}
.avatarContainer {
position: relative;
margin-bottom: 8px;
}
.fileInput {
width: 0.1px;
height: 0.1px;
opacity: 0;
overflow: hidden;
position: absolute;
z-index: -1;
}
.fileInput:focus + .fileInputButton {
outline: auto;
}
.fileInputButton {
position: absolute;
bottom: 11px;
right: -4px;
background-color: var(--bgColor4);
width: 20px;
height: 20px;
border-radius: 10px;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
}
.removeButton {
color: #0dbd8b;
}

View File

@@ -27,6 +27,8 @@ import { InspectorContextProvider } from "./room/GroupCallInspector";
rageshake.init(); rageshake.init();
console.info(`matrix-video-chat ${import.meta.env.VITE_APP_VERSION || "dev"}`);
if (import.meta.env.VITE_CUSTOM_THEME) { if (import.meta.env.VITE_CUSTOM_THEME) {
const style = document.documentElement.style; const style = document.documentElement.style;
style.setProperty("--primaryColor", import.meta.env.VITE_PRIMARY_COLOR); style.setProperty("--primaryColor", import.meta.env.VITE_PRIMARY_COLOR);

View File

@@ -50,6 +50,31 @@ export function roomAliasFromRoomName(roomName) {
.toLowerCase(); .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) { export async function createRoom(client, name) {
const { room_id, room_alias } = await client.createRoom({ const { room_id, room_alias } = await client.createRoom({
visibility: "private", visibility: "private",
@@ -95,12 +120,12 @@ export function getRoomUrl(roomId) {
const [localPart, host] = roomId.replace("#", "").split(":"); const [localPart, host] = roomId.replace("#", "").split(":");
if (host !== defaultHomeserverHost) { if (host !== defaultHomeserverHost) {
return `${window.location.host}/room/${roomId}`; return `${window.location.protocol}//${window.location.host}/room/${roomId}`;
} else { } else {
return `${window.location.host}/${localPart}`; return `${window.location.protocol}//${window.location.host}/${localPart}`;
} }
} else { } else {
return `${window.location.host}/room/${roomId}`; return `${window.location.protocol}//${window.location.host}/room/${roomId}`;
} }
} }

View File

@@ -3,22 +3,25 @@ import { Button } from "../button";
import { useProfile } from "./useProfile"; import { useProfile } from "./useProfile";
import { FieldRow, InputField, ErrorMessage } from "../input/Input"; import { FieldRow, InputField, ErrorMessage } from "../input/Input";
import { Modal, ModalContent } from "../Modal"; import { Modal, ModalContent } from "../Modal";
import { AvatarInputField } from "../input/AvatarInputField";
import styles from "./ProfileModal.module.css";
export function ProfileModal({ export function ProfileModal({ client, ...rest }) {
client,
isAuthenticated,
isPasswordlessUser,
...rest
}) {
const { onClose } = rest; const { onClose } = rest;
const { const {
success, success,
error, error,
loading, loading,
displayName: initialDisplayName, displayName: initialDisplayName,
avatarUrl,
saveProfile, saveProfile,
} = useProfile(client); } = useProfile(client);
const [displayName, setDisplayName] = useState(initialDisplayName || ""); const [displayName, setDisplayName] = useState(initialDisplayName || "");
const [removeAvatar, setRemoveAvatar] = useState(false);
const onRemoveAvatar = useCallback(() => {
setRemoveAvatar(true);
}, []);
const onChangeDisplayName = useCallback( const onChangeDisplayName = useCallback(
(e) => { (e) => {
@@ -37,9 +40,10 @@ export function ProfileModal({
saveProfile({ saveProfile({
displayName, displayName,
avatar: avatar && avatar.size > 0 ? avatar : undefined, avatar: avatar && avatar.size > 0 ? avatar : undefined,
removeAvatar: removeAvatar && (!avatar || avatar.size === 0),
}); });
}, },
[saveProfile] [saveProfile, removeAvatar]
); );
useEffect(() => { useEffect(() => {
@@ -52,6 +56,16 @@ export function ProfileModal({
<Modal title="Profile" isDismissable {...rest}> <Modal title="Profile" isDismissable {...rest}>
<ModalContent> <ModalContent>
<form onSubmit={onSubmit}> <form onSubmit={onSubmit}>
<FieldRow className={styles.avatarFieldRow}>
<AvatarInputField
id="avatar"
name="avatar"
label="Avatar"
avatarUrl={avatarUrl}
displayName={displayName}
onRemoveAvatar={onRemoveAvatar}
/>
</FieldRow>
<FieldRow> <FieldRow>
<InputField <InputField
id="userId" id="userId"
@@ -75,16 +89,6 @@ export function ProfileModal({
onChange={onChangeDisplayName} onChange={onChangeDisplayName}
/> />
</FieldRow> </FieldRow>
{isAuthenticated && (
<FieldRow>
<InputField
type="file"
id="avatar"
name="avatar"
label="Avatar"
/>
</FieldRow>
)}
{error && ( {error && (
<FieldRow> <FieldRow>
<ErrorMessage>{error.message}</ErrorMessage> <ErrorMessage>{error.message}</ErrorMessage>

View File

@@ -0,0 +1,3 @@
.avatarFieldRow {
justify-content: center;
}

View File

@@ -44,7 +44,7 @@ export function useProfile(client) {
}, [client]); }, [client]);
const saveProfile = useCallback( const saveProfile = useCallback(
async ({ displayName, avatar }) => { async ({ displayName, avatar, removeAvatar }) => {
if (client) { if (client) {
setState((prev) => ({ setState((prev) => ({
...prev, ...prev,
@@ -58,7 +58,9 @@ export function useProfile(client) {
let mxcAvatarUrl; let mxcAvatarUrl;
if (avatar) { if (removeAvatar) {
await client.setAvatarUrl("");
} else if (avatar) {
mxcAvatarUrl = await client.uploadContent(avatar); mxcAvatarUrl = await client.uploadContent(avatar);
await client.setAvatarUrl(mxcAvatarUrl); await client.setAvatarUrl(mxcAvatarUrl);
} }
@@ -66,7 +68,9 @@ export function useProfile(client) {
setState((prev) => ({ setState((prev) => ({
...prev, ...prev,
displayName, displayName,
avatarUrl: mxcAvatarUrl avatarUrl: removeAvatar
? null
: mxcAvatarUrl
? getAvatarUrl(client, mxcAvatarUrl) ? getAvatarUrl(client, mxcAvatarUrl)
: prev.avatarUrl, : prev.avatarUrl,
loading: false, loading: false,

View File

@@ -0,0 +1,76 @@
import React, { useCallback, useEffect } from "react";
import { Modal, ModalContent } from "../Modal";
import { Button } from "../button";
import { FieldRow, InputField, ErrorMessage } from "../input/Input";
import { useSubmitRageshake, useRageshakeRequest } from "../settings/rageshake";
import { Body } from "../typography/Typography";
import { randomString } from "matrix-js-sdk/src/randomstring";
export function FeedbackModal({ inCall, roomId, ...rest }) {
const { submitRageshake, sending, sent, error } = useSubmitRageshake();
const sendRageshakeRequest = useRageshakeRequest();
const onSubmitFeedback = useCallback(
(e) => {
e.preventDefault();
const data = new FormData(e.target);
const description = data.get("description");
const sendLogs = data.get("sendLogs");
const rageshakeRequestId = randomString(16);
submitRageshake({
description,
sendLogs,
rageshakeRequestId,
});
if (inCall && sendLogs) {
sendRageshakeRequest(roomId, rageshakeRequestId);
}
},
[inCall, submitRageshake, roomId, sendRageshakeRequest]
);
useEffect(() => {
if (sent) {
rest.onClose();
}
}, [sent, rest.onClose]);
return (
<Modal title="Submit Feedback" isDismissable {...rest}>
<ModalContent>
<Body>Having trouble? Help us fix it.</Body>
<form onSubmit={onSubmitFeedback}>
<FieldRow>
<InputField
id="description"
name="description"
label="Description (optional)"
type="text"
/>
</FieldRow>
<FieldRow>
<InputField
id="sendLogs"
name="sendLogs"
label="Include Debug Logs"
type="checkbox"
defaultChecked
/>
</FieldRow>
{error && (
<FieldRow>
<ErrorMessage>{error.message}</ErrorMessage>
</FieldRow>
)}
<FieldRow>
<Button type="submit" disabled={sending}>
{sending ? "Submitting feedback..." : "Submit Feedback"}
</Button>
</FieldRow>
</form>
</ModalContent>
</Modal>
);
}

View File

@@ -59,8 +59,8 @@ function getUserName(userId) {
const match = userId.match(/@([^\:]+):/); const match = userId.match(/@([^\:]+):/);
return match && match.length > 0 return match && match.length > 0
? match[1].replace("-", " ").replace("W", "") ? match[1].replace("-", " ").replace(/\W/g, "")
: userId.replace("W", ""); : userId.replace(/\W/g, "");
} }
function formatContent(type, content) { function formatContent(type, content) {
@@ -231,7 +231,7 @@ function reducer(state, action) {
), ),
}; };
} }
case "receive_to_device_event": { case "received_voip_event": {
const event = action.event; const event = action.event;
const eventsByUserId = { ...state.eventsByUserId }; const eventsByUserId = { ...state.eventsByUserId };
const fromId = event.getSender(); const fromId = event.getSender();
@@ -338,8 +338,8 @@ function useGroupCallState(client, groupCall, pollCallStats) {
// dispatch({ type: "call_hangup", call }); // dispatch({ type: "call_hangup", call });
// } // }
function onToDeviceEvent(event) { function onReceivedVoipEvent(event) {
dispatch({ type: "receive_to_device_event", event }); dispatch({ type: "received_voip_event", event });
} }
function onSendVoipEvent(event) { function onSendVoipEvent(event) {
@@ -351,7 +351,7 @@ function useGroupCallState(client, groupCall, pollCallStats) {
groupCall.on("send_voip_event", onSendVoipEvent); groupCall.on("send_voip_event", onSendVoipEvent);
//client.on("state", onCallsChanged); //client.on("state", onCallsChanged);
//client.on("hangup", onCallHangup); //client.on("hangup", onCallHangup);
client.on("toDeviceEvent", onToDeviceEvent); client.on("received_voip_event", onReceivedVoipEvent);
onUpdateRoomState(); onUpdateRoomState();
@@ -361,7 +361,7 @@ function useGroupCallState(client, groupCall, pollCallStats) {
groupCall.removeListener("send_voip_event", onSendVoipEvent); groupCall.removeListener("send_voip_event", onSendVoipEvent);
//client.removeListener("state", onCallsChanged); //client.removeListener("state", onCallsChanged);
//client.removeListener("hangup", onCallHangup); //client.removeListener("hangup", onCallHangup);
client.removeListener("toDeviceEvent", onToDeviceEvent); client.removeListener("received_voip_event", onReceivedVoipEvent);
}; };
}, [client, groupCall]); }, [client, groupCall]);

View File

@@ -2,6 +2,8 @@ import React from "react";
import { useLoadGroupCall } from "./useLoadGroupCall"; import { useLoadGroupCall } from "./useLoadGroupCall";
import { ErrorView, FullScreenView } from "../FullScreenView"; import { ErrorView, FullScreenView } from "../FullScreenView";
import { usePageTitle } from "../usePageTitle"; import { usePageTitle } from "../usePageTitle";
import { isLocalRoomId } from "../matrix-utils";
import { RoomNotFoundView } from "./RoomNotFoundView";
export function GroupCallLoader({ client, roomId, viaServers, children }) { export function GroupCallLoader({ client, roomId, viaServers, children }) {
const { loading, error, groupCall } = useLoadGroupCall( const { loading, error, groupCall } = 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) { if (error) {
return <ErrorView error={error} />; return <ErrorView error={error} />;
} }

View File

@@ -19,6 +19,9 @@ import { OverflowMenu } from "./OverflowMenu";
import { GridLayoutMenu } from "./GridLayoutMenu"; import { GridLayoutMenu } from "./GridLayoutMenu";
import { Avatar } from "../Avatar"; import { Avatar } from "../Avatar";
import { UserMenuContainer } from "../UserMenuContainer"; 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; const canScreenshare = "getDisplayMedia" in navigator.mediaDevices;
// There is currently a bug in Safari our our code with cloning and sending MediaStreams // There is currently a bug in Safari our our code with cloning and sending MediaStreams
@@ -45,7 +48,8 @@ export function InCallView({
showInspector, showInspector,
roomId, roomId,
}) { }) {
const [layout, setLayout] = useVideoGridLayout(); usePreventScroll();
const [layout, setLayout] = useVideoGridLayout(screenshareFeeds.length > 0);
const items = useMemo(() => { const items = useMemo(() => {
const participants = []; const participants = [];
@@ -55,7 +59,7 @@ export function InCallView({
id: callFeed.stream.id, id: callFeed.stream.id,
callFeed, callFeed,
focused: focused:
screenshareFeeds.length === 0 screenshareFeeds.length === 0 && layout === "spotlight"
? callFeed.userId === activeSpeaker ? callFeed.userId === activeSpeaker
: false, : false,
}); });
@@ -78,14 +82,14 @@ export function InCallView({
} }
return participants; return participants;
}, [userMediaFeeds, activeSpeaker, screenshareFeeds]); }, [userMediaFeeds, activeSpeaker, screenshareFeeds, layout]);
const onFocusTile = useCallback( const onFocusTile = useCallback(
(tiles, focusedTile) => { (tiles, focusedTile) => {
if (layout === "freedom") { if (layout === "freedom") {
return tiles.map((tile) => { return tiles.map((tile) => {
if (tile === focusedTile) { if (tile === focusedTile) {
return { ...tile, presenter: !tile.presenter }; return { ...tile, focused: !tile.focused };
} }
return tile; return tile;
@@ -120,6 +124,11 @@ export function InCallView({
[client] [client]
); );
const {
modalState: rageshakeRequestModalState,
modalProps: rageshakeRequestModalProps,
} = useRageshakeRequestModal(groupCall.room.roomId);
return ( return (
<div className={styles.inRoom}> <div className={styles.inRoom}>
<Header> <Header>
@@ -149,6 +158,7 @@ export function InCallView({
key={item.id} key={item.id}
item={item} item={item}
getAvatar={renderAvatar} getAvatar={renderAvatar}
showName={items.length > 2 || item.focused}
{...rest} {...rest}
/> />
)} )}
@@ -164,10 +174,12 @@ export function InCallView({
/> />
)} )}
<OverflowMenu <OverflowMenu
inCall
roomId={roomId} roomId={roomId}
setShowInspector={setShowInspector} setShowInspector={setShowInspector}
showInspector={showInspector} showInspector={showInspector}
client={client} client={client}
groupCall={groupCall}
/> />
<HangupButton onPress={onLeave} /> <HangupButton onPress={onLeave} />
</div> </div>
@@ -176,6 +188,9 @@ export function InCallView({
groupCall={groupCall} groupCall={groupCall}
show={showInspector} show={showInspector}
/> />
{rageshakeRequestModalState.isOpen && (
<RageshakeRequestModal {...rageshakeRequestModalProps} />
)}
</div> </div>
); );
} }

View File

@@ -10,7 +10,6 @@ import { OverflowMenu } from "./OverflowMenu";
import { UserMenuContainer } from "../UserMenuContainer"; import { UserMenuContainer } from "../UserMenuContainer";
import { Body, Link } from "../typography/Typography"; import { Body, Link } from "../typography/Typography";
import { Avatar } from "../Avatar"; import { Avatar } from "../Avatar";
import { getAvatarUrl } from "../matrix-utils";
import { useProfile } from "../profile/useProfile"; import { useProfile } from "../profile/useProfile";
import useMeasure from "react-use-measure"; import useMeasure from "react-use-measure";
import { ResizeObserver } from "@juggle/resize-observer"; import { ResizeObserver } from "@juggle/resize-observer";
@@ -86,7 +85,7 @@ export function LobbyView({
borderRadius: avatarSize, borderRadius: avatarSize,
fontSize: Math.round(avatarSize / 2), fontSize: Math.round(avatarSize / 2),
}} }}
src={avatarUrl && getAvatarUrl(client, avatarUrl, 96)} src={avatarUrl}
fallback={displayName.slice(0, 1).toUpperCase()} fallback={displayName.slice(0, 1).toUpperCase()}
/> />
</div> </div>

View File

@@ -6,21 +6,27 @@ import { Item } from "@react-stately/collections";
import { ReactComponent as SettingsIcon } from "../icons/Settings.svg"; import { ReactComponent as SettingsIcon } from "../icons/Settings.svg";
import { ReactComponent as AddUserIcon } from "../icons/AddUser.svg"; import { ReactComponent as AddUserIcon } from "../icons/AddUser.svg";
import { ReactComponent as OverflowIcon } from "../icons/Overflow.svg"; import { ReactComponent as OverflowIcon } from "../icons/Overflow.svg";
import { ReactComponent as FeedbackIcon } from "../icons/Feedback.svg";
import { useModalTriggerState } from "../Modal"; import { useModalTriggerState } from "../Modal";
import { SettingsModal } from "../settings/SettingsModal"; import { SettingsModal } from "../settings/SettingsModal";
import { InviteModal } from "./InviteModal"; import { InviteModal } from "./InviteModal";
import { Tooltip, TooltipTrigger } from "../Tooltip"; import { TooltipTrigger } from "../Tooltip";
import { FeedbackModal } from "./FeedbackModal";
export function OverflowMenu({ export function OverflowMenu({
roomId, roomId,
setShowInspector, setShowInspector,
showInspector, showInspector,
client, client,
inCall,
groupCall,
}) { }) {
const { modalState: inviteModalState, modalProps: inviteModalProps } = const { modalState: inviteModalState, modalProps: inviteModalProps } =
useModalTriggerState(); useModalTriggerState();
const { modalState: settingsModalState, modalProps: settingsModalProps } = const { modalState: settingsModalState, modalProps: settingsModalProps } =
useModalTriggerState(); useModalTriggerState();
const { modalState: feedbackModalState, modalProps: feedbackModalProps } =
useModalTriggerState();
// TODO: On closing modal, focus should be restored to the trigger button // TODO: On closing modal, focus should be restored to the trigger button
// https://github.com/adobe/react-spectrum/issues/2444 // https://github.com/adobe/react-spectrum/issues/2444
@@ -32,6 +38,9 @@ export function OverflowMenu({
case "settings": case "settings":
settingsModalState.open(); settingsModalState.open();
break; break;
case "feedback":
feedbackModalState.open();
break;
} }
}); });
@@ -54,6 +63,10 @@ export function OverflowMenu({
<SettingsIcon /> <SettingsIcon />
<span>Settings</span> <span>Settings</span>
</Item> </Item>
<Item key="feedback" textValue="Submit Feedback">
<FeedbackIcon />
<span>Submit Feedback</span>
</Item>
</Menu> </Menu>
)} )}
</PopoverMenuTrigger> </PopoverMenuTrigger>
@@ -68,6 +81,13 @@ export function OverflowMenu({
{inviteModalState.isOpen && ( {inviteModalState.isOpen && (
<InviteModal roomId={roomId} {...inviteModalProps} /> <InviteModal roomId={roomId} {...inviteModalProps} />
)} )}
{feedbackModalState.isOpen && (
<FeedbackModal
{...feedbackModalProps}
roomId={groupCall?.room.roomId}
inCall={inCall}
/>
)}
</> </>
); );
} }

View File

@@ -0,0 +1,45 @@
import React, { useEffect } from "react";
import { Modal, ModalContent } from "../Modal";
import { Button } from "../button";
import { FieldRow, ErrorMessage } from "../input/Input";
import { useSubmitRageshake } from "../settings/rageshake";
import { Body } from "../typography/Typography";
export function RageshakeRequestModal({ rageshakeRequestId, ...rest }) {
const { submitRageshake, sending, sent, error } = useSubmitRageshake();
useEffect(() => {
if (sent) {
rest.onClose();
}
}, [sent, rest.onClose]);
return (
<Modal title="Debug Log Request" isDismissable {...rest}>
<ModalContent>
<Body>
Another user on this call is having an issue. In order to better
diagnose these issues we'd like to collect a debug log.
</Body>
<FieldRow>
<Button
onPress={() =>
submitRageshake({
sendLogs: true,
rageshakeRequestId,
})
}
disabled={sending}
>
{sending ? "Sending debug log..." : "Send debug log"}
</Button>
</FieldRow>
{error && (
<FieldRow>
<ErrorMessage>{error.message}</ErrorMessage>
</FieldRow>
)}
</ModalContent>
</Modal>
);
}

View File

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

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

View File

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

View File

@@ -32,7 +32,7 @@ export function RoomPage() {
const params = new URLSearchParams(search); const params = new URLSearchParams(search);
return [params.has("simple"), params.getAll("via")]; return [params.has("simple"), params.getAll("via")];
}, [search]); }, [search]);
const roomId = maybeRoomId || hash; const roomId = (maybeRoomId || hash || "").toLowerCase();
if (loading) { if (loading) {
return <LoadingView />; return <LoadingView />;

View File

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

View File

@@ -1,4 +1,4 @@
import React, { useState } from "react"; import React from "react";
import { Modal } from "../Modal"; import { Modal } from "../Modal";
import styles from "./SettingsModal.module.css"; import styles from "./SettingsModal.module.css";
import { TabContainer, TabItem } from "../tabs/Tabs"; import { TabContainer, TabItem } from "../tabs/Tabs";
@@ -8,10 +8,10 @@ import { ReactComponent as DeveloperIcon } from "../icons/Developer.svg";
import { SelectInput } from "../input/SelectInput"; import { SelectInput } from "../input/SelectInput";
import { Item } from "@react-stately/collections"; import { Item } from "@react-stately/collections";
import { useMediaHandler } from "./useMediaHandler"; import { useMediaHandler } from "./useMediaHandler";
import { FieldRow, InputField, ErrorMessage } from "../input/Input"; import { FieldRow, InputField } from "../input/Input";
import { Button } from "../button"; import { Button } from "../button";
import { useSubmitRageshake } from "./useSubmitRageshake"; import { useDownloadDebugLog } from "./rageshake";
import { Subtitle } from "../typography/Typography"; import { Body } from "../typography/Typography";
export function SettingsModal({ export function SettingsModal({
client, client,
@@ -28,10 +28,7 @@ export function SettingsModal({
setVideoInput, setVideoInput,
} = useMediaHandler(client); } = useMediaHandler(client);
const [description, setDescription] = useState(""); const downloadDebugLog = useDownloadDebugLog();
const { submitRageshake, sending, sent, error, downloadDebugLog } =
useSubmitRageshake();
return ( return (
<Modal <Modal
@@ -86,6 +83,11 @@ export function SettingsModal({
</> </>
} }
> >
<FieldRow>
<Body className={styles.fieldRowText}>
Version: {import.meta.env.VITE_APP_VERSION || "dev"}
</Body>
</FieldRow>
<FieldRow> <FieldRow>
<InputField <InputField
id="showInspector" id="showInspector"
@@ -96,31 +98,6 @@ export function SettingsModal({
onChange={(e) => setShowInspector(e.target.checked)} onChange={(e) => setShowInspector(e.target.checked)}
/> />
</FieldRow> </FieldRow>
<Subtitle>Feedback</Subtitle>
<FieldRow>
<InputField
id="description"
name="description"
label="Description"
type="text"
value={description}
onChange={(e) => setDescription(e.target.value)}
/>
</FieldRow>
<FieldRow>
<Button onPress={() => submitRageshake({ description })}>
{sent
? "Debug Logs Sent"
: sending
? "Sending Debug Logs..."
: "Send Debug Logs"}
</Button>
</FieldRow>
{error && (
<FieldRow>
<ErrorMessage>{error.message}</ErrorMessage>
</FieldRow>
)}
<FieldRow> <FieldRow>
<Button onPress={downloadDebugLog}>Download Debug Logs</Button> <Button onPress={downloadDebugLog}>Download Debug Logs</Button>
</FieldRow> </FieldRow>

View File

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

View File

@@ -1,8 +1,9 @@
import { useCallback, useContext, useState } from "react"; import { useCallback, useContext, useEffect, useState } from "react";
import * as rageshake from "matrix-react-sdk/src/rageshake/rageshake"; import * as rageshake from "matrix-react-sdk/src/rageshake/rageshake";
import pako from "pako"; import pako from "pako";
import { useClient } from "../ClientContext"; import { useClient } from "../ClientContext";
import { InspectorContext } from "../room/GroupCallInspector"; import { InspectorContext } from "../room/GroupCallInspector";
import { useModalTriggerState } from "../Modal";
export function useSubmitRageshake() { export function useSubmitRageshake() {
const { client } = useClient(); const { client } = useClient();
@@ -40,7 +41,7 @@ export function useSubmitRageshake() {
opts.description || "User did not supply any additional text." opts.description || "User did not supply any additional text."
); );
body.append("app", "matrix-video-chat"); body.append("app", "matrix-video-chat");
body.append("version", "dev"); body.append("version", import.meta.env.VITE_APP_VERSION || "dev");
body.append("user_agent", userAgent); body.append("user_agent", userAgent);
body.append("installed_pwa", false); body.append("installed_pwa", false);
body.append("touch_input", touchInput); body.append("touch_input", touchInput);
@@ -171,6 +172,7 @@ export function useSubmitRageshake() {
} catch (e) {} } catch (e) {}
} }
if (opts.sendLogs) {
const logs = await rageshake.getLogsForReport(); const logs = await rageshake.getLogsForReport();
for (const entry of logs) { for (const entry of logs) {
@@ -190,6 +192,14 @@ export function useSubmitRageshake() {
"groupcall.txt" "groupcall.txt"
); );
} }
}
if (opts.rageshakeRequestId) {
body.append(
"group_call_rageshake_request_id",
opts.rageshakeRequestId
);
}
await fetch( await fetch(
import.meta.env.VITE_RAGESHAKE_SUBMIT_URL || import.meta.env.VITE_RAGESHAKE_SUBMIT_URL ||
@@ -209,6 +219,17 @@ export function useSubmitRageshake() {
[client] [client]
); );
return {
submitRageshake,
sending,
sent,
error,
};
}
export function useDownloadDebugLog() {
const [{ json }] = useContext(InspectorContext);
const downloadDebugLog = useCallback(() => { const downloadDebugLog = useCallback(() => {
const blob = new Blob([JSON.stringify(json)], { type: "application/json" }); const blob = new Blob([JSON.stringify(json)], { type: "application/json" });
const url = URL.createObjectURL(blob); const url = URL.createObjectURL(blob);
@@ -222,7 +243,51 @@ export function useSubmitRageshake() {
URL.revokeObjectURL(url); URL.revokeObjectURL(url);
el.parentNode.removeChild(el); el.parentNode.removeChild(el);
}, 0); }, 0);
}); }, [json]);
return { submitRageshake, sending, sent, error, downloadDebugLog }; return downloadDebugLog;
}
export function useRageshakeRequest() {
const { client } = useClient();
const sendRageshakeRequest = useCallback(
(roomId, rageshakeRequestId) => {
client.sendEvent(roomId, "org.matrix.rageshake_request", {
request_id: rageshakeRequestId,
});
},
[client]
);
return sendRageshakeRequest;
}
export function useRageshakeRequestModal(roomId) {
const { modalState, modalProps } = useModalTriggerState();
const { client } = useClient();
const [rageshakeRequestId, setRageshakeRequestId] = useState();
useEffect(() => {
const onEvent = (event) => {
const type = event.getType();
if (
type === "org.matrix.rageshake_request" &&
roomId === event.getRoomId() &&
client.getUserId() !== event.getSender()
) {
setRageshakeRequestId(event.getContent().request_id);
modalState.open();
}
};
client.on("event", onEvent);
return () => {
client.removeListener("event", onEvent);
};
}, [modalState.open, roomId]);
return { modalState, modalProps: { ...modalProps, rageshakeRequestId } };
} }

View File

@@ -15,7 +15,7 @@ export default {
}; };
export const ParticipantsTest = () => { export const ParticipantsTest = () => {
const [layout, setLayout] = useVideoGridLayout(); const [layout, setLayout] = useVideoGridLayout(false);
const [participantCount, setParticipantCount] = useState(1); const [participantCount, setParticipantCount] = useState(1);
const items = useMemo( const items = useMemo(
@@ -60,7 +60,12 @@ export const ParticipantsTest = () => {
> >
<VideoGrid layout={layout} items={items}> <VideoGrid layout={layout} items={items}>
{({ item, ...rest }) => ( {({ 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> </VideoGrid>
</div> </div>

View File

@@ -12039,6 +12039,11 @@ unique-filename@^1.1.1:
dependencies: dependencies:
unique-slug "^2.0.0" 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: unique-slug@^2.0.0:
version "2.0.2" version "2.0.2"
resolved "https://registry.yarnpkg.com/unique-slug/-/unique-slug-2.0.2.tgz#baabce91083fc64e945b0f3ad613e264f7cd4e6c" resolved "https://registry.yarnpkg.com/unique-slug/-/unique-slug-2.0.2.tgz#baabce91083fc64e945b0f3ad613e264f7cd4e6c"