Add more events for sharing and refactor I/O, dialogs (#2443)

This commit is contained in:
Lipis 2020-12-03 17:03:02 +02:00 committed by GitHub
parent c43109a230
commit 66e5b18e4e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 87 additions and 75 deletions

View File

@ -3,21 +3,19 @@
| Shape / Selection | shape | selection, rectangle, diamond, etc | `toolbar` or `shortcut` |
| Text on double click | shape | text | `double-click` |
| Lock selection | shape | lock | `on` or `off` |
| Load file | action | load | `MIME type` |
| Import from URL | action | import |
| Save | action | save |
| Save as | action | save as |
| Clear canvas | action | clear canvas |
| Zoom in | action | zoom | in | `zoom` |
| Zoom out | action | zoom | out | `zoom` |
| Zoom fit | action | zoom | fit | `zoom` |
| Zoom reset | action | zoom | reset | `zoom` |
| Export dialog | action | export | dialog |
| Export to backend | action | export | backend |
| Export as SVG | action | export | `svg` or `clipboard-svg` |
| Export to PNG | action | export | `png` or `clipboard-png` |
| Scroll back to content | action | scroll to content |
| Open shortcut menu | action | keyboard shortcuts |
| Load file | io | load | `MIME type` |
| Import from URL | io | import |
| Save | io | save |
| Save as | io | save as |
| Export to backend | io | export | backend |
| Export as SVG | io | export | `svg` or `clipboard-svg` |
| Export to PNG | io | export | `png` or `clipboard-png` |
| Canvas color | change | canvas color | `color` |
| Background color | change | background color | `color` |
| Stroke color | change | stroke color | `color` |
@ -42,6 +40,15 @@
| Center vertically | align | vertically | `center` |
| Distribute horizontally | align | distribute | `horizontally` |
| Distribute vertically | align | distribute | `vertically` |
| Start session | share | session start |
| Join session | share | session join |
| Start end | share | session end |
| Copy room link | share | copy link |
| Go to collaborator | share | go to collaborator |
| Change name | share | name |
| Shortcuts dialog | dialog | shortcuts |
| Collaboration dialog | dialog | collaboration |
| Export dialog | dialog | export |
| E2EE shield | exit | e2ee shield |
| GitHub corner | exit | github |
| Excalidraw blog | exit | blog |

View File

@ -1,14 +1,14 @@
import React from "react";
import { ProjectName } from "../components/ProjectName";
import { saveAsJSON, loadFromJSON } from "../data";
import { EVENT_CHANGE, EVENT_IO, trackEvent } from "../analytics";
import { load, save, saveAs } from "../components/icons";
import { ProjectName } from "../components/ProjectName";
import { ToolButton } from "../components/ToolButton";
import { loadFromJSON, saveAsJSON } from "../data";
import { t } from "../i18n";
import useIsMobile from "../is-mobile";
import { register } from "./register";
import { KEYS } from "../keys";
import { muteFSAbortError } from "../utils";
import { EVENT_ACTION, EVENT_CHANGE, trackEvent } from "../analytics";
import { register } from "./register";
export const actionChangeProjectName = register({
name: "changeProjectName",
@ -90,7 +90,7 @@ export const actionSaveScene = register({
perform: async (elements, appState, value) => {
try {
const { fileHandle } = await saveAsJSON(elements, appState);
trackEvent(EVENT_ACTION, "save");
trackEvent(EVENT_IO, "save");
return { commitToHistory: false, appState: { ...appState, fileHandle } };
} catch (error) {
if (error?.name !== "AbortError") {
@ -121,7 +121,7 @@ export const actionSaveAsScene = register({
...appState,
fileHandle: null,
});
trackEvent(EVENT_ACTION, "save as");
trackEvent(EVENT_IO, "save as");
return { commitToHistory: false, appState: { ...appState, fileHandle } };
} catch (error) {
if (error?.name !== "AbortError") {

View File

@ -7,7 +7,7 @@ import { register } from "./register";
import { allowFullScreen, exitFullScreen, isFullScreen } from "../utils";
import { CODES, KEYS } from "../keys";
import { HelpIcon } from "../components/HelpIcon";
import { EVENT_ACTION, trackEvent } from "../analytics";
import { EVENT_DIALOG, trackEvent } from "../analytics";
export const actionToggleCanvasMenu = register({
name: "toggleCanvasMenu",
@ -72,7 +72,7 @@ export const actionFullScreen = register({
export const actionShortcuts = register({
name: "toggleShortcuts",
perform: (_elements, appState) => {
trackEvent(EVENT_ACTION, "keyboard shortcuts");
trackEvent(EVENT_DIALOG, "shortcuts");
return {
appState: {
...appState,

View File

@ -4,11 +4,13 @@ import { register } from "./register";
import { getClientColors, getClientInitials } from "../clients";
import { Collaborator } from "../types";
import { centerScrollOn } from "../scene/scroll";
import { EVENT_SHARE, trackEvent } from "../analytics";
export const actionGoToCollaborator = register({
name: "goToCollaborator",
perform: (_elements, appState, value) => {
const point = value as Collaborator["pointer"];
trackEvent(EVENT_SHARE, "go to collaborator");
if (!point) {
return { appState, commitToHistory: false };
}

View File

@ -4,6 +4,9 @@ export const EVENT_CHANGE = "change";
export const EVENT_SHAPE = "shape";
export const EVENT_LAYER = "layer";
export const EVENT_ALIGN = "align";
export const EVENT_SHARE = "share";
export const EVENT_IO = "io";
export const EVENT_DIALOG = "dialog";
export const trackEvent = window.gtag
? (category: string, name: string, label?: string, value?: number) => {

View File

@ -181,7 +181,7 @@ import {
isSavedToFirebase,
} from "../data/firebase";
import { getNewZoom } from "../scene/zoom";
import { EVENT_SHAPE, trackEvent } from "../analytics";
import { EVENT_SHAPE, EVENT_SHARE, trackEvent } from "../analytics";
/**
* @param func handler taking at most single parameter (event).
@ -657,8 +657,8 @@ class App extends React.Component<ExcalidrawProps, AppState> {
// when joining a room we don't want user's local scene data to be merged
// into the remote scene
this.resetScene();
this.initializeSocketClient({ showLoadingState: true });
trackEvent(EVENT_SHARE, "session join");
} else if (scene) {
if (scene.appState) {
scene.appState = {
@ -1262,12 +1262,14 @@ class App extends React.Component<ExcalidrawProps, AppState> {
this.scene.replaceAllElements(this.scene.getElements());
await this.initializeSocketClient({ showLoadingState: false });
trackEvent(EVENT_SHARE, "session start");
};
closePortal = () => {
this.saveCollabRoomToFirebase();
window.history.pushState({}, "Excalidraw", window.location.origin);
this.destroySocketClient();
trackEvent(EVENT_SHARE, "session end");
};
toggleLock = () => {

View File

@ -1,24 +1,21 @@
import "./ExportDialog.scss";
import React, { useState, useEffect, useRef } from "react";
import React, { useEffect, useRef, useState } from "react";
import { render, unmountComponentAtNode } from "react-dom";
import { ToolButton } from "./ToolButton";
import { clipboard, exportFile, link } from "./icons";
import { NonDeletedExcalidrawElement } from "../element/types";
import { AppState } from "../types";
import { exportToCanvas, getExportSize } from "../scene/export";
import { ActionsManagerInterface } from "../actions/types";
import Stack from "./Stack";
import { t } from "../i18n";
import { EVENT_DIALOG, trackEvent } from "../analytics";
import { probablySupportsClipboardBlob } from "../clipboard";
import { getSelectedElements, isSomeElementSelected } from "../scene";
import useIsMobile from "../is-mobile";
import { Dialog } from "./Dialog";
import { canvasToBlob } from "../data/blob";
import { NonDeletedExcalidrawElement } from "../element/types";
import { CanvasError } from "../errors";
import { EVENT_ACTION, trackEvent } from "../analytics";
import { t } from "../i18n";
import useIsMobile from "../is-mobile";
import { getSelectedElements, isSomeElementSelected } from "../scene";
import { exportToCanvas, getExportSize } from "../scene/export";
import { AppState } from "../types";
import { Dialog } from "./Dialog";
import "./ExportDialog.scss";
import { clipboard, exportFile, link } from "./icons";
import Stack from "./Stack";
import { ToolButton } from "./ToolButton";
const scales = [1, 2, 3];
const defaultScale = scales.includes(devicePixelRatio) ? devicePixelRatio : 1;
@ -252,7 +249,7 @@ export const ExportDialog = ({
<>
<ToolButton
onClick={() => {
trackEvent(EVENT_ACTION, "export", "dialog");
trackEvent(EVENT_DIALOG, "export");
setModalIsShown(true);
}}
icon={exportFile}

View File

@ -1,15 +1,15 @@
import React, { useState, useEffect, useRef } from "react";
import clsx from "clsx";
import { ToolButton } from "./ToolButton";
import React, { useEffect, useRef, useState } from "react";
import { EVENT_DIALOG, EVENT_SHARE, trackEvent } from "../analytics";
import { copyTextToSystemClipboard } from "../clipboard";
import { t } from "../i18n";
import useIsMobile from "../is-mobile";
import { users, clipboard, start, stop } from "./icons";
import "./RoomDialog.scss";
import { copyTextToSystemClipboard } from "../clipboard";
import { Dialog } from "./Dialog";
import { AppState } from "../types";
import { KEYS } from "../keys";
import { AppState } from "../types";
import { Dialog } from "./Dialog";
import { clipboard, start, stop, users } from "./icons";
import "./RoomDialog.scss";
import { ToolButton } from "./ToolButton";
const RoomModal = ({
activeRoomLink,
@ -33,6 +33,7 @@ const RoomModal = ({
const copyRoomLink = async () => {
try {
await copyTextToSystemClipboard(activeRoomLink);
trackEvent(EVENT_SHARE, "copy link");
} catch (error) {
setErrorMessage(error.message);
}
@ -95,6 +96,7 @@ const RoomModal = ({
value={username || ""}
className="RoomDialog-username TextInput"
onChange={(event) => onUsernameChange(event.target.value)}
onBlur={() => trackEvent(EVENT_SHARE, "name")}
onKeyPress={(event) =>
event.key === KEYS.ENTER && onPressingEnter()
}
@ -161,7 +163,10 @@ export const RoomDialog = ({
className={clsx("RoomDialog-modalButton", {
"is-collaborating": isCollaborating,
})}
onClick={() => setModalIsShown(true)}
onClick={() => {
trackEvent(EVENT_DIALOG, "collaboration");
setModalIsShown(true);
}}
icon={users}
type="button"
title={t("buttons.roomDialog")}

View File

@ -1,13 +1,13 @@
import { EVENT_IO, trackEvent } from "../analytics";
import { cleanAppStateForExport } from "../appState";
import { restore } from "./restore";
import { t } from "../i18n";
import { AppState } from "../types";
import { LibraryData, ImportedDataState } from "./types";
import { calculateScrollCenter } from "../scene";
import { MIME_TYPES } from "../constants";
import { CanvasError } from "../errors";
import { clearElementsForExport } from "../element";
import { EVENT_ACTION, trackEvent } from "../analytics";
import { CanvasError } from "../errors";
import { t } from "../i18n";
import { calculateScrollCenter } from "../scene";
import { AppState } from "../types";
import { restore } from "./restore";
import { ImportedDataState, LibraryData } from "./types";
export const parseFileContents = async (blob: Blob | File) => {
let contents: string;
@ -111,7 +111,7 @@ export const loadFromBlob = async (
localAppState,
);
trackEvent(EVENT_ACTION, "load", getMimeType(blob));
trackEvent(EVENT_IO, "load", getMimeType(blob));
return result;
} catch (error) {
console.error(error.message);

View File

@ -1,29 +1,25 @@
import {
ExcalidrawElement,
NonDeletedExcalidrawElement,
} from "../element/types";
import { getDefaultAppState } from "../appState";
import { AppState } from "../types";
import { exportToCanvas, exportToSvg } from "../scene/export";
import { fileSave } from "browser-nativefs";
import { t } from "../i18n";
import { EVENT_IO, trackEvent } from "../analytics";
import { getDefaultAppState } from "../appState";
import {
copyCanvasToClipboardAsPng,
copyTextToSystemClipboard,
} from "../clipboard";
import { serializeAsJSON } from "./json";
import {
ExcalidrawElement,
NonDeletedExcalidrawElement,
} from "../element/types";
import { t } from "../i18n";
import { exportToCanvas, exportToSvg } from "../scene/export";
import { ExportType } from "../scene/types";
import { AppState } from "../types";
import { canvasToBlob } from "./blob";
import { serializeAsJSON } from "./json";
import { restore } from "./restore";
import { ImportedDataState } from "./types";
import { canvasToBlob } from "./blob";
import { EVENT_ACTION, trackEvent } from "../analytics";
export { loadFromBlob } from "./blob";
export { saveAsJSON, loadFromJSON } from "./json";
export { loadFromJSON, saveAsJSON } from "./json";
const BACKEND_GET = process.env.REACT_APP_BACKEND_V1_GET_URL;
@ -218,7 +214,7 @@ export const exportToBackend = async (
url.hash = `json=${json.id},${exportedKey.k!}`;
const urlString = url.toString();
window.prompt(`🔒${t("alerts.uploadedSecurly")}`, urlString);
trackEvent(EVENT_ACTION, "export", "backend");
trackEvent(EVENT_IO, "export", "backend");
} else if (json.error_class === "RequestTooLargeError") {
window.alert(t("alerts.couldNotCreateShareableLinkTooBig"));
} else {
@ -265,7 +261,7 @@ const importFromBackend = async (
data = await response.json();
}
trackEvent(EVENT_ACTION, "import");
trackEvent(EVENT_IO, "import");
return {
elements: data.elements || null,
appState: data.appState || null,
@ -322,10 +318,10 @@ export const exportCanvas = async (
fileName: `${name}.svg`,
extensions: [".svg"],
});
trackEvent(EVENT_ACTION, "export", "svg");
trackEvent(EVENT_IO, "export", "svg");
return;
} else if (type === "clipboard-svg") {
trackEvent(EVENT_ACTION, "export", "clipboard-svg");
trackEvent(EVENT_IO, "export", "clipboard-svg");
copyTextToSystemClipboard(tempSvg.outerHTML);
return;
}
@ -357,11 +353,11 @@ export const exportCanvas = async (
fileName,
extensions: [".png"],
});
trackEvent(EVENT_ACTION, "export", "png");
trackEvent(EVENT_IO, "export", "png");
} else if (type === "clipboard") {
try {
await copyCanvasToClipboardAsPng(tempCanvas);
trackEvent(EVENT_ACTION, "export", "clipboard-png");
trackEvent(EVENT_IO, "export", "clipboard-png");
} catch (error) {
if (error.name === "CANVAS_POSSIBLY_TOO_BIG") {
throw error;