From e0316c21838613df0fbf43df6a9ca5d696c52f47 Mon Sep 17 00:00:00 2001
From: Sam Wilkins <samwilkins333@gmail.com>
Date: Tue, 30 Jul 2019 20:19:58 -0400
Subject: more interesting speech commands and command manager pattern for
 DictationManager

---
 src/client/views/MainView.tsx | 2 ++
 1 file changed, 2 insertions(+)

(limited to 'src/client/views/MainView.tsx')

diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 91c8fe57c..36ac96907 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -39,6 +39,7 @@ import { FilterBox } from './search/FilterBox';
 import { CollectionTreeView } from './collections/CollectionTreeView';
 import { ClientUtils } from '../util/ClientUtils';
 import { SchemaHeaderField, RandomPastel } from '../../new_fields/SchemaHeaderField';
+import DictationManager from '../util/DictationManager';
 
 @observer
 export class MainView extends React.Component {
@@ -459,6 +460,7 @@ export class MainView extends React.Component {
     render() {
         return (
             <div id="main-div">
+                <h1>{DictationManager.Instance.current}</h1>
                 <DocumentDecorations />
                 {this.mainContent}
                 <PreviewCursor />
-- 
cgit v1.2.3-70-g09d2


From 8a87f7110b56ca96b3960f6fb3917c7ed8c7a814 Mon Sep 17 00:00:00 2001
From: Sam Wilkins <samwilkins333@gmail.com>
Date: Wed, 31 Jul 2019 03:21:59 -0400
Subject: overlay completed

---
 src/client/util/DictationManager.ts  | 12 +++----
 src/client/views/GlobalKeyHandler.ts | 24 +++++++++----
 src/client/views/Main.scss           | 68 +++++++++++++++++++++++++++---------
 src/client/views/MainView.tsx        | 55 +++++++++++++++++++++++++++--
 4 files changed, 127 insertions(+), 32 deletions(-)

(limited to 'src/client/views/MainView.tsx')

diff --git a/src/client/util/DictationManager.ts b/src/client/util/DictationManager.ts
index 6d67f6d6d..80efe12cd 100644
--- a/src/client/util/DictationManager.ts
+++ b/src/client/util/DictationManager.ts
@@ -1,5 +1,5 @@
 import { string } from "prop-types";
-import { observable, action } from "mobx";
+import { observable, action, autorun } from "mobx";
 import { SelectionManager } from "./SelectionManager";
 import { DocumentView } from "../views/nodes/DocumentView";
 import { UndoManager } from "./UndoManager";
@@ -8,6 +8,7 @@ import { Doc } from "../../new_fields/Doc";
 import { List } from "../../new_fields/List";
 import { Docs } from "../documents/Documents";
 import { CollectionViewType } from "../views/collections/CollectionBaseView";
+import { MainView } from "../views/MainView";
 
 namespace CORE {
     export interface IWindow extends Window {
@@ -22,8 +23,8 @@ export type RegexEntry = { key: RegExp, value: DependentAction };
 
 export default class DictationManager {
     public static Instance = new DictationManager();
-    private isListening = false;
     private recognizer: any;
+    private isListening = false;
 
     constructor() {
         this.recognizer = new webkitSpeechRecognition();
@@ -31,18 +32,14 @@ export default class DictationManager {
         this.recognizer.continuous = true;
     }
 
-    @observable public current = "";
-
-    @action
     finish = (handler: any, data: any) => {
-        this.current = data;
         handler(data);
         this.stop();
     }
 
     stop = () => {
-        this.isListening = false;
         this.recognizer.stop();
+        this.isListening = false;
     }
 
     listen = () => {
@@ -55,7 +52,6 @@ export default class DictationManager {
             this.recognizer.onresult = (e: any) => this.finish(resolve, e.results[0][0].transcript);
             this.recognizer.onerror = (e: any) => this.finish(reject, e);
         });
-
     }
 
     private sanitize = (title: string) => {
diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts
index 1d3c77ec7..a6020bd3f 100644
--- a/src/client/views/GlobalKeyHandler.ts
+++ b/src/client/views/GlobalKeyHandler.ts
@@ -3,7 +3,7 @@ import { SelectionManager } from "../util/SelectionManager";
 import { CollectionDockingView } from "./collections/CollectionDockingView";
 import { MainView } from "./MainView";
 import { DragManager } from "../util/DragManager";
-import { action } from "mobx";
+import { action, runInAction } from "mobx";
 import { Doc } from "../../new_fields/Doc";
 import { CognitiveServices } from "../cognitive_services/CognitiveServices";
 import DictationManager from "../util/DictationManager";
@@ -74,6 +74,7 @@ export default class KeyManager {
                 MainView.Instance.toggleColorPicker(true);
                 SelectionManager.DeselectAll();
                 DictationManager.Instance.stop();
+                MainView.Instance.dictationOverlayVisible = false;
                 break;
             case "delete":
             case "backspace":
@@ -107,11 +108,22 @@ export default class KeyManager {
 
         switch (keyname) {
             case " ":
-                console.log("Listening...");
-                let analyzer = DictationManager.Instance;
-                let transcript = await analyzer.listen();
-                console.log(`I heard${transcript ? `: ${transcript.toLowerCase()}` : " nothing: I thought I was still listening from an earlier session."}`);
-                transcript && analyzer.execute(transcript);
+                let main = MainView.Instance;
+                main.dictationOverlayVisible = true;
+                main.isListening = true;
+                let manager = DictationManager.Instance;
+                let command = await manager.listen();
+                main.isListening = false;
+                if (!command) {
+                    break;
+                }
+                command = command.toLowerCase();
+                main.dictatedPhrase = command;
+                main.dictationSuccess = await manager.execute(command);
+                setTimeout(() => {
+                    main.dictationOverlayVisible = false;
+                    main.dictationSuccess = undefined;
+                }, 3000);
         }
 
         return {
diff --git a/src/client/views/Main.scss b/src/client/views/Main.scss
index a16123476..8e57b88c3 100644
--- a/src/client/views/Main.scss
+++ b/src/client/views/Main.scss
@@ -24,7 +24,7 @@ div {
 
 .jsx-parser {
     width: 100%;
-    height:100%;
+    height: 100%;
     pointer-events: none;
     border-radius: inherit;
 }
@@ -119,6 +119,7 @@ button:hover {
         margin-bottom: 10px;
     }
 }
+
 .toolbar-color-picker {
     background-color: $light-color;
     border-radius: 5px;
@@ -128,6 +129,7 @@ button:hover {
     left: -3px;
     box-shadow: $intermediate-color 0.2vw 0.2vw 0.8vw;
 }
+
 .toolbar-color-button {
     border-radius: 11px;
     width: 22px;
@@ -146,7 +148,7 @@ button:hover {
     bottom: 22px;
     left: 250px;
 
-    > label {
+    >label {
         background: $dark-color;
         color: $light-color;
         display: inline-block;
@@ -168,15 +170,15 @@ button:hover {
         transform: scale(1.15);
     }
 
-    > input {
+    >input {
         display: none;
     }
 
-    > input:not(:checked)~#add-options-content {
+    >input:not(:checked)~#add-options-content {
         display: none;
     }
 
-    > input:checked~label {
+    >input:checked~label {
         transform: rotate(45deg);
         transition: transform 0.5s;
         cursor: pointer;
@@ -221,7 +223,7 @@ ul#add-options-list {
     list-style: none;
     padding: 5 0 0 0;
 
-    > li {
+    >li {
         display: inline-block;
         padding: 0;
     }
@@ -231,7 +233,7 @@ ul#add-options-list {
     height: 100%;
     position: absolute;
     display: flex;
-    flex-direction:column;
+    flex-direction: column;
 }
 
 .mainView-libraryHandle {
@@ -243,21 +245,55 @@ ul#add-options-list {
     position: absolute;
     z-index: 1;
 }
+
 .svg-inline--fa {
     vertical-align: unset;
 }
+
 .mainView-workspace {
-    height:200px;
-    position:relative;
-    display:flex;
+    height: 200px;
+    position: relative;
+    display: flex;
 }
+
 .mainView-library {
-    height:75%;
-    position:relative;
-    display:flex;
+    height: 75%;
+    position: relative;
+    display: flex;
 }
+
 .mainView-recentlyClosed {
-    height:25%;
-    position:relative;
-    display:flex;
+    height: 25%;
+    position: relative;
+    display: flex;
+}
+
+.dictation-prompt {
+    position: absolute;
+    z-index: 1000;
+    text-align: center;
+    justify-content: center;
+    align-self: center;
+    align-content: center;
+    padding: 20px;
+    background: gainsboro;
+    border-radius: 10px;
+    border: 3px solid black;
+    box-shadow: #00000044 5px 5px 10px;
+    transform: translate(-50%, -50%);
+    top: 50%;
+    font-style: italic;
+    left: 50%;
+    transition: 0.5s all ease;
+    pointer-events: none;
+}
+
+.dictation-prompt-overlay {
+    width: 100%;
+    height: 100%;
+    position: absolute;
+    background: darkslategray;
+    z-index: 999;
+    transition: 0.5s all ease;
+    pointer-events: none;
 }
\ No newline at end of file
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 36ac96907..d6449cffc 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -39,7 +39,6 @@ import { FilterBox } from './search/FilterBox';
 import { CollectionTreeView } from './collections/CollectionTreeView';
 import { ClientUtils } from '../util/ClientUtils';
 import { SchemaHeaderField, RandomPastel } from '../../new_fields/SchemaHeaderField';
-import DictationManager from '../util/DictationManager';
 
 @observer
 export class MainView extends React.Component {
@@ -48,6 +47,12 @@ export class MainView extends React.Component {
     @observable private _workspacesShown: boolean = false;
     @observable public pwidth: number = 0;
     @observable public pheight: number = 0;
+
+    @observable private dictationState = "Listening...";
+    @observable private dictationSuccessState: boolean | undefined = undefined;
+    @observable private dictationDisplayState = false;
+    @observable private dictationListeningState = false;
+
     @computed private get mainContainer(): Opt<Doc> {
         return FieldValue(Cast(CurrentUserUtils.UserDocument.activeWorkspace, Doc));
     }
@@ -65,6 +70,38 @@ export class MainView extends React.Component {
         }
     }
 
+    public get dictatedPhrase() {
+        return this.dictationState;
+    }
+
+    public set dictatedPhrase(value: string) {
+        runInAction(() => this.dictationState = value);
+    }
+
+    public get dictationSuccess() {
+        return this.dictationSuccessState;
+    }
+
+    public set dictationSuccess(value: boolean | undefined) {
+        runInAction(() => this.dictationSuccessState = value);
+    }
+
+    public get dictationOverlayVisible() {
+        return this.dictationDisplayState;
+    }
+
+    public set dictationOverlayVisible(value: boolean) {
+        runInAction(() => this.dictationDisplayState = value);
+    }
+
+    public get isListening() {
+        return this.dictationListeningState;
+    }
+
+    public set isListening(value: boolean) {
+        runInAction(() => this.dictationListeningState = value);
+    }
+
     componentWillMount() {
         var tag = document.createElement('script');
 
@@ -458,9 +495,23 @@ export class MainView extends React.Component {
     }
 
     render() {
+        let display = this.dictationOverlayVisible;
+        let success = this.dictationSuccess;
+        let result = this.isListening ? "Listening..." : `"${this.dictatedPhrase}"`;
         return (
             <div id="main-div">
-                <h1>{DictationManager.Instance.current}</h1>
+                <div
+                    className={"dictation-prompt"}
+                    style={{
+                        opacity: display ? 1 : 0,
+                        background: success === undefined ? "gainsboro" : success ? "lawngreen" : "red",
+                        borderColor: this.isListening ? "red" : "black",
+                    }}
+                >{result}</div>
+                <div
+                    className={"dictation-prompt-overlay"}
+                    style={{ opacity: display ? 0.4 : 0 }}
+                />
                 <DocumentDecorations />
                 {this.mainContent}
                 <PreviewCursor />
-- 
cgit v1.2.3-70-g09d2


From 753615c80d4cf08605ebaaeeaf0a44a0fd88d898 Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Sat, 3 Aug 2019 15:21:10 -0400
Subject: working version of clustering

---
 src/Utils.ts                                       |   2 +
 src/client/documents/Documents.ts                  |   3 +-
 src/client/util/DragManager.ts                     |   8 +-
 src/client/views/MainView.tsx                      |   4 +-
 src/client/views/TemplateMenu.tsx                  |   5 +-
 .../views/collections/CollectionDockingView.tsx    |   3 +-
 .../views/collections/CollectionSchemaView.tsx     |   3 +-
 .../collectionFreeForm/CollectionFreeFormView.tsx  | 142 +++++++++++++--------
 .../collections/collectionFreeForm/MarqueeView.tsx |   1 +
 .../views/nodes/CollectionFreeFormDocumentView.tsx |  12 +-
 src/client/views/nodes/DocumentView.tsx            |  17 +--
 .../views/presentationview/PresentationElement.tsx |   3 +-
 src/client/views/search/SearchItem.tsx             |   3 +-
 13 files changed, 125 insertions(+), 81 deletions(-)

(limited to 'src/client/views/MainView.tsx')

diff --git a/src/Utils.ts b/src/Utils.ts
index 8df67df5d..502540eb0 100644
--- a/src/Utils.ts
+++ b/src/Utils.ts
@@ -140,6 +140,8 @@ export function returnOne() { return 1; }
 
 export function returnZero() { return 0; }
 
+export function returnEmptyString() { return ""; }
+
 export function emptyFunction() { }
 
 export type Without<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 09bafcf43..07e38a4c0 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -84,6 +84,7 @@ export interface DocumentOptions {
     templates?: List<string>;
     viewType?: number;
     backgroundColor?: string;
+    defaultBackgroundColor?: string;
     dropAction?: dropActionType;
     backgroundLayout?: string;
     chromeStatus?: string;
@@ -124,7 +125,7 @@ export namespace Docs {
         const TemplateMap: TemplateMap = new Map([
             [DocumentType.TEXT, {
                 layout: { view: FormattedTextBox },
-                options: { height: 150, backgroundColor: "#f1efeb" }
+                options: { height: 150, backgroundColor: "#f1efeb", defaultBackgroundColor: "#f1efeb" }
             }],
             [DocumentType.HIST, {
                 layout: { view: HistogramBox, collectionView: [CollectionView, data] as CollectionViewType },
diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts
index 0b5c785a4..a7aaaed7c 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -404,7 +404,8 @@ export namespace DragManager {
                 hideSource = options.hideSource();
             }
         }
-        eles.map(ele => (ele.hidden = hideSource));
+        eles.map(ele => (ele.hidden = hideSource) &&
+            (ele.parentElement && ele.parentElement.className.indexOf("collectionFreeFormDocumentView") !== -1 && (ele.parentElement.hidden = hideSource)));
 
         let lastX = downX;
         let lastY = downY;
@@ -434,7 +435,10 @@ export namespace DragManager {
 
         let hideDragElements = () => {
             dragElements.map(dragElement => dragElement.parentNode === dragDiv && dragDiv.removeChild(dragElement));
-            eles.map(ele => (ele.hidden = false));
+            eles.map(ele => {
+                ele.hidden = false;
+                (ele.parentElement && ele.parentElement.className.indexOf("collectionFreeFormDocumentView") !== -1 && (ele.parentElement.hidden = false));
+            });
         };
         let endDrag = () => {
             document.removeEventListener("pointermove", moveHandler, true);
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 2ecf5fd85..53f589684 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -15,7 +15,7 @@ import { listSpec } from '../../new_fields/Schema';
 import { Cast, FieldValue, NumCast, BoolCast, StrCast } from '../../new_fields/Types';
 import { CurrentUserUtils } from '../../server/authentication/models/current_user_utils';
 import { RouteStore } from '../../server/RouteStore';
-import { emptyFunction, returnOne, returnTrue, Utils } from '../../Utils';
+import { emptyFunction, returnOne, returnTrue, Utils, returnEmptyString } from '../../Utils';
 import { DocServer } from '../DocServer';
 import { Docs } from '../documents/Documents';
 import { SetupDrag } from '../util/DragManager';
@@ -270,6 +270,7 @@ export class MainView extends React.Component {
                             PanelWidth={this.getPWidth}
                             PanelHeight={this.getPHeight}
                             renderDepth={0}
+                            backgroundColor={returnEmptyString}
                             selectOnLoad={false}
                             focus={emptyFunction}
                             parentActive={returnTrue}
@@ -334,6 +335,7 @@ export class MainView extends React.Component {
             renderDepth={0}
             selectOnLoad={false}
             focus={emptyFunction}
+            backgroundColor={returnEmptyString}
             parentActive={returnTrue}
             whenActiveChanged={emptyFunction}
             bringToFront={emptyFunction}
diff --git a/src/client/views/TemplateMenu.tsx b/src/client/views/TemplateMenu.tsx
index e654a0644..6dd908445 100644
--- a/src/client/views/TemplateMenu.tsx
+++ b/src/client/views/TemplateMenu.tsx
@@ -60,12 +60,9 @@ export class TemplateMenu extends React.Component<TemplateMenuProps> {
                     let de = new DragManager.DocumentDragData([topDoc], [undefined]);
                     de.moveDocument = topDocView.props.moveDocument;
                     let xf = newDocView.ContentDiv!.getBoundingClientRect();
-                    console.log("ex = " + ex + " " + xf.left + " " + (ex - xf.left));
                     DragManager.StartDocumentDrag([newDocView.ContentDiv!], de, ex, ey, {
                         offsetX: (ex - xf.left), offsetY: (ey - xf.top),
-                        handlers: {
-                            dragComplete: () => { },
-                        },
+                        handlers: { dragComplete: () => { }, },
                         hideSource: false
                     });
                 }
diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index 588102f01..f559480ed 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -10,7 +10,7 @@ import { Id } from '../../../new_fields/FieldSymbols';
 import { FieldId } from "../../../new_fields/RefField";
 import { listSpec } from "../../../new_fields/Schema";
 import { Cast, NumCast, StrCast, BoolCast } from "../../../new_fields/Types";
-import { emptyFunction, returnTrue, Utils, returnOne } from "../../../Utils";
+import { emptyFunction, returnTrue, Utils, returnOne, returnEmptyString } from "../../../Utils";
 import { DocServer } from "../../DocServer";
 import { DocumentManager } from '../../util/DocumentManager';
 import { DragLinksAsDocuments, DragManager } from "../../util/DragManager";
@@ -607,6 +607,7 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
             parentActive={returnTrue}
             whenActiveChanged={emptyFunction}
             focus={emptyFunction}
+            backgroundColor={returnEmptyString}
             addDocTab={this.addDocTab}
             ContainingCollectionView={undefined}
             zoomToScale={emptyFunction}
diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx
index 9efd0d3ec..8218877ba 100644
--- a/src/client/views/collections/CollectionSchemaView.tsx
+++ b/src/client/views/collections/CollectionSchemaView.tsx
@@ -6,7 +6,7 @@ import { action, computed, observable, trace, untracked } from "mobx";
 import { observer } from "mobx-react";
 import ReactTable, { CellInfo, ComponentPropsGetterR, Column, RowInfo, ResizedChangeFunction, Resize } from "react-table";
 import "react-table/react-table.css";
-import { emptyFunction, returnOne } from "../../../Utils";
+import { emptyFunction, returnOne, returnEmptyString } from "../../../Utils";
 import { Doc, DocListCast, Field, Opt } from "../../../new_fields/Doc";
 import { Id } from "../../../new_fields/FieldSymbols";
 import { List } from "../../../new_fields/List";
@@ -999,6 +999,7 @@ export class CollectionSchemaPreview extends React.Component<CollectionSchemaPre
                         PanelHeight={this.PanelHeight}
                         ContainingCollectionView={this.props.CollectionView}
                         focus={emptyFunction}
+                        backgroundColor={returnEmptyString}
                         parentActive={this.props.active}
                         whenActiveChanged={this.props.whenActiveChanged}
                         bringToFront={emptyFunction}
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index bc1aee159..ee21c5a4d 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -1,7 +1,7 @@
 import { library } from "@fortawesome/fontawesome-svg-core";
 import { faEye } from "@fortawesome/free-regular-svg-icons";
 import { faCompass, faCompressArrowsAlt, faExpandArrowsAlt, faPaintBrush, faTable, faUpload } from "@fortawesome/free-solid-svg-icons";
-import { action, computed } from "mobx";
+import { action, computed, observable } from "mobx";
 import { observer } from "mobx-react";
 import { Doc, DocListCastAsync, HeightSym, WidthSym } from "../../../../new_fields/Doc";
 import { Id } from "../../../../new_fields/FieldSymbols";
@@ -9,7 +9,7 @@ import { InkField, StrokeData } from "../../../../new_fields/InkField";
 import { createSchema, makeInterface } from "../../../../new_fields/Schema";
 import { ScriptField } from "../../../../new_fields/ScriptField";
 import { BoolCast, Cast, FieldValue, NumCast, StrCast } from "../../../../new_fields/Types";
-import { emptyFunction, returnOne, Utils } from "../../../../Utils";
+import { emptyFunction, returnOne, Utils, returnFalse, returnEmptyString } from "../../../../Utils";
 import { CognitiveServices } from "../../../cognitive_services/CognitiveServices";
 import { DocServer } from "../../../DocServer";
 import { DocumentManager } from "../../../util/DocumentManager";
@@ -37,6 +37,8 @@ import "./CollectionFreeFormView.scss";
 import { MarqueeView } from "./MarqueeView";
 import React = require("react");
 import v5 = require("uuid/v5");
+import { setScheduler } from "bluebird";
+import { DocumentType, Docs } from "../../../documents/Documents";
 
 library.add(faEye, faTable, faPaintBrush, faExpandArrowsAlt, faCompressArrowsAlt, faCompass, faUpload);
 
@@ -132,17 +134,17 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
         r2: { left: number, top: number, width: number, height: number }) {
         return !(r2.left > r1.left + r1.width || r2.left + r2.width < r1.left || r2.top > r1.top + r1.height || r2.top + r2.height < r1.top);
     }
-    _groupingBorder = 100;
+    _clusterDistance = 75;
     bounsdSelect(doc: Doc, doc2: Doc) {
-        var x2 = NumCast(doc2.x) - this._groupingBorder;
-        var y2 = NumCast(doc2.y) - this._groupingBorder;
-        var w2 = NumCast(doc2.width) + this._groupingBorder;
-        var h2 = NumCast(doc2.height) + this._groupingBorder;
-        var x = NumCast(doc.x) - this._groupingBorder;
-        var y = NumCast(doc.y) - this._groupingBorder;
-        var w = NumCast(doc.width) + this._groupingBorder;
-        var h = NumCast(doc.height) + this._groupingBorder;
-        if (this.intersectRect({ left: x, top: y, width: w, height: h }, { left: x2, top: y2, width: w2, height: h2 })) {
+        var x2 = NumCast(doc2.x) - this._clusterDistance;
+        var y2 = NumCast(doc2.y) - this._clusterDistance;
+        var w2 = NumCast(doc2.width) + this._clusterDistance;
+        var h2 = NumCast(doc2.height) + this._clusterDistance;
+        var x = NumCast(doc.x) - this._clusterDistance;
+        var y = NumCast(doc.y) - this._clusterDistance;
+        var w = NumCast(doc.width) + this._clusterDistance;
+        var h = NumCast(doc.height) + this._clusterDistance;
+        if (doc.z === doc2.z && this.intersectRect({ left: x, top: y, width: w, height: h }, { left: x2, top: y2, width: w2, height: h2 })) {
             return true;
         }
         return false;
@@ -197,36 +199,83 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
         return false;
     }
 
+    tryDragCluster(e: PointerEvent) {
+        let probe = this.getTransform().transformPoint(e.clientX, e.clientY);
+        let cluster = this.childDocs.reduce((cluster, cd) => {
+            let cx = NumCast(cd.x) - this._clusterDistance;
+            let cy = NumCast(cd.y) - this._clusterDistance;
+            let cw = NumCast(cd.width) + 2 * this._clusterDistance;
+            let ch = NumCast(cd.height) + 2 * this._clusterDistance;
+            if (!cd.z && this.intersectRect({ left: cx, top: cy, width: cw, height: ch }, { left: probe[0], top: probe[1], width: 1, height: 1 }))
+                return NumCast(cd.cluster);
+            return cluster;
+        }, -1);
+        if (cluster !== -1) {
+            let eles = this.childDocs.filter(cd => NumCast(cd.cluster) === cluster);
+            this.selectDocuments(eles);
+            let clusterDocs = SelectionManager.SelectedDocuments();
+            SelectionManager.DeselectAll();
+            let de = new DragManager.DocumentDragData(eles, eles.map(d => undefined));
+            de.moveDocument = this.props.moveDocument;
+            const [left, top] = clusterDocs[0].props.ScreenToLocalTransform().scale(clusterDocs[0].props.ContentScaling()).inverse().transformPoint(0, 0);
+            const [xoff, yoff] = this.getTransform().transformDirection(e.x - left, e.y - top);
+            de.dropAction = e.ctrlKey || e.altKey ? "alias" : undefined;
+            de.xOffset = xoff;
+            de.yOffset = yoff;
+            DragManager.StartDocumentDrag(clusterDocs.map(v => v.ContentDiv!), de, e.clientX, e.clientY, {
+                handlers: { dragComplete: action(emptyFunction) },
+                hideSource: !de.dropAction
+            });
+            return true;
+        }
+
+        return false;
+    }
+    @observable sets: (Doc[])[] = [];
     @action
     updateClusters() {
-        let sets: (Doc[])[] = []
+        this.sets.length = 0;
         this.childDocs.map(c => {
             let included = []
-            for (let i = 0; i < sets.length; i++) {
-                for (let j = 0; j < sets[i].length; j++) {
-                    if (this.bounsdSelect(c, sets[i][j])) {
+            for (let i = 0; i < this.sets.length; i++) {
+                for (let j = 0; j < this.sets[i].length; j++) {
+                    if (this.bounsdSelect(c, this.sets[i][j])) {
                         included.push(i);
                         break;
                     }
                 }
             }
             if (included.length === 0)
-                sets.push([c]);
+                this.sets.push([c]);
             else if (included.length === 1)
-                sets[included[0]].push(c);
+                this.sets[included[0]].push(c);
             else {
-                sets[included[0]].push(c);
+                this.sets[included[0]].push(c);
                 for (let s = 1; s < included.length; s++) {
-                    sets[included[0]].push(...sets[included[s]]);
-                    sets[included[s]].length = 0;
+                    this.sets[included[0]].push(...this.sets[included[s]]);
+                    this.sets[included[s]].length = 0;
                 }
             }
         });
-        for (let s = 0; s < sets.length; s++) {
-            for (let i = 0; i < sets[s].length; i++) {
-                Doc.GetProto(sets[s][i]).cluster = s;
+        for (let s = 0; s < this.sets.length; s++) {
+            for (let i = 0; i < this.sets[s].length; i++) {
+                this.sets[s][i].cluster = s;
+            }
+        }
+    }
+
+    getClusterColor = (doc: Doc) => {
+        if (this.props.Document.useClusters) {
+            let cluster = NumCast(doc.cluster);
+            let set = this.sets.length > cluster ? this.sets[NumCast(doc.cluster)] : undefined;
+            let colors = ["#da42429e", "#31ea318c", "#8c4000", "#4a7ae2c4", "#d809ff", "#ff7601", "#1dffff", "yellow", "#1b8231f2", "#000000ad"];
+            let clusterColor = colors[cluster % colors.length];
+            for (let i = 0; set && i < set.length; i++) {
+                if (set[i].backgroundColor && set[i].backgroundColor !== set[i].defaultBackgroundColor) clusterColor = StrCast(set[i].backgroundColor);
             }
+            return clusterColor;
         }
+        return "";
     }
 
     @action
@@ -249,34 +298,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
     @action
     onPointerMove = (e: PointerEvent): void => {
         if (!e.cancelBubble) {
-            let probe = this.getTransform().transformPoint(e.clientX, e.clientY);
-            let cluster = this.childDocs.reduce((cluster, cd) => {
-                let cx = NumCast(cd.x) - this._groupingBorder;
-                let cy = NumCast(cd.y) - this._groupingBorder;
-                let cw = NumCast(cd.width) + 2 * this._groupingBorder;
-                let ch = NumCast(cd.height) + 2 * this._groupingBorder;
-                if (this.intersectRect({ left: cx, top: cy, width: cw, height: ch }, { left: probe[0], top: probe[1], width: 1, height: 1 }))
-                    return NumCast(cd.cluster);
-                return cluster;
-            }, -1);
-            if (cluster !== -1) {
-                let eles = this.childDocs.filter(cd => NumCast(cd.cluster) === cluster);
-                this.selectDocuments(eles);
-                let clusterDocs = SelectionManager.SelectedDocuments();
-                SelectionManager.DeselectAll();
-                let de = new DragManager.DocumentDragData(eles, eles.map(d => undefined));
-                de.moveDocument = this.props.moveDocument;
-                const [left, top] = clusterDocs[0].props.ScreenToLocalTransform().scale(clusterDocs[0].props.ContentScaling()).inverse().transformPoint(0, 0);
-                const [xoff, yoff] = this.getTransform().transformDirection(e.x - left, e.y - top);
-                de.dropAction = e.ctrlKey || e.altKey ? "alias" : undefined;
-                de.xOffset = xoff;
-                de.yOffset = yoff;
-                DragManager.StartDocumentDrag(clusterDocs.map(v => v.ContentDiv!), de, e.clientX, e.clientY, {
-                    handlers: {
-                        dragComplete: action(emptyFunction)
-                    },
-                    hideSource: !de.dropAction
-                });
+            if (this.props.Document.useClusters && this.tryDragCluster(e)) {
                 e.stopPropagation(); // doesn't actually stop propagation since all our listeners are listening to events on 'document'  however it does mark the event as cancelBubble=true which we test for in the move event handlers
                 e.preventDefault();
                 document.removeEventListener("pointermove", this.onPointerMove);
@@ -493,6 +515,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
             ContentScaling: returnOne,
             ContainingCollectionView: this.props.CollectionView,
             focus: this.focusDocument,
+            backgroundColor: this.getClusterColor,
             parentActive: this.props.active,
             whenActiveChanged: this.props.whenActiveChanged,
             bringToFront: this.bringToFront,
@@ -516,6 +539,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
             ContentScaling: returnOne,
             ContainingCollectionView: this.props.CollectionView,
             focus: this.focusDocument,
+            backgroundColor: returnEmptyString,
             parentActive: this.props.active,
             whenActiveChanged: this.props.whenActiveChanged,
             bringToFront: this.bringToFront,
@@ -625,6 +649,15 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
             event: async () => this.props.Document.fitToBox = !this.fitToBox,
             icon: !this.fitToBox ? "expand-arrows-alt" : "compress-arrows-alt"
         });
+        layoutItems.push({
+            description: `${this.props.Document.useClusters ? "Uncluster" : "Use Clusters"}`,
+            event: async () => {
+                Docs.Prototypes.get(DocumentType.TEXT).defaultBackgroundColor = "#f1efeb";
+                Docs.Prototypes.get(DocumentType.COL).defaultBackgroundColor = "white";
+                this.props.Document.useClusters = !this.props.Document.useClusters;
+            },
+            icon: !this.props.Document.useClusters ? "expand-arrows-alt" : "compress-arrows-alt"
+        });
         layoutItems.push({
             description: "Arrange contents in grid",
             icon: "table",
@@ -700,10 +733,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
         ...this.views
     ]
     private overlayChildViews = () => {
-        console.log(this.overlayViews.length);
-        return [
-            ...this.overlayViews
-        ];
+        return [...this.overlayViews];
     }
 
     public static AddCustomLayout(doc: Doc, dataKey: string): () => void {
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index b9ee588dd..ff96bd993 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -278,6 +278,7 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
                 panX: 0,
                 panY: 0,
                 backgroundColor: this.props.container.isAnnotationOverlay ? undefined : "white",
+                defaultBackgroundColor: this.props.container.isAnnotationOverlay ? undefined : "white",
                 width: bounds.width,
                 height: bounds.height,
                 title: e.key === "s" || e.key === "S" ? "-summary-" : "a nested collection",
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
index 3b6c443c2..ee596c841 100644
--- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
@@ -8,6 +8,7 @@ import { DocumentView, DocumentViewProps, positionSchema } from "./DocumentView"
 import "./DocumentView.scss";
 import React = require("react");
 import { Doc } from "../../../new_fields/Doc";
+import { returnEmptyString } from "../../../Utils";
 
 export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps {
     x?: number;
@@ -69,6 +70,11 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
         return undefined;
     }
 
+    @computed
+    get clusterColor() { return this.props.backgroundColor(this.props.Document); }
+
+    clusterColorFunc = (doc: Doc) => this.clusterColor;
+
     render() {
         const hasPosition = this.props.x !== undefined || this.props.y !== undefined;
         return (
@@ -77,7 +83,10 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
                     transformOrigin: "left top",
                     position: "absolute",
                     backgroundColor: "transparent",
-                    boxShadow: this.props.Document.z ? `#9c9396 ${StrCast(this.props.Document.boxShadow, "10px 10px 0.9vw")}` : undefined,
+                    boxShadow: this.props.Document.z ? `#9c9396  ${StrCast(this.props.Document.boxShadow, "10px 10px 0.9vw")}` :
+                        this.clusterColor ? (
+                            this.props.Document.isBackground ? `0px 0px 50px 50px ${this.clusterColor}` :
+                                `${this.clusterColor} ${StrCast(this.props.Document.boxShadow, `0vw 0vw ${50 / this.props.ContentScaling()}px`)}`) : undefined,
                     borderRadius: this.borderRounding(),
                     transform: this.transform,
                     transition: hasPosition ? "transform 1s" : StrCast(this.props.Document.transition),
@@ -88,6 +97,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
                 <DocumentView {...this.props}
                     ContentScaling={this.contentScaling}
                     ScreenToLocalTransform={this.getTransform}
+                    backgroundColor={this.clusterColorFunc}
                     PanelWidth={this.panelWidth}
                     PanelHeight={this.panelHeight}
                     animateBetweenIcon={this.animateBetweenIcon}
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 16ae5471d..ae6a01ab4 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -1,6 +1,6 @@
 import { library } from '@fortawesome/fontawesome-svg-core';
 import * as fa from '@fortawesome/free-solid-svg-icons';
-import { action, computed, IReactionDisposer, reaction, runInAction } from "mobx";
+import { action, computed, IReactionDisposer, reaction, runInAction, trace } from "mobx";
 import { observer } from "mobx-react";
 import * as rp from "request-promise";
 import { Doc, DocListCast, DocListCastAsync, HeightSym, Opt, WidthSym } from "../../../new_fields/Doc";
@@ -95,6 +95,7 @@ export interface DocumentViewProps {
     addDocTab: (doc: Doc, dataDoc: Doc | undefined, where: string) => void;
     collapseToPoint?: (scrpt: number[], expandedDocs: Doc[] | undefined) => void;
     zoomToScale: (scale: number) => void;
+    backgroundColor: (doc: Doc) => string;
     getScale: () => number;
     animateBetweenIcon?: (iconPos: number[], startTime: number, maximizing: boolean) => void;
     ChromeHeight?: () => number;
@@ -675,12 +676,9 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
         // to determine the render JSX string, otherwise the layout field should directly contain a JSX layout string.
         return this.props.Document.layout instanceof Doc ? this.props.Document.layout : this.props.Document;
     }
+
     render() {
-        if (this.Document.hidden) {
-            return null;
-        }
-        let self = this;
-        let backgroundColor = StrCast(this.layoutDoc.backgroundColor);
+        let backgroundColor = this.props.backgroundColor(this.props.Document) || StrCast(this.layoutDoc.backgroundColor);
         let foregroundColor = StrCast(this.layoutDoc.color);
         var nativeWidth = this.nativeWidth > 0 && !BoolCast(this.props.Document.ignoreAspect) ? `${this.nativeWidth}px` : "100%";
         var nativeHeight = BoolCast(this.props.Document.ignoreAspect) ? this.props.PanelHeight() / this.props.ContentScaling() : this.nativeHeight > 0 ? `${this.nativeHeight}px` : "100%";
@@ -695,8 +693,6 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
             });
         }
         let showTextTitle = showTitle && StrCast(this.layoutDoc.layout).startsWith("<FormattedTextBox") ? showTitle : undefined;
-        let colors = ["#da42429e", "#31ea318c", "#8c4000", "#4a7ae2c4", "#d809ff", "#ff7601", "#1dffff", "yellow", "#1b8231f2", "#000000ad"];
-        let groupCol = colors[NumCast(this.props.Document.cluster) % colors.length];
         return (
             <div className={`documentView-node${this.topMost ? "-topmost" : ""}`}
                 ref={this._mainCont}
@@ -705,9 +701,6 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
                     color: foregroundColor,
                     outlineColor: "maroon",
                     outlineStyle: "dashed",
-                    boxShadow: this.layoutDoc.isBackground ?
-                        `0px 0px 50px 50px ${groupCol}` :
-                        `${groupCol} ${StrCast(this.props.Document.boxShadow, `0vw 0vw ${50 / this.props.ContentScaling()}px`)}`,
                     outlineWidth: BoolCast(this.layoutDoc.libraryBrush) && !StrCast(Doc.GetProto(this.props.Document).borderRounding) ?
                         `${this.props.ScreenToLocalTransform().Scale}px` : "0px",
                     marginLeft: BoolCast(this.layoutDoc.libraryBrush) && StrCast(Doc.GetProto(this.props.Document).borderRounding) ?
@@ -717,7 +710,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
                     border: BoolCast(this.layoutDoc.libraryBrush) && StrCast(Doc.GetProto(this.props.Document).borderRounding) ?
                         `dashed maroon ${this.props.ScreenToLocalTransform().Scale}px` : undefined,
                     borderRadius: "inherit",
-                    background: this.layoutDoc.isBackground ? groupCol : backgroundColor,
+                    background: backgroundColor,
                     width: nativeWidth,
                     height: nativeHeight,
                     transform: `scale(${this.props.ContentScaling()})`,
diff --git a/src/client/views/presentationview/PresentationElement.tsx b/src/client/views/presentationview/PresentationElement.tsx
index 11f3eb846..e2d8daea9 100644
--- a/src/client/views/presentationview/PresentationElement.tsx
+++ b/src/client/views/presentationview/PresentationElement.tsx
@@ -9,7 +9,7 @@ import { Id } from "../../../new_fields/FieldSymbols";
 import { List } from "../../../new_fields/List";
 import { listSpec } from "../../../new_fields/Schema";
 import { BoolCast, Cast, NumCast, StrCast } from "../../../new_fields/Types";
-import { Utils, returnFalse, emptyFunction, returnOne } from "../../../Utils";
+import { Utils, returnFalse, emptyFunction, returnOne, returnEmptyString } from "../../../Utils";
 import { DragManager, dropActionType, SetupDrag } from "../../util/DragManager";
 import { SelectionManager } from "../../util/SelectionManager";
 import { ContextMenu } from "../ContextMenu";
@@ -843,6 +843,7 @@ export default class PresentationElement extends React.Component<PresentationEle
                     PanelWidth={() => 350}
                     PanelHeight={() => 90}
                     focus={emptyFunction}
+                    backgroundColor={returnEmptyString}
                     selectOnLoad={false}
                     parentActive={returnFalse}
                     whenActiveChanged={returnFalse}
diff --git a/src/client/views/search/SearchItem.tsx b/src/client/views/search/SearchItem.tsx
index 0390359b3..1b9bba5c6 100644
--- a/src/client/views/search/SearchItem.tsx
+++ b/src/client/views/search/SearchItem.tsx
@@ -7,7 +7,7 @@ import { observer } from "mobx-react";
 import { Doc, DocListCast, HeightSym, WidthSym } from "../../../new_fields/Doc";
 import { Id } from "../../../new_fields/FieldSymbols";
 import { Cast, NumCast, StrCast } from "../../../new_fields/Types";
-import { emptyFunction, returnFalse, returnOne, Utils } from "../../../Utils";
+import { emptyFunction, returnFalse, returnOne, Utils, returnEmptyString } from "../../../Utils";
 import { DocumentType } from "../../documents/Documents";
 import { DocumentManager } from "../../util/DocumentManager";
 import { SetupDrag, DragManager } from "../../util/DragManager";
@@ -223,6 +223,7 @@ export class SearchItem extends React.Component<SearchItemProps> {
                     PanelWidth={returnXDimension}
                     PanelHeight={returnYDimension}
                     focus={emptyFunction}
+                    backgroundColor={returnEmptyString}
                     selectOnLoad={false}
                     parentActive={returnFalse}
                     whenActiveChanged={returnFalse}
-- 
cgit v1.2.3-70-g09d2


From 813476d753f0286d546a0b1c9f1b1774481604e0 Mon Sep 17 00:00:00 2001
From: Sam Wilkins <samwilkins333@gmail.com>
Date: Sat, 3 Aug 2019 17:17:43 -0400
Subject: final refactor and commenting

---
 src/client/util/DictationManager.ts  | 29 +++++++++++++++++++++++++++--
 src/client/views/GlobalKeyHandler.ts | 14 +++++++-------
 src/client/views/MainView.tsx        | 16 ++++++++++++++++
 3 files changed, 50 insertions(+), 9 deletions(-)

(limited to 'src/client/views/MainView.tsx')

diff --git a/src/client/util/DictationManager.ts b/src/client/util/DictationManager.ts
index 2ed74a729..b799238ba 100644
--- a/src/client/util/DictationManager.ts
+++ b/src/client/util/DictationManager.ts
@@ -1,6 +1,6 @@
 import { SelectionManager } from "./SelectionManager";
 import { DocumentView } from "../views/nodes/DocumentView";
-import { undoBatch } from "./UndoManager";
+import { UndoManager } from "./UndoManager";
 import * as converter from "words-to-numbers";
 import { Doc, Opt } from "../../new_fields/Doc";
 import { List } from "../../new_fields/List";
@@ -11,8 +11,26 @@ import { listSpec } from "../../new_fields/Schema";
 import { AudioField, ImageField } from "../../new_fields/URLField";
 import { HistogramField } from "../northstar/dash-fields/HistogramField";
 
+/**
+ * This namespace provides a singleton instance of a manager that
+ * handles the listening and text-conversion of user speech.
+ * 
+ * The basic manager functionality can be attained by the DictationManager.Controls namespace, which provide
+ * a simple recording operation that returns the interpreted text as a string.
+ * 
+ * Additionally, however, the DictationManager also exposes the ability to execute voice commands within Dash.
+ * It stores a default library of registered commands that can be triggered by listen()'ing for a phrase and then
+ * passing the results into the execute() function.
+ * 
+ * In addition to compile-time default commands, you can invoke DictationManager.Commands.Register(Independent|Dependent)
+ * to add new commands as classes or components are constructed.
+ */
 export namespace DictationManager {
 
+    /**
+     * Some type maneuvering to access Webkit's built-in
+     * speech recognizer.
+     */
     namespace CORE {
         export interface IWindow extends Window {
             webkitSpeechRecognition: any;
@@ -24,6 +42,7 @@ export namespace DictationManager {
     const recognizer = (() => {
         let initialized = new webkitSpeechRecognition();
         initialized.continuous = true;
+        initialized.language = "en-US";
         return initialized;
     })();
 
@@ -77,6 +96,8 @@ export namespace DictationManager {
 
     export namespace Commands {
 
+        export const dictationFadeDuration = 2000;
+
         export type IndependentAction = (target: DocumentView) => any | Promise<any>;
         export type IndependentEntry = { action: IndependentAction, restrictTo?: DocumentType[] };
 
@@ -91,14 +112,16 @@ export namespace DictationManager {
             if (!targets || !targets.length) {
                 return;
             }
-
+            phrase = phrase.toLowerCase();
             let entry = Independent.get(phrase);
             if (entry) {
                 let success = false;
                 let restrictTo = entry.restrictTo;
                 for (let target of targets) {
                     if (!restrictTo || validate(target, restrictTo)) {
+                        let batch = UndoManager.StartBatch("Independent Command");
                         await entry.action(target);
+                        batch.end();
                         success = true;
                     }
                 }
@@ -114,7 +137,9 @@ export namespace DictationManager {
                     let restrictTo = entry.restrictTo;
                     for (let target of targets) {
                         if (!restrictTo || validate(target, restrictTo)) {
+                            let batch = UndoManager.StartBatch("Dependent Command");
                             await entry.action(target, matches);
+                            batch.end();
                             success = true;
                         }
                     }
diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts
index b1ea92bb8..82289c249 100644
--- a/src/client/views/GlobalKeyHandler.ts
+++ b/src/client/views/GlobalKeyHandler.ts
@@ -74,7 +74,7 @@ export default class KeyManager {
                 DictationManager.Controls.stop();
                 main.dictationOverlayVisible = false;
                 main.dictationSuccess = undefined;
-                main.overlayTimeout && clearTimeout(main.overlayTimeout);
+                main.cancelDictationFade();
                 break;
             case "delete":
             case "backspace":
@@ -110,19 +110,19 @@ export default class KeyManager {
             case " ":
                 let main = MainView.Instance;
                 main.dictationOverlayVisible = true;
+
                 main.isListening = true;
                 let command = await DictationManager.Controls.listen((results: any) => console.log(results));
                 main.isListening = false;
+
                 if (!command) {
                     break;
                 }
-                command = command.toLowerCase();
-                main.dictatedPhrase = command;
+
+                main.dictatedPhrase = command = command.toLowerCase();
                 main.dictationSuccess = await DictationManager.Commands.execute(command);
-                main.overlayTimeout = setTimeout(() => {
-                    main.dictationOverlayVisible = false;
-                    main.dictationSuccess = undefined;
-                }, 2000);
+                main.initiateDictationFade();
+
                 stopPropagation = true;
                 preventDefault = true;
         }
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 4a5e4a3d1..748e1e634 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -39,6 +39,7 @@ import { FilterBox } from './search/FilterBox';
 import { CollectionTreeView } from './collections/CollectionTreeView';
 import { ClientUtils } from '../util/ClientUtils';
 import { SchemaHeaderField, RandomPastel } from '../../new_fields/SchemaHeaderField';
+import { DictationManager } from '../util/DictationManager';
 
 @observer
 export class MainView extends React.Component {
@@ -55,6 +56,21 @@ export class MainView extends React.Component {
 
     public overlayTimeout: NodeJS.Timeout | undefined;
 
+    public initiateDictationFade = () => {
+        let duration = DictationManager.Commands.dictationFadeDuration;
+        this.overlayTimeout = setTimeout(() => {
+            this.dictationOverlayVisible = false;
+            this.dictationSuccess = undefined;
+        }, duration);
+    }
+
+    public cancelDictationFade = () => {
+        if (this.overlayTimeout) {
+            clearTimeout(this.overlayTimeout);
+            this.overlayTimeout = undefined;
+        }
+    }
+
     @computed private get mainContainer(): Opt<Doc> {
         return FieldValue(Cast(CurrentUserUtils.UserDocument.activeWorkspace, Doc));
     }
-- 
cgit v1.2.3-70-g09d2


From d6fda11588f1a117e8acc30ea5600d34ff22e01b Mon Sep 17 00:00:00 2001
From: Sam Wilkins <samwilkins333@gmail.com>
Date: Sat, 3 Aug 2019 21:29:56 -0400
Subject: now differentiate between continuous, indefinite and, separately,
 interim vs final only dictation results

---
 src/client/util/DictationManager.ts     | 81 +++++++++++++++++++++------------
 src/client/views/GlobalKeyHandler.ts    |  3 +-
 src/client/views/MainView.tsx           |  2 +-
 src/client/views/nodes/DocumentView.tsx |  3 +-
 src/new_fields/Doc.ts                   |  2 +-
 5 files changed, 59 insertions(+), 32 deletions(-)

(limited to 'src/client/views/MainView.tsx')

diff --git a/src/client/util/DictationManager.ts b/src/client/util/DictationManager.ts
index b02a5ecbe..89797f101 100644
--- a/src/client/util/DictationManager.ts
+++ b/src/client/util/DictationManager.ts
@@ -39,57 +39,82 @@ export namespace DictationManager {
     const { webkitSpeechRecognition }: CORE.IWindow = window as CORE.IWindow;
 
     let isListening = false;
-    const recognizer = (() => {
-        let initialized = new webkitSpeechRecognition();
-        initialized.continuous = true;
-        initialized.language = "en-US";
-        return initialized;
-    })();
+    let isManuallyStopped = false;
+    const recognizer: SpeechRecognition = new webkitSpeechRecognition();
 
     export namespace Controls {
 
+        let newestResult: string;
         export type InterimResultHandler = (results: any) => any;
+        export type ContinuityArgs = { indefinite: boolean } | false;
+        export interface ListeningOptions {
+            language: string;
+            continuous: ContinuityArgs;
+            interimHandler: InterimResultHandler;
+            delimiter: string;
+        }
 
-        export const listen = (handler: Opt<InterimResultHandler> = undefined) => {
+        export const listen = (options?: Partial<ListeningOptions>) => {
             if (isListening) {
                 return undefined;
             }
             isListening = true;
 
+            let handler = options ? options.interimHandler : undefined;
+            let continuous = options ? options.continuous : undefined;
+            let language = options ? options.language : undefined;
+            let delimiter = options ? options.delimiter : undefined;
+
             recognizer.interimResults = handler !== undefined;
+            recognizer.continuous = continuous === undefined ? false : continuous !== false;
+            recognizer.lang = language === undefined ? "en-US" : language;
+
             recognizer.start();
 
             return new Promise<string>((resolve, reject) => {
+
                 recognizer.onerror = (e: any) => {
                     reject(e);
                     stop();
                 };
-                if (handler) {
-                    let newestResult: string;
-                    recognizer.onresult = (e: any) => {
-                        newestResult = e.results[0][0].transcript;
-                        handler(newestResult);
-                    };
-                    recognizer.onend = (e: any) => {
+
+                recognizer.onresult = (e: SpeechRecognitionEvent) => {
+                    newestResult = synthesize(e, delimiter);
+                    handler && handler(newestResult);
+                };
+
+                recognizer.onend = (e: Event) => {
+                    if (continuous && continuous.indefinite && !isManuallyStopped) {
+                        recognizer.start();
+                    } else {
                         resolve(newestResult);
-                        stop();
-                    };
-                } else {
-                    recognizer.onresult = (e: any) => {
-                        let finalResult = e.results[0][0].transcript;
-                        resolve(finalResult);
-                        stop();
-                    };
-                }
+                        reset();
+                    }
+                };
+
             });
         };
 
-        export const stop = () => {
-            recognizer.stop();
+        export const stop = (saveCumulative = true) => {
+            saveCumulative ? recognizer.stop() : recognizer.abort();
+            reset();
+        };
+
+        const reset = () => {
             isListening = false;
-            recognizer.onresult = undefined;
-            recognizer.onend = undefined;
-            recognizer.onerror = undefined;
+            isManuallyStopped = false;
+            recognizer.onresult = null;
+            recognizer.onend = null;
+            recognizer.onerror = null;
+        };
+
+        const synthesize = (e: SpeechRecognitionEvent, delimiter?: string) => {
+            let results = e.results;
+            let transcripts: string[] = [];
+            for (let i = 0; i < results.length; i++) {
+                transcripts.push(results.item(i).item(0).transcript.trim());
+            }
+            return transcripts.join(delimiter || "...");
         };
 
     }
diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts
index 82289c249..98df43a1e 100644
--- a/src/client/views/GlobalKeyHandler.ts
+++ b/src/client/views/GlobalKeyHandler.ts
@@ -112,7 +112,8 @@ export default class KeyManager {
                 main.dictationOverlayVisible = true;
 
                 main.isListening = true;
-                let command = await DictationManager.Controls.listen((results: any) => console.log(results));
+                // let printer = (results: any) => console.log(results);
+                let command = await DictationManager.Controls.listen();
                 main.isListening = false;
 
                 if (!command) {
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 748e1e634..5cec34293 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -49,7 +49,7 @@ export class MainView extends React.Component {
     @observable public pwidth: number = 0;
     @observable public pheight: number = 0;
 
-    @observable private dictationState = "Listening...";
+    @observable private dictationState = "";
     @observable private dictationSuccessState: boolean | undefined = undefined;
     @observable private dictationDisplayState = false;
     @observable private dictationListeningState = false;
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index c5d526a5a..bd87bf21b 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -539,7 +539,8 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
     }
 
     listen = async () => {
-        let transcript = await DictationManager.Controls.listen();
+        let options = { continuous: { indefinite: true }, delimiter: " " };
+        let transcript = await DictationManager.Controls.listen(options);
         transcript && (Doc.GetProto(this.props.Document).transcript = transcript);
     }
 
diff --git a/src/new_fields/Doc.ts b/src/new_fields/Doc.ts
index 84b8589dd..979574f16 100644
--- a/src/new_fields/Doc.ts
+++ b/src/new_fields/Doc.ts
@@ -531,7 +531,7 @@ export namespace Doc {
                 d.layout = detailLayout;
                 d.nativeWidth = d.nativeHeight = undefined;
                 if (detailLayout instanceof Doc) {
-                    let delegDetailLayout = Doc.MakeDelegate(detailLayout) as Doc;
+                    let delegDetailLayout = Doc.MakeDelegate(detailLayout);
                     d.layout = delegDetailLayout;
                     let subDetailLayout1 = await PromiseValue(delegDetailLayout.detailedLayout);
                     let subDetailLayout = await PromiseValue(delegDetailLayout.detailedLayout);
-- 
cgit v1.2.3-70-g09d2


From 6d718c8a243e68d23199d35592bfded285385c91 Mon Sep 17 00:00:00 2001
From: Sam Wilkins <samwilkins333@gmail.com>
Date: Sun, 4 Aug 2019 05:09:56 -0400
Subject: now all listen() calls display recording UI

---
 src/client/util/DictationManager.ts     | 24 ++++++++++++++++++++----
 src/client/views/GlobalKeyHandler.ts    | 21 ++++-----------------
 src/client/views/Main.scss              |  1 -
 src/client/views/MainView.tsx           | 11 ++++++++---
 src/client/views/nodes/DocumentView.tsx | 16 +++++++++++++---
 5 files changed, 45 insertions(+), 28 deletions(-)

(limited to 'src/client/views/MainView.tsx')

diff --git a/src/client/util/DictationManager.ts b/src/client/util/DictationManager.ts
index b6f871713..a882994c1 100644
--- a/src/client/util/DictationManager.ts
+++ b/src/client/util/DictationManager.ts
@@ -10,6 +10,7 @@ import { Cast, CastCtor } from "../../new_fields/Types";
 import { listSpec } from "../../new_fields/Schema";
 import { AudioField, ImageField } from "../../new_fields/URLField";
 import { HistogramField } from "../northstar/dash-fields/HistogramField";
+import { MainView } from "../views/MainView";
 
 /**
  * This namespace provides a singleton instance of a manager that
@@ -37,9 +38,11 @@ export namespace DictationManager {
         }
     }
     const { webkitSpeechRecognition }: CORE.IWindow = window as CORE.IWindow;
+    export const placeholder = "Listening...";
 
     export namespace Controls {
 
+        const infringe = "unable to process: dictation manager still involved in previous session";
         const intraSession = ". ";
         const interSession = " ... ";
 
@@ -52,7 +55,7 @@ export namespace DictationManager {
         const recognizer: SpeechRecognition = new webkitSpeechRecognition() || new SpeechRecognition();
         recognizer.onstart = () => console.log("initiating speech recognition session...");
 
-        export type InterimResultHandler = (results: any) => any;
+        export type InterimResultHandler = (results: string) => any;
         export type ContinuityArgs = { indefinite: boolean } | false;
         export type DelimiterArgs = { inter: string, intra: string };
 
@@ -61,21 +64,33 @@ export namespace DictationManager {
             continuous: ContinuityArgs;
             delimiters: DelimiterArgs;
             interimHandler: InterimResultHandler;
+            tryExecute: boolean;
         }
 
         export const listen = async (options?: Partial<ListeningOptions>) => {
-            let results: any;
+            let results: string | undefined;
+            MainView.Instance.dictationOverlayVisible = true;
+            MainView.Instance.isListening = true;
             try {
                 results = await listenImpl(options);
+                if (results) {
+                    MainView.Instance.isListening = false;
+                    MainView.Instance.dictatedPhrase = results = results.toLowerCase();
+                    MainView.Instance.dictationSuccess = options && options.tryExecute ? await DictationManager.Commands.execute(results) : true;
+                }
             } catch (e) {
-                results = `Dictation Error: ${"error" in e ? e.error : "unknown error"}`;
+                MainView.Instance.isListening = false;
+                MainView.Instance.dictatedPhrase = results = `dictation error: ${"error" in e ? e.error : "unknown error"}`;
+                MainView.Instance.dictationSuccess = false;
+            } finally {
+                MainView.Instance.initiateDictationFade();
             }
             return results;
         };
 
         const listenImpl = (options?: Partial<ListeningOptions>) => {
             if (isListening) {
-                return undefined;
+                return infringe;
             }
             isListening = true;
 
@@ -126,6 +141,7 @@ export namespace DictationManager {
                     } else {
                         resolve(current);
                     }
+                    current = undefined;
                     reset();
                 };
 
diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts
index c3e6ae6c8..0989e8db1 100644
--- a/src/client/views/GlobalKeyHandler.ts
+++ b/src/client/views/GlobalKeyHandler.ts
@@ -73,9 +73,11 @@ export default class KeyManager {
                 SelectionManager.DeselectAll();
                 DictationManager.Controls.stop();
                 if (main.dictationOverlayVisible) {
+                    main.cancelDictationFade();
                     main.dictationOverlayVisible = false;
+                    main.isListening = true;
+                    main.dictatedPhrase = "";
                     main.dictationSuccess = undefined;
-                    main.cancelDictationFade();
                 }
                 break;
             case "delete":
@@ -110,22 +112,7 @@ export default class KeyManager {
 
         switch (keyname) {
             case " ":
-                let main = MainView.Instance;
-                main.dictationOverlayVisible = true;
-
-                main.isListening = true;
-                // let printer = (results: any) => console.log(results);
-                let command = await DictationManager.Controls.listen();
-                main.isListening = false;
-
-                if (!command) {
-                    break;
-                }
-
-                main.dictatedPhrase = command = command.toLowerCase();
-                main.dictationSuccess = await DictationManager.Commands.execute(command);
-                main.initiateDictationFade();
-
+                DictationManager.Controls.listen({ tryExecute: true });
                 stopPropagation = true;
                 preventDefault = true;
         }
diff --git a/src/client/views/Main.scss b/src/client/views/Main.scss
index 8e57b88c3..f76abaff3 100644
--- a/src/client/views/Main.scss
+++ b/src/client/views/Main.scss
@@ -292,7 +292,6 @@ ul#add-options-list {
     width: 100%;
     height: 100%;
     position: absolute;
-    background: darkslategray;
     z-index: 999;
     transition: 0.5s all ease;
     pointer-events: none;
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 4443eea6d..383efa1e3 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -1,7 +1,7 @@
 import { IconName, library } from '@fortawesome/fontawesome-svg-core';
 import { faArrowDown, faCloudUploadAlt, faArrowUp, faClone, faCheck, faPlay, faPause, faCaretUp, faLongArrowAltRight, faCommentAlt, faCut, faExclamation, faFilePdf, faFilm, faFont, faGlobeAsia, faPortrait, faMusic, faObjectGroup, faPenNib, faRedoAlt, faTable, faThumbtack, faTree, faUndoAlt, faCat, faBolt } from '@fortawesome/free-solid-svg-icons';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { action, computed, configure, observable, runInAction, reaction, trace } from 'mobx';
+import { action, computed, configure, observable, runInAction, reaction, trace, autorun } from 'mobx';
 import { observer } from 'mobx-react';
 import "normalize.css";
 import * as React from 'react';
@@ -167,6 +167,8 @@ export class MainView extends React.Component {
             }
         }
 
+        autorun(() => console.log(`this.isListening = ${this.isListening}`));
+
         library.add(faFont);
         library.add(faExclamation);
         library.add(faPortrait);
@@ -523,7 +525,7 @@ export class MainView extends React.Component {
     render() {
         let display = this.dictationOverlayVisible;
         let success = this.dictationSuccess;
-        let result = this.isListening ? "Listening..." : `"${this.dictatedPhrase}"`;
+        let result = this.isListening ? DictationManager.placeholder : `"${this.dictatedPhrase}"`;
         return (
             <div id="main-div">
                 <div
@@ -536,7 +538,10 @@ export class MainView extends React.Component {
                 >{result}</div>
                 <div
                     className={"dictation-prompt-overlay"}
-                    style={{ opacity: display ? 0.4 : 0 }}
+                    style={{
+                        opacity: display ? 0.4 : 0,
+                        backgroundColor: this.isListening ? "red" : "darkslategrey"
+                    }}
                 />
                 <DocumentDecorations />
                 {this.mainContent}
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 1d9cb3c80..a415aefda 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -38,6 +38,7 @@ import "./DocumentView.scss";
 import { FormattedTextBox } from './FormattedTextBox';
 import React = require("react");
 import { DictationManager } from '../../util/DictationManager';
+import { MainView } from '../MainView';
 const JsxParser = require('react-jsx-parser').default; //TODO Why does this need to be imported like this?
 
 library.add(fa.faTrash);
@@ -536,9 +537,18 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
     }
 
     listen = async () => {
-        let options = { continuous: { indefinite: true }, delimiter: " " };
-        let transcript = await DictationManager.Controls.listen(options);
-        transcript && (Doc.GetProto(this.props.Document).transcript = transcript);
+        let dataDoc = Doc.GetProto(this.props.Document);
+        let options = {
+            continuous: { indefinite: true },
+            delimiter: " ",
+            interimHandler: (results: string) => {
+                MainView.Instance.isListening = false;
+                MainView.Instance.dictationSuccess = true;
+                MainView.Instance.dictatedPhrase = results;
+            }
+        };
+        let final = await DictationManager.Controls.listen(options);
+        final && (dataDoc.transcript = final);
     }
 
     @action
-- 
cgit v1.2.3-70-g09d2


From 0010f88e1002feb14ecfb111c2c6ae56ee34cf2d Mon Sep 17 00:00:00 2001
From: Sam Wilkins <samwilkins333@gmail.com>
Date: Sun, 4 Aug 2019 13:32:19 -0400
Subject: clean up and UI fix

---
 src/client/util/DictationManager.ts     | 43 +++++++++++++++++++--------------
 src/client/views/MainView.tsx           | 11 ++++-----
 src/client/views/nodes/DocumentView.tsx | 20 ++++++---------
 3 files changed, 38 insertions(+), 36 deletions(-)

(limited to 'src/client/views/MainView.tsx')

diff --git a/src/client/util/DictationManager.ts b/src/client/util/DictationManager.ts
index 2af7c53cb..ee1f11b4f 100644
--- a/src/client/util/DictationManager.ts
+++ b/src/client/util/DictationManager.ts
@@ -58,6 +58,7 @@ export namespace DictationManager {
         export type InterimResultHandler = (results: string) => any;
         export type ContinuityArgs = { indefinite: boolean } | false;
         export type DelimiterArgs = { inter: string, intra: string };
+        export type ListeningUIStatus = { interim: boolean } | false;
 
         export interface ListeningOptions {
             language: string;
@@ -69,22 +70,28 @@ export namespace DictationManager {
 
         export const listen = async (options?: Partial<ListeningOptions>) => {
             let results: string | undefined;
-            MainView.Instance.dictationOverlayVisible = true;
-            MainView.Instance.isListening = true;
+            let main = MainView.Instance;
+
+            main.dictationOverlayVisible = true;
+            let interim = options !== undefined && options.interimHandler !== undefined;
+            main.isListening = { interim: interim };
+
             try {
                 results = await listenImpl(options);
                 if (results) {
-                    MainView.Instance.isListening = false;
-                    MainView.Instance.dictatedPhrase = results = results.toLowerCase();
-                    MainView.Instance.dictationSuccess = options && options.tryExecute ? await DictationManager.Commands.execute(results) : true;
+                    main.isListening = false;
+                    main.dictatedPhrase = results;
+                    let execute = options && options.tryExecute;
+                    main.dictationSuccess = execute ? await DictationManager.Commands.execute(results) : true;
                 }
             } catch (e) {
-                MainView.Instance.isListening = false;
-                MainView.Instance.dictatedPhrase = results = `dictation error: ${"error" in e ? e.error : "unknown error"}`;
-                MainView.Instance.dictationSuccess = false;
+                main.isListening = false;
+                main.dictatedPhrase = results = `dictation error: ${"error" in e ? e.error : "unknown error"}`;
+                main.dictationSuccess = false;
             } finally {
-                MainView.Instance.initiateDictationFade();
+                main.initiateDictationFade();
             }
+
             return results;
         };
 
@@ -137,11 +144,10 @@ export namespace DictationManager {
                 let complete = () => {
                     if (indefinite) {
                         current && sessionResults.push(current);
-                        resolve(sessionResults.join(inter || interSession));
+                        sessionResults.length && resolve(sessionResults.join(inter || interSession));
                     } else {
                         resolve(current);
                     }
-                    current = undefined;
                     reset();
                 };
 
@@ -154,12 +160,12 @@ export namespace DictationManager {
             }
             isManuallyStopped = true;
             salvageSession ? recognizer.stop() : recognizer.abort();
-            if (MainView.Instance.dictationOverlayVisible) {
-                MainView.Instance.cancelDictationFade();
-                MainView.Instance.dictationOverlayVisible = false;
-                MainView.Instance.isListening = true;
-                MainView.Instance.dictatedPhrase = "";
-                MainView.Instance.dictationSuccess = undefined;
+            let main = MainView.Instance;
+            if (main.dictationOverlayVisible) {
+                main.cancelDictationFade();
+                main.dictationOverlayVisible = false;
+                main.dictationSuccess = undefined;
+                setTimeout(() => main.dictatedPhrase = placeholder, 500);
             }
         };
 
@@ -173,12 +179,13 @@ export namespace DictationManager {
         };
 
         const reset = () => {
+            current = undefined;
+            sessionResults = [];
             isListening = false;
             isManuallyStopped = false;
             recognizer.onresult = null;
             recognizer.onerror = null;
             recognizer.onend = null;
-            sessionResults = [];
         };
 
     }
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 383efa1e3..631d24cb1 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -49,10 +49,10 @@ export class MainView extends React.Component {
     @observable public pwidth: number = 0;
     @observable public pheight: number = 0;
 
-    @observable private dictationState = "";
+    @observable private dictationState = DictationManager.placeholder;
     @observable private dictationSuccessState: boolean | undefined = undefined;
     @observable private dictationDisplayState = false;
-    @observable private dictationListeningState = false;
+    @observable private dictationListeningState: DictationManager.Controls.ListeningUIStatus = false;
 
     public overlayTimeout: NodeJS.Timeout | undefined;
 
@@ -61,6 +61,7 @@ export class MainView extends React.Component {
         this.overlayTimeout = setTimeout(() => {
             this.dictationOverlayVisible = false;
             this.dictationSuccess = undefined;
+            setTimeout(() => this.dictatedPhrase = DictationManager.placeholder, 500);
         }, duration);
     }
 
@@ -116,7 +117,7 @@ export class MainView extends React.Component {
         return this.dictationListeningState;
     }
 
-    public set isListening(value: boolean) {
+    public set isListening(value: DictationManager.Controls.ListeningUIStatus) {
         runInAction(() => this.dictationListeningState = value);
     }
 
@@ -167,8 +168,6 @@ export class MainView extends React.Component {
             }
         }
 
-        autorun(() => console.log(`this.isListening = ${this.isListening}`));
-
         library.add(faFont);
         library.add(faExclamation);
         library.add(faPortrait);
@@ -525,7 +524,7 @@ export class MainView extends React.Component {
     render() {
         let display = this.dictationOverlayVisible;
         let success = this.dictationSuccess;
-        let result = this.isListening ? DictationManager.placeholder : `"${this.dictatedPhrase}"`;
+        let result = this.isListening && !this.isListening.interim ? DictationManager.placeholder : `"${this.dictatedPhrase}"`;
         return (
             <div id="main-div">
                 <div
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 458fb582d..d6ae17537 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -536,19 +536,15 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
         this.props.Document.lockedPosition = BoolCast(this.props.Document.lockedPosition) ? undefined : true;
     }
 
-    listen = async () => {
-        let dataDoc = Doc.GetProto(this.props.Document);
-        let handler = (results: string) => {
-            MainView.Instance.isListening = false;
-            MainView.Instance.dictationSuccess = true;
-            MainView.Instance.dictatedPhrase = results;
-        };
-        let final = await DictationManager.Controls.listen({
+    listen = async () =>
+        Doc.GetProto(this.props.Document).transcript = await DictationManager.Controls.listen({
             continuous: { indefinite: true },
-            interimHandler: handler
-        });
-        final && (dataDoc.transcript = final);
-    }
+            interimHandler: (results: string) => {
+                MainView.Instance.isListening = { interim: true };
+                MainView.Instance.dictationSuccess = true;
+                MainView.Instance.dictatedPhrase = results;
+            }
+        })
 
     @action
     onContextMenu = async (e: React.MouseEvent): Promise<void> => {
-- 
cgit v1.2.3-70-g09d2


From 857e98b16f693db968b04ede62e3f864f8b2ed35 Mon Sep 17 00:00:00 2001
From: Sam Wilkins <samwilkins333@gmail.com>
Date: Sun, 4 Aug 2019 14:04:22 -0400
Subject: final UI tweaks

---
 src/client/util/DictationManager.ts     |  5 ++---
 src/client/views/MainView.tsx           |  8 ++++----
 src/client/views/nodes/DocumentView.tsx | 12 +++++++-----
 3 files changed, 13 insertions(+), 12 deletions(-)

(limited to 'src/client/views/MainView.tsx')

diff --git a/src/client/util/DictationManager.ts b/src/client/util/DictationManager.ts
index ee1f11b4f..978889830 100644
--- a/src/client/util/DictationManager.ts
+++ b/src/client/util/DictationManager.ts
@@ -73,15 +73,14 @@ export namespace DictationManager {
             let main = MainView.Instance;
 
             main.dictationOverlayVisible = true;
-            let interim = options !== undefined && options.interimHandler !== undefined;
-            main.isListening = { interim: interim };
+            main.isListening = { interim: false };
 
             try {
                 results = await listenImpl(options);
                 if (results) {
                     main.isListening = false;
-                    main.dictatedPhrase = results;
                     let execute = options && options.tryExecute;
+                    main.dictatedPhrase = execute ? results.toLowerCase() : results;
                     main.dictationSuccess = execute ? await DictationManager.Commands.execute(results) : true;
                 }
             } catch (e) {
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 631d24cb1..618e83bfa 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -89,7 +89,7 @@ export class MainView extends React.Component {
         }
     }
 
-    public get dictatedPhrase() {
+    @computed public get dictatedPhrase() {
         return this.dictationState;
     }
 
@@ -97,7 +97,7 @@ export class MainView extends React.Component {
         runInAction(() => this.dictationState = value);
     }
 
-    public get dictationSuccess() {
+    @computed public get dictationSuccess() {
         return this.dictationSuccessState;
     }
 
@@ -105,7 +105,7 @@ export class MainView extends React.Component {
         runInAction(() => this.dictationSuccessState = value);
     }
 
-    public get dictationOverlayVisible() {
+    @computed public get dictationOverlayVisible() {
         return this.dictationDisplayState;
     }
 
@@ -113,7 +113,7 @@ export class MainView extends React.Component {
         runInAction(() => this.dictationDisplayState = value);
     }
 
-    public get isListening() {
+    @computed public get isListening() {
         return this.dictationListeningState;
     }
 
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index d6ae17537..57e66ff1b 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -536,15 +536,17 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
         this.props.Document.lockedPosition = BoolCast(this.props.Document.lockedPosition) ? undefined : true;
     }
 
-    listen = async () =>
+    listen = async () => {
         Doc.GetProto(this.props.Document).transcript = await DictationManager.Controls.listen({
             continuous: { indefinite: true },
             interimHandler: (results: string) => {
-                MainView.Instance.isListening = { interim: true };
-                MainView.Instance.dictationSuccess = true;
-                MainView.Instance.dictatedPhrase = results;
+                let main = MainView.Instance;
+                main.dictationSuccess = true;
+                main.dictatedPhrase = results;
+                main.isListening = { interim: true };
             }
-        })
+        });
+    }
 
     @action
     onContextMenu = async (e: React.MouseEvent): Promise<void> => {
-- 
cgit v1.2.3-70-g09d2


From 35cb61f0d93983464b29152f159d09ea6bd4edf9 Mon Sep 17 00:00:00 2001
From: Tyler Schicke <tyler_schicke@brown.edu>
Date: Wed, 7 Aug 2019 21:37:14 -0400
Subject: Mostly have field level read only working

---
 src/client/DocServer.ts           | 34 +++++++++++++++++++++++++++++++++-
 src/client/views/MainView.tsx     |  8 ++++++++
 src/client/views/nodes/WebBox.tsx |  3 ++-
 src/new_fields/Doc.ts             | 38 +++++++++++++++++++++++++++++++-------
 src/new_fields/util.ts            | 11 +++++++++--
 src/server/GarbageCollector.ts    |  2 +-
 6 files changed, 84 insertions(+), 12 deletions(-)

(limited to 'src/client/views/MainView.tsx')

diff --git a/src/client/DocServer.ts b/src/client/DocServer.ts
index 87a87be92..5af89cf49 100644
--- a/src/client/DocServer.ts
+++ b/src/client/DocServer.ts
@@ -1,6 +1,6 @@
 import * as OpenSocket from 'socket.io-client';
 import { MessageStore, Diff, YoutubeQueryTypes } from "./../server/Message";
-import { Opt } from '../new_fields/Doc';
+import { Opt, Doc } from '../new_fields/Doc';
 import { Utils, emptyFunction } from '../Utils';
 import { SerializationHelper } from './util/SerializationHelper';
 import { RefField } from '../new_fields/RefField';
@@ -26,6 +26,38 @@ export namespace DocServer {
     let GUID: string;
     // indicates whether or not a document is currently being udpated, and, if so, its id
 
+    export enum WriteMode {
+        Always = 0,
+        None = 1,
+        SameUser = 2,
+    }
+
+    const fieldWriteModes: { [field: string]: WriteMode } = {};
+    const docsWithUpdates: { [field: string]: Doc[] } = {};
+
+    export function setFieldWriteMode(field: string, writeMode: WriteMode) {
+        fieldWriteModes[field] = writeMode;
+        if (writeMode === WriteMode.Always) {
+            const docs = docsWithUpdates[field];
+            if (docs) {
+                docs.forEach(doc => Doc.RunCachedUpdate(doc, field));
+                delete docsWithUpdates[field];
+            }
+        }
+    }
+
+    export function getFieldWriteMode(field: string) {
+        return fieldWriteModes[field];
+    }
+
+    export function registerDocWithCachedUpdate(doc: Doc, field: string) {
+        let list = docsWithUpdates[field];
+        if (!list) {
+            list = docsWithUpdates[field] = [];
+        }
+        list.push(doc);
+    }
+
     export function init(protocol: string, hostname: string, port: number, identifier: string) {
         _cache = {};
         GUID = identifier;
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 53f589684..7bc31e961 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -422,6 +422,14 @@ export class MainView extends React.Component {
                             </button>
                         </div></li>)}
                     <li key="undoTest"><button className="add-button round-button" title="Click if undo isn't working" onClick={() => UndoManager.TraceOpenBatches()}><FontAwesomeIcon icon="exclamation" size="sm" /></button></li>
+                    <li key="test"><button className="add-button round-button" title="asdf" onClick={(() => {
+                        let state = DocServer.WriteMode.Always;
+                        return () => {
+                            state++;
+                            state = state % 3;
+                            DocServer.setFieldWriteMode("x", state);
+                        };
+                    })()}><FontAwesomeIcon icon="exclamation" size="sm" /></button></li>
                     <li key="color"><button className="add-button round-button" title="Select Color" style={{ zIndex: 1000 }} onClick={() => this.toggleColorPicker()}><div className="toolbar-color-button" style={{ backgroundColor: InkingControl.Instance.selectedColor }} >
                         <div className="toolbar-color-picker" onClick={this.onColorClick} style={this._colorPickerDisplay ? { color: "black", display: "block" } : { color: "black", display: "none" }}>
                             <SketchPicker color={InkingControl.Instance.selectedColor} onChange={InkingControl.Instance.switchColor} />
diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx
index 162ac1d98..c8749b7cd 100644
--- a/src/client/views/nodes/WebBox.tsx
+++ b/src/client/views/nodes/WebBox.tsx
@@ -8,6 +8,7 @@ import "./WebBox.scss";
 import React = require("react");
 import { InkTool } from "../../../new_fields/InkField";
 import { Cast, FieldValue, NumCast } from "../../../new_fields/Types";
+import { Utils } from "../../../Utils";
 
 @observer
 export class WebBox extends React.Component<FieldViewProps> {
@@ -52,7 +53,7 @@ export class WebBox extends React.Component<FieldViewProps> {
         if (field instanceof HtmlField) {
             view = <span id="webBox-htmlSpan" dangerouslySetInnerHTML={{ __html: field.html }} />;
         } else if (field instanceof WebField) {
-            view = <iframe src={field.url.href} style={{ position: "absolute", width: "100%", height: "100%" }} />;
+            view = <iframe src={Utils.CorsProxy(field.url.href)} style={{ position: "absolute", width: "100%", height: "100%" }} />;
         } else {
             view = <iframe src={"https://crossorigin.me/https://cs.brown.edu"} style={{ position: "absolute", width: "100%", height: "100%" }} />;
         }
diff --git a/src/new_fields/Doc.ts b/src/new_fields/Doc.ts
index c01f4e8cf..ebe3a5ba8 100644
--- a/src/new_fields/Doc.ts
+++ b/src/new_fields/Doc.ts
@@ -68,6 +68,7 @@ export function DocListCast(field: FieldResult): Doc[] {
 
 export const WidthSym = Symbol("Width");
 export const HeightSym = Symbol("Height");
+const CachedUpdates = Symbol("Cached updates");
 
 function fetchProto(doc: Doc) {
     const proto = doc.proto;
@@ -147,6 +148,8 @@ export class Doc extends RefField {
         return "invalid";
     }
 
+    private [CachedUpdates]: { [key: string]: () => Promise<any> } = {};
+
     public async [HandleUpdate](diff: any) {
         const set = diff.$set;
         if (set) {
@@ -154,11 +157,18 @@ export class Doc extends RefField {
                 if (!key.startsWith("fields.")) {
                     continue;
                 }
-                const value = await SerializationHelper.Deserialize(set[key]);
                 const fKey = key.substring(7);
-                updatingFromServer = true;
-                this[fKey] = value;
-                updatingFromServer = false;
+                const fn = async () => {
+                    const value = await SerializationHelper.Deserialize(set[key]);
+                    updatingFromServer = true;
+                    this[fKey] = value;
+                    updatingFromServer = false;
+                };
+                if (DocServer.getFieldWriteMode(fKey)) {
+                    this[CachedUpdates][fKey] = fn;
+                } else {
+                    await fn();
+                }
             }
         }
         const unset = diff.$unset;
@@ -168,9 +178,16 @@ export class Doc extends RefField {
                     continue;
                 }
                 const fKey = key.substring(7);
-                updatingFromServer = true;
-                delete this[fKey];
-                updatingFromServer = false;
+                const fn = async () => {
+                    updatingFromServer = true;
+                    delete this[fKey];
+                    updatingFromServer = false;
+                };
+                if (DocServer.getFieldWriteMode(fKey)) {
+                    this[CachedUpdates][fKey] = fn;
+                } else {
+                    await fn();
+                }
             }
         }
     }
@@ -187,6 +204,13 @@ export namespace Doc {
     //         return Cast(field, ctor);
     //     });
     // }
+    export function RunCachedUpdate(doc: Doc, field: string) {
+        const update = doc[CachedUpdates][field];
+        if (update) {
+            update();
+            delete doc[CachedUpdates][field];
+        }
+    }
     export function MakeReadOnly(): { end(): void } {
         makeReadOnly();
         return {
diff --git a/src/new_fields/util.ts b/src/new_fields/util.ts
index c6f693f7f..245b844fb 100644
--- a/src/new_fields/util.ts
+++ b/src/new_fields/util.ts
@@ -7,6 +7,8 @@ import { ObjectField } from "./ObjectField";
 import { action } from "mobx";
 import { Parent, OnUpdate, Update, Id, SelfProxy, Self } from "./FieldSymbols";
 import { ComputedField } from "./ScriptField";
+import { DocServer } from "../client/DocServer";
+import { CurrentUserUtils } from "../server/authentication/models/current_user_utils";
 
 function _readOnlySetter(): never {
     throw new Error("Documents can't be modified in read-only mode");
@@ -63,9 +65,14 @@ const _setterImpl = action(function (target: any, prop: string | symbol | number
     } else {
         target.__fields[prop] = value;
     }
-    if (value === undefined) target[Update]({ '$unset': { ["fields." + prop]: "" } });
+    const writeMode = DocServer.getFieldWriteMode(prop as string);
     if (typeof value === "object" && !(value instanceof ObjectField)) debugger;
-    else target[Update]({ '$set': { ["fields." + prop]: value instanceof ObjectField ? SerializationHelper.Serialize(value) : (value === undefined ? null : value) } });
+    if (!writeMode || (writeMode === DocServer.WriteMode.SameUser && receiver.author === CurrentUserUtils.email)) {
+        if (value === undefined) target[Update]({ '$unset': { ["fields." + prop]: "" } });
+        else target[Update]({ '$set': { ["fields." + prop]: value instanceof ObjectField ? SerializationHelper.Serialize(value) : (value === undefined ? null : value) } });
+    } else {
+        DocServer.registerDocWithCachedUpdate(receiver, prop as string);
+    }
     UndoManager.AddEvent({
         redo: () => receiver[prop] = value,
         undo: () => receiver[prop] = curValue
diff --git a/src/server/GarbageCollector.ts b/src/server/GarbageCollector.ts
index ea5388004..09b52eadf 100644
--- a/src/server/GarbageCollector.ts
+++ b/src/server/GarbageCollector.ts
@@ -13,7 +13,7 @@ function addDoc(doc: any, ids: string[], files: { [name: string]: string[] }) {
         if (field === undefined || field === null) {
             continue;
         }
-        if (field.__type === "proxy") {
+        if (field.__type === "proxy" || field.__type === "prefetch_proxy") {
             ids.push(field.fieldId);
         } else if (field.__type === "list") {
             addDoc(field.fields, ids, files);
-- 
cgit v1.2.3-70-g09d2


From 461d40170a0d1473ba410bec7ee75257f3ca2da9 Mon Sep 17 00:00:00 2001
From: Tyler Schicke <tyler_schicke@brown.edu>
Date: Wed, 7 Aug 2019 21:37:56 -0400
Subject: from last (to please Sam)

---
 src/client/views/MainView.tsx | 1 +
 1 file changed, 1 insertion(+)

(limited to 'src/client/views/MainView.tsx')

diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 7bc31e961..a698344f2 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -428,6 +428,7 @@ export class MainView extends React.Component {
                             state++;
                             state = state % 3;
                             DocServer.setFieldWriteMode("x", state);
+                            DocServer.setFieldWriteMode("y", state);
                         };
                     })()}><FontAwesomeIcon icon="exclamation" size="sm" /></button></li>
                     <li key="color"><button className="add-button round-button" title="Select Color" style={{ zIndex: 1000 }} onClick={() => this.toggleColorPicker()}><div className="toolbar-color-button" style={{ backgroundColor: InkingControl.Instance.selectedColor }} >
-- 
cgit v1.2.3-70-g09d2

