diff --git a/.storybook/main.js b/.storybook/main.js
index af43aa68..682489d9 100644
--- a/.storybook/main.js
+++ b/.storybook/main.js
@@ -18,11 +18,6 @@ module.exports = {
);
config.plugins.push(svgrPlugin());
config.resolve = config.resolve || {};
- config.resolve.alias = config.resolve.alias || {};
- config.resolve.alias["$(res)"] = path.resolve(
- __dirname,
- "../node_modules/matrix-react-sdk/res"
- );
config.resolve.dedupe = config.resolve.dedupe || [];
config.resolve.dedupe.push("react", "react-dom", "matrix-js-sdk");
return config;
diff --git a/Dockerfile b/Dockerfile
index 88551511..6125b970 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -2,13 +2,13 @@ FROM node:16-buster as builder
WORKDIR /src
-COPY . /src/matrix-video-chat
-RUN matrix-video-chat/scripts/dockerbuild.sh
+COPY . /src/element-call
+RUN element-call/scripts/dockerbuild.sh
# App
FROM nginxinc/nginx-unprivileged:alpine
-COPY --from=builder /src/matrix-video-chat/dist /app
+COPY --from=builder /src/element-call/dist /app
COPY scripts/default.conf /etc/nginx/conf.d/
USER root
diff --git a/README.md b/README.md
index d522d0f3..75c9a5e6 100644
--- a/README.md
+++ b/README.md
@@ -6,7 +6,7 @@ Discussion in [#webrtc:matrix.org: ![#webrtc:matrix.org](https://img.shields.io/
## Getting Started
-`element-call` is built against the `robertlong/group-call` branch of both [matrix-js-sdk](https://github.com/matrix-org/matrix-js-sdk/pull/1902) and [matrix-react-sdk](https://github.com/matrix-org/matrix-react-sdk/pull/6848). Because of how these packages are configured and Vite's requirements, you will need to clone them locally and use `yarn link` to stich things together.
+`element-call` is built against the `robertlong/group-call` branch of [matrix-js-sdk](https://github.com/matrix-org/matrix-js-sdk/pull/1902). Because of how this package is configured and Vite's requirements, you will need to clone it locally and use `yarn link` to stich things together.
First clone, install, and link `matrix-js-sdk`
@@ -18,17 +18,6 @@ yarn
yarn link
```
-Then clone, install, link `matrix-js-sdk` into `matrix-react-sdk`, and link `matrix-react-sdk`
-
-```
-git clone https://github.com/matrix-org/matrix-react-sdk.git
-cd matrix-react-sdk
-git checkout robertlong/group-call
-yarn
-yarn link matrix-js-sdk
-yarn link
-```
-
Next you'll also need [Synapse](https://matrix-org.github.io/synapse/latest/setup/installation.html) installed locally and running on port 8008.
Finally we can set up this project.
@@ -38,7 +27,6 @@ git clone https://github.com/vector-im/element-call.git
cd element-call
yarn
yarn link matrix-js-sdk
-yarn link matrix-react-sdk
yarn dev
```
diff --git a/package.json b/package.json
index b1012757..9471442e 100644
--- a/package.json
+++ b/package.json
@@ -18,6 +18,7 @@
"@react-aria/tabs": "^3.1.0",
"@react-aria/tooltip": "^3.1.3",
"@react-aria/utils": "^3.10.0",
+ "@react-spring/web": "^9.4.4",
"@react-stately/collections": "^3.3.4",
"@react-stately/overlays": "^3.1.3",
"@react-stately/select": "^3.1.3",
@@ -25,11 +26,12 @@
"@react-stately/tree": "^3.2.0",
"@sentry/react": "^6.13.3",
"@sentry/tracing": "^6.13.3",
+ "@use-gesture/react": "^10.2.11",
"classnames": "^2.3.1",
"color-hash": "^2.0.1",
"events": "^3.3.0",
+ "lodash-move": "^1.1.1",
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#robertlong/group-call",
- "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",
diff --git a/scripts/dockerbuild.sh b/scripts/dockerbuild.sh
index 2a10d4eb..491fc02a 100755
--- a/scripts/dockerbuild.sh
+++ b/scripts/dockerbuild.sh
@@ -13,22 +13,11 @@ git checkout robertlong/group-call
yarn install
yarn run build
yarn link
-cd ..
-git clone https://github.com/matrix-org/matrix-react-sdk.git
-cd matrix-react-sdk
-git checkout robertlong/group-call
-yarn link matrix-js-sdk
-yarn install
-yarn run build
-yarn link
-cd ..
-
-cd matrix-video-chat
+cd ../element-call
export VITE_APP_VERSION=$(git describe --tags --abbrev=0)
yarn link matrix-js-sdk
-yarn link matrix-react-sdk
yarn install
yarn run build
diff --git a/src/icons/MicMuted.svg b/src/icons/MicMuted.svg
new file mode 100644
index 00000000..0cb7ad1c
--- /dev/null
+++ b/src/icons/MicMuted.svg
@@ -0,0 +1,5 @@
+
diff --git a/src/icons/VideoMuted.svg b/src/icons/VideoMuted.svg
new file mode 100644
index 00000000..188ed08b
--- /dev/null
+++ b/src/icons/VideoMuted.svg
@@ -0,0 +1,6 @@
+
diff --git a/src/main.jsx b/src/main.jsx
index 21138b34..d620ff91 100644
--- a/src/main.jsx
+++ b/src/main.jsx
@@ -22,10 +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 { init as initRageshake } from "./settings/rageshake";
import { InspectorContextProvider } from "./room/GroupCallInspector";
-rageshake.init();
+initRageshake();
console.info(`matrix-video-chat ${import.meta.env.VITE_APP_VERSION || "dev"}`);
diff --git a/src/room/FeedbackModal.jsx b/src/room/FeedbackModal.jsx
index 34b524ee..84338d64 100644
--- a/src/room/FeedbackModal.jsx
+++ b/src/room/FeedbackModal.jsx
@@ -2,7 +2,10 @@ 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 {
+ useSubmitRageshake,
+ useRageshakeRequest,
+} from "../settings/submit-rageshake";
import { Body } from "../typography/Typography";
import { randomString } from "matrix-js-sdk/src/randomstring";
diff --git a/src/room/GroupCallView.jsx b/src/room/GroupCallView.jsx
index a7c5f507..908613fb 100644
--- a/src/room/GroupCallView.jsx
+++ b/src/room/GroupCallView.jsx
@@ -1,7 +1,7 @@
import React, { useCallback, useEffect, useState } from "react";
import { useHistory } from "react-router-dom";
import { GroupCallState } from "matrix-js-sdk/src/webrtc/groupCall";
-import { useGroupCall } from "matrix-react-sdk/src/hooks/useGroupCall";
+import { useGroupCall } from "./useGroupCall";
import { ErrorView, FullScreenView } from "../FullScreenView";
import { LobbyView } from "./LobbyView";
import { InCallView } from "./InCallView";
@@ -14,7 +14,6 @@ export function GroupCallView({
isPasswordlessUser,
roomId,
groupCall,
- simpleGrid,
}) {
const [showInspector, setShowInspector] = useState(
() => !!localStorage.getItem("matrix-group-call-inspector")
@@ -89,7 +88,6 @@ export function GroupCallView({
isScreensharing={isScreensharing}
localScreenshareFeed={localScreenshareFeed}
screenshareFeeds={screenshareFeeds}
- simpleGrid={simpleGrid}
setShowInspector={onChangeShowInspector}
showInspector={showInspector}
roomId={roomId}
diff --git a/src/room/InCallView.jsx b/src/room/InCallView.jsx
index 563c11f8..ea194f06 100644
--- a/src/room/InCallView.jsx
+++ b/src/room/InCallView.jsx
@@ -7,19 +7,15 @@ import {
ScreenshareButton,
} from "../button";
import { Header, LeftNav, RightNav, RoomHeaderInfo } from "../Header";
-import VideoGrid, {
- useVideoGridLayout,
-} from "matrix-react-sdk/src/components/views/voip/GroupCallView/VideoGrid";
-import { VideoTileContainer } from "matrix-react-sdk/src/components/views/voip/GroupCallView/VideoTileContainer";
-import SimpleVideoGrid from "matrix-react-sdk/src/components/views/voip/GroupCallView/SimpleVideoGrid";
-import "matrix-react-sdk/res/css/views/voip/GroupCallView/_VideoGrid.scss";
+import { VideoGrid, useVideoGridLayout } from "../video-grid/VideoGrid";
+import { VideoTileContainer } from "../video-grid/VideoTileContainer";
import { getAvatarUrl } from "../matrix-utils";
import { GroupCallInspector } from "./GroupCallInspector";
import { OverflowMenu } from "./OverflowMenu";
import { GridLayoutMenu } from "./GridLayoutMenu";
import { Avatar } from "../Avatar";
import { UserMenuContainer } from "../UserMenuContainer";
-import { useRageshakeRequestModal } from "../settings/rageshake";
+import { useRageshakeRequestModal } from "../settings/submit-rageshake";
import { RageshakeRequestModal } from "./RageshakeRequestModal";
import { usePreventScroll } from "@react-aria/overlays";
import { useMediaHandler } from "../settings/useMediaHandler";
@@ -44,7 +40,6 @@ export function InCallView({
toggleScreensharing,
isScreensharing,
screenshareFeeds,
- simpleGrid,
setShowInspector,
showInspector,
roomId,
@@ -149,8 +144,6 @@ export function InCallView({
Waiting for other participants...
- ) : simpleGrid ? (
-
) : (
{
+ const [viaServers] = useMemo(() => {
const params = new URLSearchParams(search);
- return [params.has("simple"), params.getAll("via")];
+ return [params.getAll("via")];
}, [search]);
const roomId = (maybeRoomId || hash || "").toLowerCase();
@@ -56,7 +56,6 @@ export function RoomPage() {
roomId={roomId}
groupCall={groupCall}
isPasswordlessUser={isPasswordlessUser}
- simpleGrid={simpleGrid}
/>
)}
diff --git a/src/room/useGroupCall.js b/src/room/useGroupCall.js
new file mode 100644
index 00000000..d5c535c2
--- /dev/null
+++ b/src/room/useGroupCall.js
@@ -0,0 +1,252 @@
+import { useCallback, useEffect, useState } from "react";
+import {
+ GroupCallEvent,
+ GroupCallState,
+} from "matrix-js-sdk/src/webrtc/groupCall";
+import { usePageUnload } from "./usePageUnload";
+
+export function useGroupCall(groupCall) {
+ const [
+ {
+ state,
+ calls,
+ localCallFeed,
+ activeSpeaker,
+ userMediaFeeds,
+ error,
+ microphoneMuted,
+ localVideoMuted,
+ isScreensharing,
+ screenshareFeeds,
+ localScreenshareFeed,
+ localDesktopCapturerSourceId,
+ participants,
+ hasLocalParticipant,
+ requestingScreenshare,
+ },
+ setState,
+ ] = useState({
+ state: GroupCallState.LocalCallFeedUninitialized,
+ calls: [],
+ userMediaFeeds: [],
+ microphoneMuted: false,
+ localVideoMuted: false,
+ screenshareFeeds: [],
+ isScreensharing: false,
+ requestingScreenshare: false,
+ participants: [],
+ hasLocalParticipant: false,
+ });
+
+ const updateState = (state) =>
+ setState((prevState) => ({ ...prevState, ...state }));
+
+ useEffect(() => {
+ function onGroupCallStateChanged() {
+ updateState({
+ state: groupCall.state,
+ calls: [...groupCall.calls],
+ localCallFeed: groupCall.localCallFeed,
+ activeSpeaker: groupCall.activeSpeaker,
+ userMediaFeeds: [...groupCall.userMediaFeeds],
+ microphoneMuted: groupCall.isMicrophoneMuted(),
+ localVideoMuted: groupCall.isLocalVideoMuted(),
+ isScreensharing: groupCall.isScreensharing(),
+ localScreenshareFeed: groupCall.localScreenshareFeed,
+ localDesktopCapturerSourceId: groupCall.localDesktopCapturerSourceId,
+ screenshareFeeds: [...groupCall.screenshareFeeds],
+ participants: [...groupCall.participants],
+ });
+ }
+
+ function onUserMediaFeedsChanged(userMediaFeeds) {
+ updateState({
+ userMediaFeeds: [...userMediaFeeds],
+ });
+ }
+
+ function onScreenshareFeedsChanged(screenshareFeeds) {
+ updateState({
+ screenshareFeeds: [...screenshareFeeds],
+ });
+ }
+
+ function onActiveSpeakerChanged(activeSpeaker) {
+ updateState({
+ activeSpeaker: activeSpeaker,
+ });
+ }
+
+ function onLocalMuteStateChanged(microphoneMuted, localVideoMuted) {
+ updateState({
+ microphoneMuted,
+ localVideoMuted,
+ });
+ }
+
+ function onLocalScreenshareStateChanged(
+ isScreensharing,
+ localScreenshareFeed,
+ localDesktopCapturerSourceId
+ ) {
+ updateState({
+ isScreensharing,
+ localScreenshareFeed,
+ localDesktopCapturerSourceId,
+ });
+ }
+
+ function onCallsChanged(calls) {
+ updateState({
+ calls: [...calls],
+ });
+ }
+
+ function onParticipantsChanged(participants) {
+ updateState({
+ participants: [...participants],
+ hasLocalParticipant: groupCall.hasLocalParticipant(),
+ });
+ }
+
+ groupCall.on(GroupCallEvent.GroupCallStateChanged, onGroupCallStateChanged);
+ groupCall.on(GroupCallEvent.UserMediaFeedsChanged, onUserMediaFeedsChanged);
+ groupCall.on(
+ GroupCallEvent.ScreenshareFeedsChanged,
+ onScreenshareFeedsChanged
+ );
+ groupCall.on(GroupCallEvent.ActiveSpeakerChanged, onActiveSpeakerChanged);
+ groupCall.on(GroupCallEvent.LocalMuteStateChanged, onLocalMuteStateChanged);
+ groupCall.on(
+ GroupCallEvent.LocalScreenshareStateChanged,
+ onLocalScreenshareStateChanged
+ );
+ groupCall.on(GroupCallEvent.CallsChanged, onCallsChanged);
+ groupCall.on(GroupCallEvent.ParticipantsChanged, onParticipantsChanged);
+
+ updateState({
+ error: null,
+ state: groupCall.state,
+ calls: [...groupCall.calls],
+ localCallFeed: groupCall.localCallFeed,
+ activeSpeaker: groupCall.activeSpeaker,
+ userMediaFeeds: [...groupCall.userMediaFeeds],
+ microphoneMuted: groupCall.isMicrophoneMuted(),
+ localVideoMuted: groupCall.isLocalVideoMuted(),
+ isScreensharing: groupCall.isScreensharing(),
+ localScreenshareFeed: groupCall.localScreenshareFeed,
+ localDesktopCapturerSourceId: groupCall.localDesktopCapturerSourceId,
+ screenshareFeeds: [...groupCall.screenshareFeeds],
+ participants: [...groupCall.participants],
+ hasLocalParticipant: groupCall.hasLocalParticipant(),
+ });
+
+ return () => {
+ groupCall.removeListener(
+ GroupCallEvent.GroupCallStateChanged,
+ onGroupCallStateChanged
+ );
+ groupCall.removeListener(
+ GroupCallEvent.UserMediaFeedsChanged,
+ onUserMediaFeedsChanged
+ );
+ groupCall.removeListener(
+ GroupCallEvent.ScreenshareFeedsChanged,
+ onScreenshareFeedsChanged
+ );
+ groupCall.removeListener(
+ GroupCallEvent.ActiveSpeakerChanged,
+ onActiveSpeakerChanged
+ );
+ groupCall.removeListener(
+ GroupCallEvent.LocalMuteStateChanged,
+ onLocalMuteStateChanged
+ );
+ groupCall.removeListener(
+ GroupCallEvent.LocalScreenshareStateChanged,
+ onLocalScreenshareStateChanged
+ );
+ groupCall.removeListener(GroupCallEvent.CallsChanged, onCallsChanged);
+ groupCall.removeListener(
+ GroupCallEvent.ParticipantsChanged,
+ onParticipantsChanged
+ );
+ groupCall.leave();
+ };
+ }, [groupCall]);
+
+ usePageUnload(() => {
+ groupCall.leave();
+ });
+
+ const initLocalCallFeed = useCallback(
+ () => groupCall.initLocalCallFeed(),
+ [groupCall]
+ );
+
+ const enter = useCallback(() => {
+ if (
+ groupCall.state !== GroupCallState.LocalCallFeedUninitialized &&
+ groupCall.state !== GroupCallState.LocalCallFeedInitialized
+ ) {
+ return;
+ }
+
+ groupCall.enter().catch((error) => {
+ console.error(error);
+ updateState({ error });
+ });
+ }, [groupCall]);
+
+ const leave = useCallback(() => groupCall.leave(), [groupCall]);
+
+ const toggleLocalVideoMuted = useCallback(() => {
+ groupCall.setLocalVideoMuted(!groupCall.isLocalVideoMuted());
+ }, [groupCall]);
+
+ const toggleMicrophoneMuted = useCallback(() => {
+ groupCall.setMicrophoneMuted(!groupCall.isMicrophoneMuted());
+ }, [groupCall]);
+
+ const toggleScreensharing = useCallback(() => {
+ updateState({ requestingScreenshare: true });
+
+ groupCall.setScreensharingEnabled(!groupCall.isScreensharing()).then(() => {
+ updateState({ requestingScreenshare: false });
+ });
+ }, [groupCall]);
+
+ useEffect(() => {
+ if (window.RTCPeerConnection === undefined) {
+ const error = new Error(
+ "WebRTC is not supported or is being blocked in this browser."
+ );
+ console.error(error);
+ updateState({ error });
+ }
+ }, []);
+
+ return {
+ state,
+ calls,
+ localCallFeed,
+ activeSpeaker,
+ userMediaFeeds,
+ microphoneMuted,
+ localVideoMuted,
+ error,
+ initLocalCallFeed,
+ enter,
+ leave,
+ toggleLocalVideoMuted,
+ toggleMicrophoneMuted,
+ toggleScreensharing,
+ requestingScreenshare,
+ isScreensharing,
+ screenshareFeeds,
+ localScreenshareFeed,
+ localDesktopCapturerSourceId,
+ participants,
+ hasLocalParticipant,
+ };
+}
diff --git a/src/room/usePageUnload.js b/src/room/usePageUnload.js
new file mode 100644
index 00000000..2d685201
--- /dev/null
+++ b/src/room/usePageUnload.js
@@ -0,0 +1,54 @@
+import { useEffect } from "react";
+
+// https://stackoverflow.com/a/9039885
+function isIOS() {
+ return (
+ [
+ "iPad Simulator",
+ "iPhone Simulator",
+ "iPod Simulator",
+ "iPad",
+ "iPhone",
+ "iPod",
+ ].includes(navigator.platform) ||
+ // iPad on iOS 13 detection
+ (navigator.userAgent.includes("Mac") && "ontouchend" in document)
+ );
+}
+
+export function usePageUnload(callback) {
+ useEffect(() => {
+ let pageVisibilityTimeout;
+
+ function onBeforeUnload(event) {
+ if (event.type === "visibilitychange") {
+ if (document.visibilityState === "visible") {
+ clearTimeout(pageVisibilityTimeout);
+ } else {
+ // Wait 5 seconds before closing the page to avoid accidentally leaving
+ // TODO: Make this configurable?
+ pageVisibilityTimeout = setTimeout(() => {
+ callback();
+ }, 5000);
+ }
+ } else {
+ callback();
+ }
+ }
+
+ // iOS doesn't fire beforeunload event, so leave the call when you hide the page.
+ if (isIOS()) {
+ window.addEventListener("pagehide", onBeforeUnload);
+ document.addEventListener("visibilitychange", onBeforeUnload);
+ }
+
+ window.addEventListener("beforeunload", onBeforeUnload);
+
+ return () => {
+ window.removeEventListener("pagehide", onBeforeUnload);
+ document.removeEventListener("visibilitychange", onBeforeUnload);
+ window.removeEventListener("beforeunload", onBeforeUnload);
+ clearTimeout(pageVisibilityTimeout);
+ };
+ }, [callback]);
+}
diff --git a/src/settings/SettingsModal.jsx b/src/settings/SettingsModal.jsx
index 2945daca..fc57c624 100644
--- a/src/settings/SettingsModal.jsx
+++ b/src/settings/SettingsModal.jsx
@@ -10,7 +10,7 @@ import { Item } from "@react-stately/collections";
import { useMediaHandler } from "./useMediaHandler";
import { FieldRow, InputField } from "../input/Input";
import { Button } from "../button";
-import { useDownloadDebugLog } from "./rageshake";
+import { useDownloadDebugLog } from "./submit-rageshake";
import { Body } from "../typography/Typography";
export function SettingsModal({ setShowInspector, showInspector, ...rest }) {
diff --git a/src/settings/rageshake.js b/src/settings/rageshake.js
index 80ee5399..c9e855ee 100644
--- a/src/settings/rageshake.js
+++ b/src/settings/rageshake.js
@@ -1,300 +1,535 @@
-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";
+/*
+Copyright 2017 OpenMarket Ltd
+Copyright 2018 New Vector Ltd
+Copyright 2019 The Matrix.org Foundation C.I.C.
-export function useSubmitRageshake() {
- const { client } = useClient();
- const [{ json }] = useContext(InspectorContext);
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
- const [{ sending, sent, error }, setState] = useState({
- sending: false,
- sent: false,
- error: null,
- });
+ http://www.apache.org/licenses/LICENSE-2.0
- const submitRageshake = useCallback(
- async (opts) => {
- if (sending) {
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// This module contains all the code needed to log the console, persist it to
+// disk and submit bug reports. Rationale is as follows:
+// - Monkey-patching the console is preferable to having a log library because
+// we can catch logs by other libraries more easily, without having to all
+// depend on the same log framework / pass the logger around.
+// - We use IndexedDB to persists logs because it has generous disk space
+// limits compared to local storage. IndexedDB does not work in incognito
+// mode, in which case this module will not be able to write logs to disk.
+// However, the logs will still be stored in-memory, so can still be
+// submitted in a bug report should the user wish to: we can also store more
+// logs in-memory than in local storage, which does work in incognito mode.
+// We also need to handle the case where there are 2+ tabs. Each JS runtime
+// generates a random string which serves as the "ID" for that tab/session.
+// These IDs are stored along with the log lines.
+// - Bug reports are sent as a POST over HTTPS: it purposefully does not use
+// Matrix as bug reports may be made when Matrix is not responsive (which may
+// be the cause of the bug). We send the most recent N MB of UTF-8 log data,
+// starting with the most recent, which we know because the "ID"s are
+// actually timestamps. We then purge the remaining logs. We also do this
+// purge on startup to prevent logs from accumulating.
+
+// the frequency with which we flush to indexeddb
+import { logger } from "matrix-js-sdk/src/logger";
+
+const FLUSH_RATE_MS = 30 * 1000;
+
+// the length of log data we keep in indexeddb (and include in the reports)
+const MAX_LOG_SIZE = 1024 * 1024 * 5; // 5 MB
+
+// A class which monkey-patches the global console and stores log lines.
+export class ConsoleLogger {
+ logs = "";
+
+ monkeyPatch(consoleObj) {
+ // Monkey-patch console logging
+ const consoleFunctionsToLevels = {
+ log: "I",
+ info: "I",
+ warn: "W",
+ error: "E",
+ };
+ Object.keys(consoleFunctionsToLevels).forEach((fnName) => {
+ const level = consoleFunctionsToLevels[fnName];
+ const originalFn = consoleObj[fnName].bind(consoleObj);
+ consoleObj[fnName] = (...args) => {
+ this.log(level, ...args);
+ originalFn(...args);
+ };
+ });
+ }
+
+ log(level, ...args) {
+ // We don't know what locale the user may be running so use ISO strings
+ const ts = new Date().toISOString();
+
+ // Convert objects and errors to helpful things
+ args = args.map((arg) => {
+ if (arg instanceof DOMException) {
+ return arg.message + ` (${arg.name} | ${arg.code})`;
+ } else if (arg instanceof Error) {
+ return arg.message + (arg.stack ? `\n${arg.stack}` : "");
+ } else if (typeof arg === "object") {
+ try {
+ return JSON.stringify(arg);
+ } catch (e) {
+ // In development, it can be useful to log complex cyclic
+ // objects to the console for inspection. This is fine for
+ // the console, but default `stringify` can't handle that.
+ // We workaround this by using a special replacer function
+ // to only log values of the root object and avoid cycles.
+ return JSON.stringify(arg, (key, value) => {
+ if (key && typeof value === "object") {
+ return "