Compare commits

...

20 Commits

Author SHA1 Message Date
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
28 changed files with 683 additions and 143 deletions

View File

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

View File

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

View File

@@ -23,6 +23,7 @@ import React, {
useContext,
} from "react";
import { useHistory } from "react-router-dom";
import { ErrorView } from "./FullScreenView";
import { initClient, defaultHomeserver } from "./matrix-utils";
const ClientContext = createContext();
@@ -30,7 +31,7 @@ const ClientContext = createContext();
export function ClientProvider({ children }) {
const history = useHistory();
const [
{ loading, isAuthenticated, isPasswordlessUser, client, userName },
{ loading, isAuthenticated, isPasswordlessUser, client, userName, error },
setState,
] = useState({
loading: true,
@@ -38,6 +39,7 @@ export function ClientProvider({ children }) {
isPasswordlessUser: false,
client: undefined,
userName: null,
error: undefined,
});
useEffect(() => {
@@ -143,35 +145,71 @@ 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 ("BroadcastChannel" in window) {
const loadTime = Date.now();
const broadcastChannel = new BroadcastChannel("matrix-video-chat");
function onMessage({ data }) {
if (data.load !== undefined && data.load > loadTime) {
if (client) {
client.stopClient();
}
setState((prev) => ({
...prev,
error: new Error(
"This application has been opened in another tab."
),
}));
}
}
broadcastChannel.addEventListener("message", onMessage);
broadcastChannel.postMessage({ load: loadTime });
return () => {
broadcastChannel.removeEventListener("message", onMessage);
};
}
}, [client]);
const context = useMemo(
() => ({
loading,
@@ -195,6 +233,14 @@ export function ClientProvider({ children }) {
]
);
useEffect(() => {
window.matrixclient = client;
}, [client]);
if (error) {
return <ErrorView error={error} />;
}
return (
<ClientContext.Provider value={context}>{children}</ClientContext.Provider>
);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

@@ -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",

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

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

View File

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

View File

@@ -6,21 +6,27 @@ import { Item } from "@react-stately/collections";
import { ReactComponent as SettingsIcon } from "../icons/Settings.svg";
import { ReactComponent as AddUserIcon } from "../icons/AddUser.svg";
import { ReactComponent as OverflowIcon } from "../icons/Overflow.svg";
import { ReactComponent as FeedbackIcon } from "../icons/Feedback.svg";
import { useModalTriggerState } from "../Modal";
import { SettingsModal } from "../settings/SettingsModal";
import { InviteModal } from "./InviteModal";
import { Tooltip, TooltipTrigger } from "../Tooltip";
import { TooltipTrigger } from "../Tooltip";
import { FeedbackModal } from "./FeedbackModal";
export function OverflowMenu({
roomId,
setShowInspector,
showInspector,
client,
inCall,
groupCall,
}) {
const { modalState: inviteModalState, modalProps: inviteModalProps } =
useModalTriggerState();
const { modalState: settingsModalState, modalProps: settingsModalProps } =
useModalTriggerState();
const { modalState: feedbackModalState, modalProps: feedbackModalProps } =
useModalTriggerState();
// TODO: On closing modal, focus should be restored to the trigger button
// https://github.com/adobe/react-spectrum/issues/2444
@@ -32,6 +38,9 @@ export function OverflowMenu({
case "settings":
settingsModalState.open();
break;
case "feedback":
feedbackModalState.open();
break;
}
});
@@ -54,6 +63,10 @@ export function OverflowMenu({
<SettingsIcon />
<span>Settings</span>
</Item>
<Item key="feedback" textValue="Submit Feedback">
<FeedbackIcon />
<span>Submit Feedback</span>
</Item>
</Menu>
)}
</PopoverMenuTrigger>
@@ -68,6 +81,13 @@ export function OverflowMenu({
{inviteModalState.isOpen && (
<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 { Form } from "../form/Form";
import { UserMenuContainer } from "../UserMenuContainer";
import { generateRandomName } from "../auth/generateRandomName";
export function RoomAuthView() {
const [loading, setLoading] = useState(false);
@@ -21,13 +22,20 @@ export function RoomAuthView() {
(e) => {
e.preventDefault();
const data = new FormData(e.target);
const userName = data.get("userName");
const displayName = data.get("displayName");
async function submit() {
setError(undefined);
setLoading(true);
const recaptchaResponse = await execute();
await register(userName, randomString(16), recaptchaResponse, true);
const userName = generateRandomName();
await register(
userName,
randomString(16),
displayName,
recaptchaResponse,
true
);
}
submit().catch((error) => {
@@ -58,10 +66,10 @@ export function RoomAuthView() {
<Form className={styles.form} onSubmit={onSubmit}>
<FieldRow>
<InputField
id="userName"
name="userName"
label="Pick a user name"
placeholder="Pick a user name"
id="displayName"
name="displayName"
label="Display Name"
placeholder="Display Name"
type="text"
required
autoComplete="off"

View File

@@ -0,0 +1,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);
return [params.has("simple"), params.getAll("via")];
}, [search]);
const roomId = maybeRoomId || hash;
const roomId = (maybeRoomId || hash || "").toLowerCase();
if (loading) {
return <LoadingView />;

View File

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

View File

@@ -1,4 +1,4 @@
import React, { useState } from "react";
import React from "react";
import { Modal } from "../Modal";
import styles from "./SettingsModal.module.css";
import { TabContainer, TabItem } from "../tabs/Tabs";
@@ -8,10 +8,9 @@ import { ReactComponent as DeveloperIcon } from "../icons/Developer.svg";
import { SelectInput } from "../input/SelectInput";
import { Item } from "@react-stately/collections";
import { useMediaHandler } from "./useMediaHandler";
import { FieldRow, InputField, ErrorMessage } from "../input/Input";
import { FieldRow, InputField } from "../input/Input";
import { Button } from "../button";
import { useSubmitRageshake } from "./useSubmitRageshake";
import { Subtitle } from "../typography/Typography";
import { useDownloadDebugLog } from "./rageshake";
export function SettingsModal({
client,
@@ -28,10 +27,7 @@ export function SettingsModal({
setVideoInput,
} = useMediaHandler(client);
const [description, setDescription] = useState("");
const { submitRageshake, sending, sent, error, downloadDebugLog } =
useSubmitRageshake();
const downloadDebugLog = useDownloadDebugLog();
return (
<Modal
@@ -96,31 +92,6 @@ export function SettingsModal({
onChange={(e) => setShowInspector(e.target.checked)}
/>
</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>
<Button onPress={downloadDebugLog}>Download Debug Logs</Button>
</FieldRow>

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 pako from "pako";
import { useClient } from "../ClientContext";
import { InspectorContext } from "../room/GroupCallInspector";
import { useModalTriggerState } from "../Modal";
export function useSubmitRageshake() {
const { client } = useClient();
@@ -171,23 +172,32 @@ export function useSubmitRageshake() {
} catch (e) {}
}
const logs = await rageshake.getLogsForReport();
if (opts.sendLogs) {
const logs = await rageshake.getLogsForReport();
for (const entry of logs) {
// encode as UTF-8
let buf = new TextEncoder().encode(entry.lines);
for (const entry of logs) {
// encode as UTF-8
let buf = new TextEncoder().encode(entry.lines);
// compress
buf = pako.gzip(buf);
// compress
buf = pako.gzip(buf);
body.append("compressed-log", new Blob([buf]), entry.id);
body.append("compressed-log", new Blob([buf]), entry.id);
}
if (json) {
body.append(
"file",
new Blob([JSON.stringify(json)], { type: "text/plain" }),
"groupcall.txt"
);
}
}
if (json) {
if (opts.rageshakeRequestId) {
body.append(
"file",
new Blob([JSON.stringify(json)], { type: "text/plain" }),
"groupcall.txt"
"group_call_rageshake_request_id",
opts.rageshakeRequestId
);
}
@@ -209,6 +219,17 @@ export function useSubmitRageshake() {
[client]
);
return {
submitRageshake,
sending,
sent,
error,
};
}
export function useDownloadDebugLog() {
const [{ json }] = useContext(InspectorContext);
const downloadDebugLog = useCallback(() => {
const blob = new Blob([JSON.stringify(json)], { type: "application/json" });
const url = URL.createObjectURL(blob);
@@ -222,7 +243,51 @@ export function useSubmitRageshake() {
URL.revokeObjectURL(url);
el.parentNode.removeChild(el);
}, 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 = () => {
const [layout, setLayout] = useVideoGridLayout();
const [layout, setLayout] = useVideoGridLayout(false);
const [participantCount, setParticipantCount] = useState(1);
const items = useMemo(
@@ -60,7 +60,12 @@ export const ParticipantsTest = () => {
>
<VideoGrid layout={layout} items={items}>
{({ item, ...rest }) => (
<VideoTile key={item.id} name={`User ${item.id}`} {...rest} />
<VideoTile
key={item.id}
name={`User ${item.id}`}
showName={items.length > 2 || item.focused}
{...rest}
/>
)}
</VideoGrid>
</div>

View File

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