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` | | Shape / Selection | shape | selection, rectangle, diamond, etc | `toolbar` or `shortcut` |
| Text on double click | shape | text | `double-click` | | Text on double click | shape | text | `double-click` |
| Lock selection | shape | lock | `on` or `off` | | 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 | | Clear canvas | action | clear canvas |
| Zoom in | action | zoom | in | `zoom` | | Zoom in | action | zoom | in | `zoom` |
| Zoom out | action | zoom | out | `zoom` | | Zoom out | action | zoom | out | `zoom` |
| Zoom fit | action | zoom | fit | `zoom` | | Zoom fit | action | zoom | fit | `zoom` |
| Zoom reset | action | zoom | reset | `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 | | 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` | | Canvas color | change | canvas color | `color` |
| Background color | change | background color | `color` | | Background color | change | background color | `color` |
| Stroke color | change | stroke color | `color` | | Stroke color | change | stroke color | `color` |
@ -42,6 +40,15 @@
| Center vertically | align | vertically | `center` | | Center vertically | align | vertically | `center` |
| Distribute horizontally | align | distribute | `horizontally` | | Distribute horizontally | align | distribute | `horizontally` |
| Distribute vertically | align | distribute | `vertically` | | 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 | | E2EE shield | exit | e2ee shield |
| GitHub corner | exit | github | | GitHub corner | exit | github |
| Excalidraw blog | exit | blog | | Excalidraw blog | exit | blog |

View File

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

View File

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

View File

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

View File

@ -4,6 +4,9 @@ export const EVENT_CHANGE = "change";
export const EVENT_SHAPE = "shape"; export const EVENT_SHAPE = "shape";
export const EVENT_LAYER = "layer"; export const EVENT_LAYER = "layer";
export const EVENT_ALIGN = "align"; 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 export const trackEvent = window.gtag
? (category: string, name: string, label?: string, value?: number) => { ? (category: string, name: string, label?: string, value?: number) => {

View File

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

View File

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

View File

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

View File

@ -1,13 +1,13 @@
import { EVENT_IO, trackEvent } from "../analytics";
import { cleanAppStateForExport } from "../appState"; 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 { MIME_TYPES } from "../constants";
import { CanvasError } from "../errors";
import { clearElementsForExport } from "../element"; 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) => { export const parseFileContents = async (blob: Blob | File) => {
let contents: string; let contents: string;
@ -111,7 +111,7 @@ export const loadFromBlob = async (
localAppState, localAppState,
); );
trackEvent(EVENT_ACTION, "load", getMimeType(blob)); trackEvent(EVENT_IO, "load", getMimeType(blob));
return result; return result;
} catch (error) { } catch (error) {
console.error(error.message); 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 { fileSave } from "browser-nativefs";
import { EVENT_IO, trackEvent } from "../analytics";
import { t } from "../i18n"; import { getDefaultAppState } from "../appState";
import { import {
copyCanvasToClipboardAsPng, copyCanvasToClipboardAsPng,
copyTextToSystemClipboard, copyTextToSystemClipboard,
} from "../clipboard"; } 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 { ExportType } from "../scene/types";
import { AppState } from "../types";
import { canvasToBlob } from "./blob";
import { serializeAsJSON } from "./json";
import { restore } from "./restore"; import { restore } from "./restore";
import { ImportedDataState } from "./types"; import { ImportedDataState } from "./types";
import { canvasToBlob } from "./blob";
import { EVENT_ACTION, trackEvent } from "../analytics";
export { loadFromBlob } from "./blob"; 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; 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!}`; url.hash = `json=${json.id},${exportedKey.k!}`;
const urlString = url.toString(); const urlString = url.toString();
window.prompt(`🔒${t("alerts.uploadedSecurly")}`, urlString); window.prompt(`🔒${t("alerts.uploadedSecurly")}`, urlString);
trackEvent(EVENT_ACTION, "export", "backend"); trackEvent(EVENT_IO, "export", "backend");
} else if (json.error_class === "RequestTooLargeError") { } else if (json.error_class === "RequestTooLargeError") {
window.alert(t("alerts.couldNotCreateShareableLinkTooBig")); window.alert(t("alerts.couldNotCreateShareableLinkTooBig"));
} else { } else {
@ -265,7 +261,7 @@ const importFromBackend = async (
data = await response.json(); data = await response.json();
} }
trackEvent(EVENT_ACTION, "import"); trackEvent(EVENT_IO, "import");
return { return {
elements: data.elements || null, elements: data.elements || null,
appState: data.appState || null, appState: data.appState || null,
@ -322,10 +318,10 @@ export const exportCanvas = async (
fileName: `${name}.svg`, fileName: `${name}.svg`,
extensions: [".svg"], extensions: [".svg"],
}); });
trackEvent(EVENT_ACTION, "export", "svg"); trackEvent(EVENT_IO, "export", "svg");
return; return;
} else if (type === "clipboard-svg") { } else if (type === "clipboard-svg") {
trackEvent(EVENT_ACTION, "export", "clipboard-svg"); trackEvent(EVENT_IO, "export", "clipboard-svg");
copyTextToSystemClipboard(tempSvg.outerHTML); copyTextToSystemClipboard(tempSvg.outerHTML);
return; return;
} }
@ -357,11 +353,11 @@ export const exportCanvas = async (
fileName, fileName,
extensions: [".png"], extensions: [".png"],
}); });
trackEvent(EVENT_ACTION, "export", "png"); trackEvent(EVENT_IO, "export", "png");
} else if (type === "clipboard") { } else if (type === "clipboard") {
try { try {
await copyCanvasToClipboardAsPng(tempCanvas); await copyCanvasToClipboardAsPng(tempCanvas);
trackEvent(EVENT_ACTION, "export", "clipboard-png"); trackEvent(EVENT_IO, "export", "clipboard-png");
} catch (error) { } catch (error) {
if (error.name === "CANVAS_POSSIBLY_TOO_BIG") { if (error.name === "CANVAS_POSSIBLY_TOO_BIG") {
throw error; throw error;