Add sequence diagrams to inspector

This commit is contained in:
Robert Long
2022-01-13 14:11:06 -08:00
parent 3c30ca5f95
commit 3d571a00c6
4 changed files with 980 additions and 179 deletions

View File

@@ -1,7 +1,10 @@
import { Resizable } from "re-resizable";
import React, { useEffect, useState, useMemo } from "react";
import { useCallback } from "react";
import React, { useEffect, useState, useReducer, useRef } from "react";
import ReactJson from "react-json-view";
import mermaid from "mermaid";
import styles from "./GroupCallInspector.module.css";
import { SelectInput } from "../input/SelectInput";
import { Item } from "@react-stately/collections";
function getCallUserId(call) {
return call.getOpponentMember()?.userId || call.invitee || null;
@@ -27,7 +30,7 @@ const dateFormatter = new Intl.DateTimeFormat([], {
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
fractionalSecondDigits: 2,
fractionalSecondDigits: 3,
});
const defaultCollapsedFields = [
@@ -45,212 +48,400 @@ function shouldCollapse({ name, src, type, namespace }) {
return defaultCollapsedFields.includes(name);
}
export function GroupCallInspector({ client, groupCall, show }) {
const [roomStateEvents, setRoomStateEvents] = useState([]);
const [toDeviceEvents, setToDeviceEvents] = useState([]);
const [sentVoipEvents, setSentVoipEvents] = useState([]);
const [state, setState] = useState({
userId: client.getUserId(),
});
function getUserName(userId) {
const match = userId.match("@(.+):");
const updateState = useCallback(
(next) => setState((prev) => ({ ...prev, ...next })),
[]
return match && match.length > 0 ? match[1].replace("-", " ") : userId;
}
function formatContent(type, content) {
if (type === "m.call.invite") {
return `callId: ${content.call_id.slice(-4)} deviceId: ${
content.device_id
} sessionId: ${content.session_id}`;
} else if (type === "m.call.answer") {
return `callId: ${content.call_id.slice(-4)} deviceId: ${
content.device_id
} sessionId: ${content.session_id}`;
} else if (type === "m.call.select_answer") {
return `callId: ${content.call_id.slice(-4)} deviceId: ${
content.device_id
} sessionId: ${content.session_id}`;
} else if (type === "m.call.candidates") {
return `callId: ${content.call_id.slice(-4)} deviceId: ${
content.device_id
} sessionId: ${content.session_id}`;
} else if (type === "m.call.hangup") {
return `callId: ${content.call_id.slice(-4)} reason: ${
content.reason
} deviceId: ${content.device_id} sessionId: ${content.session_id}`;
} else if (type === "org.matrix.msc3401.call.member") {
const call =
content["m.calls"] &&
content["m.calls"].length > 0 &&
content["m.calls"][0];
const device =
call &&
call["m.devices"] &&
call["m.devices"].length > 0 &&
call["m.devices"][0];
return `callId: ${call && call["m.call_id"].slice(-4)} deviceId: ${
device && device.device_id
} sessionId: ${device && device.session_id}`;
} else {
return "";
}
}
function formatTimestamp(timestamp) {
return dateFormatter.format(timestamp);
}
function SequenceDiagramViewer({
localUserId,
remoteUserIds,
selectedUserId,
onSelectUserId,
events,
}) {
const mermaidElRef = useRef();
useEffect(() => {
mermaid.initialize({
startOnLoad: true,
theme: "dark",
sequence: {
showSequenceNumbers: true,
},
});
}, []);
useEffect(() => {
const graphDefinition = `sequenceDiagram
participant ${getUserName(localUserId)}
participant Room
participant ${selectedUserId ? getUserName(selectedUserId) : "unknown"}
${
events
? events
.map(
({ to, from, timestamp, type, content }) =>
`${getUserName(from)} ->> ${getUserName(
to
)}: ${formatTimestamp(timestamp)} ${type} ${formatContent(
type,
content
)}`
)
.join("\n ")
: ""
}
`;
mermaid.mermaidAPI.render("mermaid", graphDefinition, (svgCode) => {
mermaidElRef.current.innerHTML = svgCode;
});
}, [events, localUserId, selectedUserId]);
return (
<div className={styles.scrollContainer}>
<div className={styles.sequenceDiagramViewer}>
<SelectInput
className={styles.selectInput}
label="Remote User"
selectedKey={selectedUserId}
onSelectionChange={onSelectUserId}
>
{remoteUserIds.map((userId) => (
<Item key={userId}>{userId}</Item>
))}
</SelectInput>
<div id="mermaid" />
<div ref={mermaidElRef} />
</div>
</div>
);
}
function reducer(state, action) {
switch (action.type) {
case "receive_room_state_event": {
const { event, callStateEvent, memberStateEvents } = action;
let eventsByUserId = state.eventsByUserId;
let remoteUserIds = state.remoteUserIds;
if (event) {
const fromId = event.getStateKey();
eventsByUserId = new Map(state.eventsByUserId);
if (event.getStateKey() === state.localUserId) {
for (const userId in eventsByUserId) {
eventsByUserId.set(userId, [
...(eventsByUserId.get(userId) || []),
{
from: fromId,
to: "Room",
type: event.getType(),
content: event.getContent(),
timestamp: event.getTs() || Date.now(),
},
]);
}
} else {
eventsByUserId.set(fromId, [
...(eventsByUserId.get(fromId) || []),
{
from: fromId,
to: "Room",
type: event.getType(),
content: event.getContent(),
timestamp: event.getTs() || Date.now(),
},
]);
}
remoteUserIds =
fromId === state.localUserId || eventsByUserId.has(fromId)
? state.remoteUserIds
: [...state.remoteUserIds, fromId];
}
return {
...state,
eventsByUserId,
remoteUserIds,
callStateEvent: callStateEvent.getContent(),
memberStateEvents: Object.fromEntries(
memberStateEvents.map((e) => [e.getStateKey(), e.getContent()])
),
};
}
case "receive_to_device_event": {
const event = action.event;
const eventsByUserId = new Map(state.eventsByUserId);
const fromId = event.getSender();
const toId = state.localUserId;
const remoteUserIds = eventsByUserId.has(fromId)
? state.remoteUserIds
: [...state.remoteUserIds, fromId];
eventsByUserId.set(fromId, [
...(eventsByUserId.get(fromId) || []),
{
from: fromId,
to: toId,
type: event.getType(),
content: event.getContent(),
timestamp: event.getTs() || Date.now(),
},
]);
return { ...state, eventsByUserId, remoteUserIds };
}
case "send_voip_event": {
const event = action.event;
const eventsByUserId = new Map(state.eventsByUserId);
const fromId = state.localUserId;
const toId = event.userId;
const remoteUserIds = eventsByUserId.has(toId)
? state.remoteUserIds
: [...state.remoteUserIds, toId];
eventsByUserId.set(toId, [
...(eventsByUserId.get(toId) || []),
{
from: fromId,
to: toId,
type: event.eventType,
content: event.content,
timestamp: Date.now(),
},
]);
return { ...state, eventsByUserId, remoteUserIds };
}
default:
return state;
}
}
function useGroupCallState(client, groupCall, pollCallStats) {
const [state, dispatch] = useReducer(reducer, {
groupCall,
localUserId: client.getUserId(),
eventsByUserId: new Map(),
remoteUserIds: [],
callStateEvent: null,
memberStateEvents: {},
});
useEffect(() => {
function onUpdateRoomState(event) {
if (event) {
setRoomStateEvents((prev) => [
...prev,
{
timestamp: dateFormatter.format(new Date()),
eventType: event.getType(),
stateKey: event.getStateKey(),
content: event.getContent(),
},
]);
}
const roomEvent = groupCall.room.currentState
.getStateEvents("org.matrix.msc3401.call", groupCall.groupCallId)
.getContent();
const memberEvents = Object.fromEntries(
groupCall.room.currentState
.getStateEvents("org.matrix.msc3401.call.member")
.map((event) => [event.getStateKey(), event.getContent()])
const callStateEvent = groupCall.room.currentState.getStateEvents(
"org.matrix.msc3401.call",
groupCall.groupCallId
);
updateState({
["org.matrix.msc3401.call"]: roomEvent,
["org.matrix.msc3401.call.member"]: memberEvents,
const memberStateEvents = groupCall.room.currentState.getStateEvents(
"org.matrix.msc3401.call.member"
);
dispatch({
type: "receive_room_state_event",
event,
callStateEvent,
memberStateEvents,
});
}
function onCallsChanged() {
const calls = groupCall.calls.reduce((obj, call) => {
obj[
`${call.callId} (${call.getOpponentMember()?.userId || call.sender})`
] = getCallState(call);
return obj;
}, {});
// function onCallsChanged() {
// const calls = groupCall.calls.reduce((obj, call) => {
// obj[
// `${call.callId} (${call.getOpponentMember()?.userId || call.sender})`
// ] = getCallState(call);
// return obj;
// }, {});
updateState({ calls });
}
// updateState({ calls });
// }
function onCallHangup(call) {
setState(({ hangupCalls, ...rest }) => ({
...rest,
hangupCalls: {
...hangupCalls,
[`${call.callId} (${
call.getOpponentMember()?.userId || call.sender
})`]: getHangupCallState(call),
},
}));
}
// function onCallHangup(call) {
// setState(({ hangupCalls, ...rest }) => ({
// ...rest,
// hangupCalls: {
// ...hangupCalls,
// [`${call.callId} (${
// call.getOpponentMember()?.userId || call.sender
// })`]: getHangupCallState(call),
// },
// }));
// dispatch({ type: "call_hangup", call });
// }
function onToDeviceEvent(event) {
const eventType = event.getType();
if (
!(
eventType.startsWith("m.call.") ||
eventType.startsWith("org.matrix.call.")
)
) {
return;
}
const content = event.getContent();
if (content.conf_id && content.conf_id !== groupCall.groupCallId) {
return;
}
setToDeviceEvents((prev) => [
...prev,
{
timestamp: dateFormatter.format(new Date()),
eventType,
content,
sender: event.getSender(),
},
]);
dispatch({ type: "receive_to_device_event", event });
}
function onSendVoipEvent(event) {
setSentVoipEvents((prev) => [
...prev,
{ timestamp: dateFormatter.format(new Date()), ...event },
]);
dispatch({ type: "send_voip_event", event });
}
client.on("RoomState.events", onUpdateRoomState);
groupCall.on("calls_changed", onCallsChanged);
//groupCall.on("calls_changed", onCallsChanged);
groupCall.on("send_voip_event", onSendVoipEvent);
client.on("state", onCallsChanged);
client.on("hangup", onCallHangup);
//client.on("state", onCallsChanged);
//client.on("hangup", onCallHangup);
client.on("toDeviceEvent", onToDeviceEvent);
onUpdateRoomState();
}, [client, groupCall]);
const toDeviceEventsByCall = useMemo(() => {
const result = {};
for (const event of toDeviceEvents) {
const callId = event.content.call_id;
const key = `${callId} (${event.sender})`;
result[key] = result[key] || [];
result[key].push(event);
}
return result;
}, [toDeviceEvents]);
const sentVoipEventsByCall = useMemo(() => {
const result = {};
for (const event of sentVoipEvents) {
const callId = event.content.call_id;
const key = `${callId} (${event.userId})`;
result[key] = result[key] || [];
result[key].push(event);
}
return result;
}, [sentVoipEvents]);
useEffect(() => {
let timeout;
async function updateCallStats() {
const callIds = groupCall.calls.map(
(call) =>
`${call.callId} (${call.getOpponentMember()?.userId || call.sender})`
);
const stats = await Promise.all(
groupCall.calls.map((call) =>
call.peerConn
? call.peerConn
.getStats(null)
.then((stats) =>
Object.fromEntries(
Array.from(stats).map(([_id, report], i) => [
report.type + i,
report,
])
)
)
: Promise.resolve(null)
)
);
const callStats = {};
for (let i = 0; i < groupCall.calls.length; i++) {
callStats[callIds[i]] = stats[i];
}
updateState({ callStats });
timeout = setTimeout(updateCallStats, 1000);
}
if (show) {
updateCallStats();
}
return () => {
clearTimeout(timeout);
client.removeListener("RoomState.events", onUpdateRoomState);
//groupCall.removeListener("calls_changed", onCallsChanged);
groupCall.removeListener("send_voip_event", onSendVoipEvent);
//client.removeListener("state", onCallsChanged);
//client.removeListener("hangup", onCallHangup);
client.removeListener("toDeviceEvent", onToDeviceEvent);
};
}, [show]);
}, [client, groupCall]);
// useEffect(() => {
// let timeout;
// async function updateCallStats() {
// const callIds = groupCall.calls.map(
// (call) =>
// `${call.callId} (${call.getOpponentMember()?.userId || call.sender})`
// );
// const stats = await Promise.all(
// groupCall.calls.map((call) =>
// call.peerConn
// ? call.peerConn
// .getStats(null)
// .then((stats) =>
// Object.fromEntries(
// Array.from(stats).map(([_id, report], i) => [
// report.type + i,
// report,
// ])
// )
// )
// : Promise.resolve(null)
// )
// );
// const callStats = {};
// for (let i = 0; i < groupCall.calls.length; i++) {
// callStats[callIds[i]] = stats[i];
// }
// dispatch({ type: "callStats", callStats });
// timeout = setTimeout(updateCallStats, 1000);
// }
// if (pollCallStats) {
// updateCallStats();
// }
// return () => {
// clearTimeout(timeout);
// };
// }, [pollCallStats]);
return state;
}
export function GroupCallInspector({ client, groupCall, show }) {
const [currentTab, setCurrentTab] = useState("inspector");
const [selectedUserId, setSelectedUserId] = useState();
const state = useGroupCallState(client, groupCall, show);
if (!show) {
return null;
}
return (
<Resizable enable={{ top: true }} defaultSize={{ height: 200 }}>
<ReactJson
theme="monokai"
src={{
...state,
roomStateEvents,
toDeviceEvents,
toDeviceEventsByCall,
sentVoipEvents,
sentVoipEventsByCall,
}}
name={null}
indentWidth={2}
shouldCollapse={shouldCollapse}
displayDataTypes={false}
displayObjectSize={false}
enableClipboard
style={{ height: "100%", overflowY: "scroll" }}
/>
<Resizable
enable={{ top: true }}
defaultSize={{ height: 200 }}
className={styles.inspector}
>
<div className={styles.toolbar}>
<button onClick={() => setCurrentTab("inspector")}>Inspector</button>
<button onClick={() => setCurrentTab("sequence-diagrams")}>
Sequence Diagrams
</button>
</div>
{currentTab === "sequence-diagrams" && (
<SequenceDiagramViewer
localUserId={state.localUserId}
selectedUserId={selectedUserId}
onSelectUserId={setSelectedUserId}
remoteUserIds={state.remoteUserIds}
events={state.eventsByUserId.get(selectedUserId)}
/>
)}
{currentTab === "inspector" && (
<ReactJson
theme="monokai"
src={{
...state,
eventsByUserId: Object.fromEntries(state.eventsByUserId),
}}
name={null}
indentWidth={2}
shouldCollapse={shouldCollapse}
displayDataTypes={false}
displayObjectSize={false}
enableClipboard
style={{ height: "100%", overflowY: "scroll" }}
/>
)}
</Resizable>
);
}

View File

@@ -0,0 +1,25 @@
.inspector {
background-color: var(--bgColor2);
}
.scrollContainer {
height: 100%;
overflow-y: auto;
}
.sequenceDiagramViewer {
display: flex;
flex-direction: column;
align-items: center;
padding: 20px;
}
.selectInput {
align-self: flex-start;
}
.sequenceDiagramViewer :global(.messageText) {
font-size: 12px;
fill: var(--textColor1) !important;
stroke: var(--textColor1) !important;
}