diff --git a/.env b/.env
index 2785e053..f9e5c887 100644
--- a/.env
+++ b/.env
@@ -7,6 +7,9 @@
# Used for determining the homeserver to use for short urls etc.
# VITE_DEFAULT_HOMESERVER=http://localhost:8008
+# Used for submitting debug logs to an external rageshake server
+# VITE_RAGESHAKE_SUBMIT_URL=http://localhost:9110/api/submit
+
# The Sentry DSN to use for error reporting. Leave undefined to disable.
# VITE_SENTRY_DSN=https://examplePublicKey@o0.ingest.sentry.io/0
diff --git a/package.json b/package.json
index 0437c93a..e78203ed 100644
--- a/package.json
+++ b/package.json
@@ -32,6 +32,7 @@
"matrix-react-sdk": "github:matrix-org/matrix-react-sdk#robertlong/group-call",
"mermaid": "^8.13.8",
"normalize.css": "^8.0.1",
+ "pako": "^2.0.4",
"postcss-preset-env": "^6.7.0",
"re-resizable": "^6.9.0",
"react": "^17.0.0",
diff --git a/src/App.jsx b/src/App.jsx
index 5dea73e2..3c882c51 100644
--- a/src/App.jsx
+++ b/src/App.jsx
@@ -25,6 +25,7 @@ import { RoomPage } from "./room/RoomPage";
import { RoomRedirect } from "./room/RoomRedirect";
import { ClientProvider } from "./ClientContext";
import { usePageFocusStyle } from "./usePageFocusStyle";
+import { SequenceDiagramViewerPage } from "./SequenceDiagramViewerPage";
const SentryRoute = Sentry.withSentryRouting(Route);
@@ -48,6 +49,9 @@ export default function App({ history }) {
+
+
+
diff --git a/src/SequenceDiagramViewerPage.jsx b/src/SequenceDiagramViewerPage.jsx
new file mode 100644
index 00000000..f19b9033
--- /dev/null
+++ b/src/SequenceDiagramViewerPage.jsx
@@ -0,0 +1,38 @@
+import React, { useCallback, useState } from "react";
+import { SequenceDiagramViewer } from "./room/GroupCallInspector";
+import { FieldRow, InputField } from "./input/Input";
+
+export function SequenceDiagramViewerPage() {
+ const [debugLog, setDebugLog] = useState();
+ const [selectedUserId, setSelectedUserId] = useState();
+ const onChangeDebugLog = useCallback((e) => {
+ if (e.target.files && e.target.files.length > 0) {
+ e.target.files[0].text().then((text) => {
+ setDebugLog(JSON.parse(text));
+ });
+ }
+ }, []);
+
+ return (
+
+
+
+
+ {debugLog && (
+
+ )}
+
+ );
+}
diff --git a/src/main.jsx b/src/main.jsx
index 5db2324f..c43db9a2 100644
--- a/src/main.jsx
+++ b/src/main.jsx
@@ -22,6 +22,10 @@ import App from "./App";
import * as Sentry from "@sentry/react";
import { Integrations } from "@sentry/tracing";
import { ErrorView } from "./FullScreenView";
+import * as rageshake from "matrix-react-sdk/src/rageshake/rageshake";
+import { InspectorContextProvider } from "./room/GroupCallInspector";
+
+rageshake.init();
if (import.meta.env.VITE_CUSTOM_THEME) {
const style = document.documentElement.style;
@@ -59,7 +63,9 @@ Sentry.init({
ReactDOM.render(
-
+
+
+
,
document.getElementById("root")
diff --git a/src/room/GroupCallInspector.jsx b/src/room/GroupCallInspector.jsx
index a46db0be..c64f34d0 100644
--- a/src/room/GroupCallInspector.jsx
+++ b/src/room/GroupCallInspector.jsx
@@ -1,5 +1,12 @@
import { Resizable } from "re-resizable";
-import React, { useEffect, useState, useReducer, useRef } from "react";
+import React, {
+ useEffect,
+ useState,
+ useReducer,
+ useRef,
+ createContext,
+ useContext,
+} from "react";
import ReactJson from "react-json-view";
import mermaid from "mermaid";
import styles from "./GroupCallInspector.module.css";
@@ -90,7 +97,18 @@ function formatTimestamp(timestamp) {
return dateFormatter.format(timestamp);
}
-function SequenceDiagramViewer({
+export const InspectorContext = createContext();
+
+export function InspectorContextProvider({ children }) {
+ const context = useState({});
+ return (
+
+ {children}
+
+ );
+}
+
+export function SequenceDiagramViewer({
localUserId,
remoteUserIds,
selectedUserId,
@@ -168,16 +186,16 @@ function reducer(state, action) {
const fromId = event.getStateKey();
remoteUserIds =
- fromId === state.localUserId || eventsByUserId.has(fromId)
+ fromId === state.localUserId || eventsByUserId[fromId]
? state.remoteUserIds
: [...state.remoteUserIds, fromId];
- eventsByUserId = new Map(state.eventsByUserId);
+ eventsByUserId = { ...state.eventsByUserId };
if (event.getStateKey() === state.localUserId) {
for (const userId in eventsByUserId) {
- eventsByUserId.set(userId, [
- ...(eventsByUserId.get(userId) || []),
+ eventsByUserId[userId] = [
+ ...(eventsByUserId[userId] || []),
{
from: fromId,
to: "Room",
@@ -186,11 +204,11 @@ function reducer(state, action) {
timestamp: event.getTs() || Date.now(),
ignored: false,
},
- ]);
+ ];
}
} else {
- eventsByUserId.set(fromId, [
- ...(eventsByUserId.get(fromId) || []),
+ eventsByUserId[fromId] = [
+ ...(eventsByUserId[fromId] || []),
{
from: fromId,
to: "Room",
@@ -199,7 +217,7 @@ function reducer(state, action) {
timestamp: event.getTs() || Date.now(),
ignored: false,
},
- ]);
+ ];
}
}
@@ -215,17 +233,17 @@ function reducer(state, action) {
}
case "receive_to_device_event": {
const event = action.event;
- const eventsByUserId = new Map(state.eventsByUserId);
+ const eventsByUserId = { ...state.eventsByUserId };
const fromId = event.getSender();
const toId = state.localUserId;
const content = event.getContent();
- const remoteUserIds = eventsByUserId.has(fromId)
+ const remoteUserIds = eventsByUserId[fromId]
? state.remoteUserIds
: [...state.remoteUserIds, fromId];
- eventsByUserId.set(fromId, [
- ...(eventsByUserId.get(fromId) || []),
+ eventsByUserId[fromId] = [
+ ...(eventsByUserId[fromId] || []),
{
from: fromId,
to: toId,
@@ -234,22 +252,22 @@ function reducer(state, action) {
timestamp: event.getTs() || Date.now(),
ignored: state.localSessionId !== content.dest_session_id,
},
- ]);
+ ];
return { ...state, eventsByUserId, remoteUserIds };
}
case "send_voip_event": {
const event = action.event;
- const eventsByUserId = new Map(state.eventsByUserId);
+ const eventsByUserId = { ...state.eventsByUserId };
const fromId = state.localUserId;
const toId = event.userId;
- const remoteUserIds = eventsByUserId.has(toId)
+ const remoteUserIds = eventsByUserId[toId]
? state.remoteUserIds
: [...state.remoteUserIds, toId];
- eventsByUserId.set(toId, [
- ...(eventsByUserId.get(toId) || []),
+ eventsByUserId[toId] = [
+ ...(eventsByUserId[toId] || []),
{
from: fromId,
to: toId,
@@ -258,7 +276,7 @@ function reducer(state, action) {
timestamp: Date.now(),
ignored: false,
},
- ]);
+ ];
return { ...state, eventsByUserId, remoteUserIds };
}
@@ -271,7 +289,7 @@ function useGroupCallState(client, groupCall, pollCallStats) {
const [state, dispatch] = useReducer(reducer, {
localUserId: client.getUserId(),
localSessionId: client.getSessionId(),
- eventsByUserId: new Map(),
+ eventsByUserId: {},
remoteUserIds: [],
callStateEvent: null,
memberStateEvents: {},
@@ -399,6 +417,12 @@ export function GroupCallInspector({ client, groupCall, show }) {
const [selectedUserId, setSelectedUserId] = useState();
const state = useGroupCallState(client, groupCall, show);
+ const [_, setState] = useContext(InspectorContext);
+
+ useEffect(() => {
+ setState({ json: state });
+ }, [setState, state]);
+
if (!show) {
return null;
}
@@ -421,16 +445,13 @@ export function GroupCallInspector({ client, groupCall, show }) {
selectedUserId={selectedUserId}
onSelectUserId={setSelectedUserId}
remoteUserIds={state.remoteUserIds}
- events={state.eventsByUserId.get(selectedUserId)}
+ events={state.eventsByUserId[selectedUserId]}
/>
)}
{currentTab === "inspector" && (
setShowInspector(e.target.checked)}
/>
+
+
+
+
+
+
diff --git a/src/settings/useSubmitRageshake.js b/src/settings/useSubmitRageshake.js
new file mode 100644
index 00000000..2507e900
--- /dev/null
+++ b/src/settings/useSubmitRageshake.js
@@ -0,0 +1,209 @@
+import { useCallback, useContext } 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";
+
+export function useSubmitRageshake() {
+ const { client } = useClient();
+ const [{ json, svg }] = useContext(InspectorContext);
+
+ const submitRageshake = useCallback(
+ async (opts) => {
+ let userAgent = "UNKNOWN";
+ if (window.navigator && window.navigator.userAgent) {
+ userAgent = window.navigator.userAgent;
+ }
+
+ let touchInput = "UNKNOWN";
+ try {
+ // MDN claims broad support across browsers
+ touchInput = String(window.matchMedia("(pointer: coarse)").matches);
+ } catch (e) {}
+
+ const body = new FormData();
+ body.append(
+ "text",
+ opts.description || "User did not supply any additional text."
+ );
+ body.append("app", "matrix-video-chat");
+ body.append("version", "dev");
+ body.append("user_agent", userAgent);
+ body.append("installed_pwa", false);
+ body.append("touch_input", touchInput);
+
+ if (client) {
+ body.append("user_id", client.credentials.userId);
+ body.append("device_id", client.deviceId);
+
+ if (client.isCryptoEnabled()) {
+ const keys = [`ed25519:${client.getDeviceEd25519Key()}`];
+ if (client.getDeviceCurve25519Key) {
+ keys.push(`curve25519:${client.getDeviceCurve25519Key()}`);
+ }
+ body.append("device_keys", keys.join(", "));
+ body.append("cross_signing_key", client.getCrossSigningId());
+
+ // add cross-signing status information
+ const crossSigning = client.crypto.crossSigningInfo;
+ const secretStorage = client.crypto.secretStorage;
+
+ body.append(
+ "cross_signing_ready",
+ String(await client.isCrossSigningReady())
+ );
+ body.append(
+ "cross_signing_supported_by_hs",
+ String(
+ await client.doesServerSupportUnstableFeature(
+ "org.matrix.e2e_cross_signing"
+ )
+ )
+ );
+ body.append("cross_signing_key", crossSigning.getId());
+ body.append(
+ "cross_signing_privkey_in_secret_storage",
+ String(
+ !!(await crossSigning.isStoredInSecretStorage(secretStorage))
+ )
+ );
+
+ const pkCache = client.getCrossSigningCacheCallbacks();
+ body.append(
+ "cross_signing_master_privkey_cached",
+ String(
+ !!(pkCache && (await pkCache.getCrossSigningKeyCache("master")))
+ )
+ );
+ body.append(
+ "cross_signing_self_signing_privkey_cached",
+ String(
+ !!(
+ pkCache &&
+ (await pkCache.getCrossSigningKeyCache("self_signing"))
+ )
+ )
+ );
+ body.append(
+ "cross_signing_user_signing_privkey_cached",
+ String(
+ !!(
+ pkCache &&
+ (await pkCache.getCrossSigningKeyCache("user_signing"))
+ )
+ )
+ );
+
+ body.append(
+ "secret_storage_ready",
+ String(await client.isSecretStorageReady())
+ );
+ body.append(
+ "secret_storage_key_in_account",
+ String(!!(await secretStorage.hasKey()))
+ );
+
+ body.append(
+ "session_backup_key_in_secret_storage",
+ String(!!(await client.isKeyBackupKeyStored()))
+ );
+ const sessionBackupKeyFromCache =
+ await client.crypto.getSessionBackupPrivateKey();
+ body.append(
+ "session_backup_key_cached",
+ String(!!sessionBackupKeyFromCache)
+ );
+ body.append(
+ "session_backup_key_well_formed",
+ String(sessionBackupKeyFromCache instanceof Uint8Array)
+ );
+ }
+ }
+
+ if (opts.label) {
+ body.append("label", opts.label);
+ }
+
+ // add storage persistence/quota information
+ if (navigator.storage && navigator.storage.persisted) {
+ try {
+ body.append(
+ "storageManager_persisted",
+ String(await navigator.storage.persisted())
+ );
+ } catch (e) {}
+ } else if (document.hasStorageAccess) {
+ // Safari
+ try {
+ body.append(
+ "storageManager_persisted",
+ String(await document.hasStorageAccess())
+ );
+ } catch (e) {}
+ }
+
+ if (navigator.storage && navigator.storage.estimate) {
+ try {
+ const estimate = await navigator.storage.estimate();
+ body.append("storageManager_quota", String(estimate.quota));
+ body.append("storageManager_usage", String(estimate.usage));
+ if (estimate.usageDetails) {
+ Object.keys(estimate.usageDetails).forEach((k) => {
+ body.append(
+ `storageManager_usage_${k}`,
+ String(estimate.usageDetails[k])
+ );
+ });
+ }
+ } catch (e) {}
+ }
+
+ const logs = await rageshake.getLogsForReport();
+
+ for (const entry of logs) {
+ // encode as UTF-8
+ let buf = new TextEncoder().encode(entry.lines);
+
+ // compress
+ buf = pako.gzip(buf);
+
+ body.append("compressed-log", new Blob([buf]), entry.id);
+ }
+
+ if (json) {
+ body.append(
+ "file",
+ new Blob([JSON.stringify(json)], { type: "text/plain" }),
+ "groupcall.txt"
+ );
+ }
+
+ await fetch(
+ import.meta.env.VITE_RAGESHAKE_SUBMIT_URL ||
+ "https://element.io/bugreports/submit",
+ {
+ method: "POST",
+ body,
+ }
+ );
+ },
+ [client]
+ );
+
+ const downloadDebugLog = useCallback(() => {
+ const blob = new Blob([JSON.stringify(json)], { type: "application/json" });
+ const url = URL.createObjectURL(blob);
+ const el = document.createElement("a");
+ el.href = url;
+ el.download = "groupcall.json";
+ el.style.display = "none";
+ document.body.appendChild(el);
+ el.click();
+ setTimeout(() => {
+ URL.revokeObjectURL(url);
+ el.parentNode.removeChild(el);
+ }, 0);
+ });
+
+ return { submitRageshake, downloadDebugLog };
+}
diff --git a/yarn.lock b/yarn.lock
index 6a2e98c0..8f363095 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -9273,7 +9273,7 @@ p-try@^2.0.0:
resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6"
integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==
-pako@^2.0.3:
+pako@^2.0.3, pako@^2.0.4:
version "2.0.4"
resolved "https://registry.yarnpkg.com/pako/-/pako-2.0.4.tgz#6cebc4bbb0b6c73b0d5b8d7e8476e2b2fbea576d"
integrity sha512-v8tweI900AUkZN6heMU/4Uy4cXRc2AYNRggVmTR+dEncawDJgCdLMximOVA2p4qO57WMynangsfGRb5WD6L1Bg==