diff --git a/src/appState.ts b/src/appState.ts index f77e6cffa..42fbf9f0d 100644 --- a/src/appState.ts +++ b/src/appState.ts @@ -73,6 +73,7 @@ export const getDefaultAppState = (): Omit< zenModeEnabled: false, zoom: { value: 1 as NormalizedZoomValue, translation: { x: 0, y: 0 } }, viewModeEnabled: false, + networkSpeed: "calculating...", }; }; @@ -153,6 +154,7 @@ const APP_STATE_STORAGE_CONF = (< zenModeEnabled: { browser: true, export: false }, zoom: { browser: true, export: false }, viewModeEnabled: { browser: false, export: false }, + networkSpeed: { browser: false, export: false }, }); const _clearAppStateForStorage = ( diff --git a/src/components/App.tsx b/src/components/App.tsx index fd6759fbe..0a8164465 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -1,3 +1,4 @@ +// @ts-nocheck import { Point, simplify } from "points-on-curve"; import React from "react"; import { RoughCanvas } from "roughjs/bin/canvas"; @@ -182,6 +183,7 @@ import LayerUI from "./LayerUI"; import { Stats } from "./Stats"; import { Toast } from "./Toast"; import { actionToggleViewMode } from "../actions/actionToggleViewMode"; +import { getNetworkSpeed } from "../networkStats"; const { history } = createHistory(); @@ -461,6 +463,7 @@ class App extends React.Component { setAppState={this.setAppState} elements={this.scene.getElements()} onClose={this.toggleStats} + isCollaborating={this.props.isCollaborating} /> )} {this.state.toastMessage !== null && ( @@ -845,6 +848,21 @@ class App extends React.Component { this.addEventListeners(); } + if ( + prevState.showStats !== this.state.showStats || + prevProps.isCollaborating !== this.props.isCollaborating + ) { + if (this.state.showStats && this.props.isCollaborating) { + this.calculateNetStats(); + navigator.connection.addEventListener("change", this.calculateNetStats); + } else { + navigator.connection.removeEventListener( + "change", + this.calculateNetStats, + ); + } + } + document .querySelector(".excalidraw") ?.classList.toggle("Appearance_dark", this.state.appearance === "dark"); @@ -970,6 +988,11 @@ class App extends React.Component { } } + private calculateNetStats = async () => { + const speed = await getNetworkSpeed(); + const networkSpeed = speed === -1 ? "Error!" : speed; + this.setState({ networkSpeed }); + }; // Copy/paste private onCut = withBatchedUpdates((event: ClipboardEvent) => { diff --git a/src/components/Stats.tsx b/src/components/Stats.tsx index 56b3e2c04..ea4f4d992 100644 --- a/src/components/Stats.tsx +++ b/src/components/Stats.tsx @@ -30,6 +30,7 @@ export const Stats = (props: { setAppState: React.Component["setState"]; elements: readonly NonDeletedExcalidrawElement[]; onClose: () => void; + isCollaborating: boolean; }) => { const isMobile = useIsMobile(); const [storageSizes, setStorageSizes] = useState({ @@ -192,6 +193,21 @@ export const Stats = (props: { {hash} + {props.isCollaborating ? ( + <> + + {t("stats.collaboration")} + + + {t("stats.collaborators")} + {props.appState.collaborators.size} + + + {t("stats.networkSpeed")} + {props.appState.networkSpeed} + + + ) : null} diff --git a/src/locales/en.json b/src/locales/en.json index 2759d4964..cd3f9dd78 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -238,7 +238,10 @@ "version": "Version", "versionCopy": "Click to copy", "versionNotAvailable": "Version not available", - "width": "Width" + "width": "Width", + "collaboration": "Collaboration", + "networkSpeed": "Network Speed", + "collaborators": "Collaborators" }, "toast": { "copyStyles": "Copied styles.", diff --git a/src/networkStats.ts b/src/networkStats.ts new file mode 100644 index 000000000..26d05c682 --- /dev/null +++ b/src/networkStats.ts @@ -0,0 +1,37 @@ +const IMAGE_URL = + "https://user-images.githubusercontent.com/11256141/107117897-76fa3880-68a3-11eb-9ec6-c214c7af373b.png"; +const IMAGE_SIZE = 4525154; // in bytes +const calculateSpeed = (startTime: number, endTime: number) => { + const duration = (endTime - startTime) / 1000; + const imageSizeInBits = IMAGE_SIZE * 8; + let speed = imageSizeInBits / duration; + const suffix = ["bps", "kbps", "mbps", "gbps"]; + let index = 0; + while (speed > 1024) { + index++; + speed = speed / 1024; + } + return `${speed.toFixed(2)} ${suffix[index]}`; +}; + +const processImage = () => { + return new Promise((resolve, reject) => { + const image = new Image(); + let endTime: number; + image.onload = () => { + endTime = new Date().getTime(); + const speed = calculateSpeed(startTime, endTime); + resolve(speed); + }; + + image.onerror = () => { + resolve(-1); + }; + + const startTime = new Date().getTime(); + image.src = `${IMAGE_URL}?t=${startTime}`; // start time acts as a cache buster so everytime new url is requested + }); +}; +export const getNetworkSpeed = async () => { + return await processImage(); +}; diff --git a/src/types.ts b/src/types.ts index 2d8089d28..d78ba3e4c 100644 --- a/src/types.ts +++ b/src/types.ts @@ -88,6 +88,7 @@ export type AppState = { appearance: "light" | "dark"; gridSize: number | null; viewModeEnabled: boolean; + networkSpeed?: string; /** top-most selected groups (i.e. does not include nested groups) */ selectedGroupIds: { [groupId: string]: boolean };