From 7ea6b44b10e1bf23287ba33e0081cacbfc595780 Mon Sep 17 00:00:00 2001
From: Sam Wilkins <samwilkins333@gmail.com>
Date: Mon, 19 Aug 2019 14:58:02 -0400
Subject: seemed to fix syncing with remote google doc

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

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

diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 6616d5d58..963722fe3 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -1,5 +1,6 @@
 import { library } from '@fortawesome/fontawesome-svg-core';
 import { faLink, faTag } from '@fortawesome/free-solid-svg-icons';
+import * as fa from '@fortawesome/free-brands-svg-icons';
 import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
 import { action, computed, observable, reaction, runInAction } from "mobx";
 import { observer } from "mobx-react";
@@ -36,6 +37,7 @@ export const Flyout = higflyout.default;
 
 library.add(faLink);
 library.add(faTag);
+library.add(fa.faGoogleDrive as any);
 
 @observer
 export class DocumentDecorations extends React.Component<{}, { value: string }> {
@@ -618,6 +620,17 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
         );
     }
 
+    considerGoogleDoc = () => {
+        let thisDoc = SelectionManager.SelectedDocuments()[0].props.Document;
+        let canEmbed = thisDoc.data && thisDoc.data instanceof RichTextField;
+        if (!canEmbed) return (null);
+        return (
+            <div className={"linkButtonWrapper"}>
+                <FontAwesomeIcon className="documentdecorations-icon" icon="image" size="sm" />
+            </div>
+        );
+    }
+
     considerTooltip = () => {
         let thisDoc = SelectionManager.SelectedDocuments()[0].props.Document;
         let isTextDoc = thisDoc.data && thisDoc.data instanceof RichTextField;
@@ -768,6 +781,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
                     </div>
                     {this.metadataMenu}
                     {this.considerEmbed()}
+                    {this.considerGoogleDoc()}
                     {/* {this.considerTooltip()} */}
                 </div>
             </div >
-- 
cgit v1.2.3-70-g09d2


From df11d1afdb271b37618ab9e1d362b2f4a1145982 Mon Sep 17 00:00:00 2001
From: Sam Wilkins <samwilkins333@gmail.com>
Date: Mon, 19 Aug 2019 17:48:57 -0400
Subject: cleanup, pull push

---
 .../apis/google_docs/GoogleApiClientUtils.ts       | 27 +++++---
 src/client/views/DocumentDecorations.tsx           | 32 +++++++---
 src/client/views/collections/CollectionSubView.tsx | 14 +++-
 src/client/views/nodes/FormattedTextBox.tsx        | 74 +++++++++++++---------
 4 files changed, 98 insertions(+), 49 deletions(-)

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

diff --git a/src/client/apis/google_docs/GoogleApiClientUtils.ts b/src/client/apis/google_docs/GoogleApiClientUtils.ts
index 9d78b632d..55b4a76f8 100644
--- a/src/client/apis/google_docs/GoogleApiClientUtils.ts
+++ b/src/client/apis/google_docs/GoogleApiClientUtils.ts
@@ -27,8 +27,8 @@ export namespace GoogleApiClientUtils {
         export type CreationResult = Opt<DocumentId>;
         export type RetrievalResult = Opt<docs_v1.Schema$Document>;
         export type UpdateResult = Opt<docs_v1.Schema$BatchUpdateDocumentResponse>;
-        export type ReadLinesResult = Opt<string[]>;
-        export type ReadResult = Opt<string>;
+        export type ReadLinesResult = Opt<{ title?: string, bodyLines?: string[] }>;
+        export type ReadResult = { title?: string, body?: string };
 
         export interface CreateOptions {
             handler: IdHandler; // callback to process the documentId of the newly created Google Doc
@@ -148,18 +148,27 @@ export namespace GoogleApiClientUtils {
         };
 
         export const read = async (options: ReadOptions): Promise<ReadResult> => {
-            return retrieve(options).then(schema => {
-                return schema ? Utils.extractText(schema, options.removeNewlines) : undefined;
+            return retrieve(options).then(document => {
+                let result: ReadResult = {};
+                if (document) {
+                    let title = document.title;
+                    let body = Utils.extractText(document, options.removeNewlines);
+                    result = { title, body };
+                }
+                return result;
             });
         };
 
         export const readLines = async (options: ReadOptions): Promise<ReadLinesResult> => {
-            return retrieve(options).then(schema => {
-                if (!schema) {
-                    return undefined;
+            return retrieve(options).then(document => {
+                let result: ReadLinesResult = {};
+                if (document) {
+                    let title = document.title;
+                    let bodyLines = Utils.extractText(document).split("\n");
+                    options.removeNewlines && (bodyLines = bodyLines.filter(line => line.length));
+                    result = { title, bodyLines };
                 }
-                const lines = Utils.extractText(schema).split("\n");
-                return options.removeNewlines ? lines.filter(line => line.length) : lines;
+                return result;
             });
         };
 
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 963722fe3..80d4ecb9b 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -1,6 +1,5 @@
 import { library } from '@fortawesome/fontawesome-svg-core';
-import { faLink, faTag } from '@fortawesome/free-solid-svg-icons';
-import * as fa from '@fortawesome/free-brands-svg-icons';
+import { faLink, faTag, faArrowAltCircleDown, faArrowAltCircleUp } from '@fortawesome/free-solid-svg-icons';
 import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
 import { action, computed, observable, reaction, runInAction } from "mobx";
 import { observer } from "mobx-react";
@@ -37,7 +36,8 @@ export const Flyout = higflyout.default;
 
 library.add(faLink);
 library.add(faTag);
-library.add(fa.faGoogleDrive as any);
+library.add(faArrowAltCircleDown);
+library.add(faArrowAltCircleUp);
 
 @observer
 export class DocumentDecorations extends React.Component<{}, { value: string }> {
@@ -620,13 +620,28 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
         );
     }
 
-    considerGoogleDoc = () => {
+    considerGoogleDocsPush = () => {
         let thisDoc = SelectionManager.SelectedDocuments()[0].props.Document;
-        let canEmbed = thisDoc.data && thisDoc.data instanceof RichTextField;
-        if (!canEmbed) return (null);
+        let canPush = thisDoc.data && thisDoc.data instanceof RichTextField;
+        if (!canPush) return (null);
+        return (
+            <div className={"linkButtonWrapper"}>
+                <div title="Push to Google Docs" className="linkButton-linker" onClick={() => thisDoc.pushToGoogleDocsTrigger = !thisDoc.pushToGoogleDocsTrigger}>
+                    <FontAwesomeIcon className="documentdecorations-icon" icon="arrow-alt-circle-up" size="sm" />
+                </div>
+            </div>
+        );
+    }
+
+    considerGoogleDocsPull = () => {
+        let thisDoc = SelectionManager.SelectedDocuments()[0].props.Document;
+        let canPull = thisDoc.data && thisDoc.data instanceof RichTextField;
+        if (!canPull) return (null);
         return (
             <div className={"linkButtonWrapper"}>
-                <FontAwesomeIcon className="documentdecorations-icon" icon="image" size="sm" />
+                <div title="Pull From Google Docs" className="linkButton-linker" onClick={() => thisDoc.pullFromGoogleDocsTrigger = !thisDoc.pullFromGoogleDocsTrigger}>
+                    <FontAwesomeIcon className="documentdecorations-icon" icon="arrow-alt-circle-down" size="sm" />
+                </div>
             </div>
         );
     }
@@ -781,7 +796,8 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
                     </div>
                     {this.metadataMenu}
                     {this.considerEmbed()}
-                    {this.considerGoogleDoc()}
+                    {this.considerGoogleDocsPush()}
+                    {this.considerGoogleDocsPull()}
                     {/* {this.considerTooltip()} */}
                 </div>
             </div >
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index 077f3f941..70c0632d1 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -16,7 +16,7 @@ import { DragManager } from "../../util/DragManager";
 import { undoBatch, UndoManager } from "../../util/UndoManager";
 import { DocComponent } from "../DocComponent";
 import { FieldViewProps } from "../nodes/FieldView";
-import { FormattedTextBox } from "../nodes/FormattedTextBox";
+import { FormattedTextBox, Blank } from "../nodes/FormattedTextBox";
 import { CollectionPDFView } from "./CollectionPDFView";
 import { CollectionVideoView } from "./CollectionVideoView";
 import { CollectionView } from "./CollectionView";
@@ -206,7 +206,17 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
                 this.props.addDocument(Docs.Create.VideoDocument(url, { ...options, title: url, width: 400, height: 315, nativeWidth: 600, nativeHeight: 472.5 }));
                 return;
             }
-
+            let matches: RegExpExecArray | null;
+            if ((matches = /(https:\/\/)?docs\.google\.com\/document\/d\/([^\\]+)\/edit/g.exec(text)) !== null) {
+                let newBox = Docs.Create.TextDocument({ ...options, width: 600, height: 400, title: "Fetching title from Google Docs..." });
+                let proto = newBox.proto!;
+                proto.autoHeight = true;
+                proto.googleDocId = matches[2];
+                proto.data = "Fetching contents from Google Docs...";
+                proto.backgroundColor = "#eeeeff";
+                this.props.addDocument(newBox);
+                return;
+            }
             let batch = UndoManager.StartBatch("collection view drop");
             let promises: Promise<void>[] = [];
             // tslint:disable-next-line:prefer-for-of
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index bc057bb5f..406c2c4a6 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -1,6 +1,6 @@
 import { library } from '@fortawesome/fontawesome-svg-core';
 import { faEdit, faSmile, faTextHeight, faUpload } from '@fortawesome/free-solid-svg-icons';
-import { action, IReactionDisposer, observable, reaction, runInAction, computed, Lambda, trace } from "mobx";
+import { action, IReactionDisposer, observable, reaction, runInAction, computed, Lambda, trace, untracked } from "mobx";
 import { observer } from "mobx-react";
 import { baseKeymap } from "prosemirror-commands";
 import { history } from "prosemirror-history";
@@ -47,6 +47,8 @@ library.add(faSmile, faTextHeight, faUpload);
 // FormattedTextBox: Displays an editable plain text node that maps to a specified Key of a Document
 //
 
+export const Blank = `{"doc":{"type":"doc","content":[]},"selection":{"type":"text","anchor":0,"head":0}}`;
+
 export interface FormattedTextBoxProps {
     isOverlay?: boolean;
     hideOnLeave?: boolean;
@@ -84,7 +86,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
     private _proxyReactionDisposer: Opt<IReactionDisposer>;
     private dropDisposer?: DragManager.DragDropDisposer;
     public get CurrentDiv(): HTMLDivElement { return this._ref.current!; }
-    private isOpening = false;
+    private isGoogleDocsUpdate = false;
     @observable _entered = false;
 
     @observable public static InputBoxOverlay?: FormattedTextBox = undefined;
@@ -290,28 +292,6 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
         }
     }
 
-    componentWillMount() {
-        this.pollExportedCounterpart();
-    }
-
-    pollExportedCounterpart = async () => {
-        let dataDoc = Doc.GetProto(this.props.Document);
-        let documentId = StrCast(dataDoc[googleDocId]);
-        if (documentId) {
-            let exportState = await GoogleApiClientUtils.Docs.read({ documentId });
-            if (exportState) {
-                let data = Cast(dataDoc.data, RichTextField);
-                if (data) {
-                    this.isOpening = true;
-                    dataDoc.data = new RichTextField(data[FromGoogleDocText](exportState));
-                }
-            } else {
-                delete dataDoc[googleDocId];
-            }
-            this.tryUpdateHeight();
-        }
-    }
-
     componentDidMount() {
         const config = {
             schema,
@@ -347,23 +327,32 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
         this._reactionDisposer = reaction(
             () => {
                 const field = this.dataDoc ? Cast(this.dataDoc[this.props.fieldKey], RichTextField) : undefined;
-                return field ? field.Data : `{"doc":{"type":"doc","content":[]},"selection":{"type":"text","anchor":0,"head":0}}`;
+                return field ? field.Data : Blank;
             },
             incomingValue => {
                 if (this._editorView && !this._applyingChange) {
                     let updatedState = JSON.parse(incomingValue);
                     this._editorView.updateState(EditorState.fromJSON(config, updatedState));
                     // manually sets cursor selection at the end of the text on focus
-                    if (this.isOpening) {
-                        this.isOpening = false;
+                    if (this.isGoogleDocsUpdate) {
+                        this.isGoogleDocsUpdate = false;
                         let end = this._editorView.state.doc.content.size - 1;
                         updatedState.selection = { type: "text", anchor: end, head: end };
                         this._editorView.updateState(EditorState.fromJSON(config, updatedState));
                     }
+                    this.tryUpdateHeight();
                 }
             }
         );
 
+        reaction(() => this.props.Document.pullFromGoogleDocsTrigger, () => {
+            this.pullFromGoogleDoc();
+        });
+
+        reaction(() => this.props.Document.pushToGoogleDocsTrigger, () => {
+            this.pushToGoogleDoc();
+        });
+
         this._textReactionDisposer = reaction(
             () => this.extensionDoc,
             () => {
@@ -391,6 +380,13 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
                 this.unhighlightSearchTerms();
             }
         }, { fireImmediately: true });
+
+        ["pushToGoogleDocsTrigger", "pullFromGoogleDocsTrigger"].map(key => {
+            let doc = this.props.Document;
+            if (doc[key] === undefined) {
+                untracked(() => doc[key] = false);
+            }
+        });
     }
 
     clipboardTextSerializer = (slice: Slice): string => {
@@ -644,7 +640,6 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
             this._undoTyping.end();
             this._undoTyping = undefined;
         }
-        this.updateGoogleDoc();
     }
     public _undoTyping?: UndoManager.Batch;
     onKeyPress = (e: React.KeyboardEvent) => {
@@ -701,13 +696,13 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
         if (!(googleDocId in Doc.GetProto(this.props.Document))) {
             ContextMenu.Instance.addItem({
                 description: "Export to Google Doc...",
-                event: this.updateGoogleDoc,
+                event: this.pushToGoogleDoc,
                 icon: "upload"
             });
         }
     }
 
-    updateGoogleDoc = () => {
+    pushToGoogleDoc = () => {
         let modes = GoogleApiClientUtils.Docs.WriteMode;
         let mode = modes.Replace;
         let reference: Opt<GoogleApiClientUtils.Docs.Reference> = Cast(this.dataDoc[googleDocId], "string");
@@ -725,6 +720,25 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
         }
     }
 
+    pullFromGoogleDoc = async () => {
+        let dataDoc = Doc.GetProto(this.props.Document);
+        let documentId = StrCast(dataDoc[googleDocId]);
+        if (documentId) {
+            let exportState = await GoogleApiClientUtils.Docs.read({ documentId });
+            if (exportState && exportState.body && exportState.title) {
+                let data = Cast(dataDoc.data, RichTextField);
+                if (data) {
+                    this.isGoogleDocsUpdate = true;
+                    dataDoc.data = new RichTextField(data[FromGoogleDocText](exportState.body));
+                    dataDoc.title = exportState.title;
+                }
+            } else {
+                delete dataDoc[googleDocId];
+            }
+        }
+    }
+
+
     render() {
         let self = this;
         let style = this.props.isOverlay ? "scroll" : "hidden";
-- 
cgit v1.2.3-70-g09d2


From 00cecb10d80bad8d717cd06ddc4bf156664b9f2d Mon Sep 17 00:00:00 2001
From: Sam Wilkins <samwilkins333@gmail.com>
Date: Mon, 19 Aug 2019 22:39:56 -0400
Subject: fixed push pull UI

---
 .../apis/google_docs/GoogleApiClientUtils.ts       |   3 +
 src/client/views/DocumentDecorations.tsx           |  14 ++-
 src/client/views/nodes/FormattedTextBox.tsx        | 122 +++++++++++----------
 src/new_fields/RichTextField.ts                    |   8 +-
 4 files changed, 81 insertions(+), 66 deletions(-)

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

diff --git a/src/client/apis/google_docs/GoogleApiClientUtils.ts b/src/client/apis/google_docs/GoogleApiClientUtils.ts
index 55b4a76f8..821c52270 100644
--- a/src/client/apis/google_docs/GoogleApiClientUtils.ts
+++ b/src/client/apis/google_docs/GoogleApiClientUtils.ts
@@ -4,6 +4,9 @@ import { RouteStore } from "../../../server/RouteStore";
 import { Opt } from "../../../new_fields/Doc";
 import { isArray } from "util";
 
+export const Pulls = "googleDocsPullCount";
+export const Pushes = "googleDocsPushCount";
+
 export namespace GoogleApiClientUtils {
 
     export namespace Docs {
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 80d4ecb9b..797b43add 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -30,6 +30,7 @@ import { ObjectField } from '../../new_fields/ObjectField';
 import { MetadataEntryMenu } from './MetadataEntryMenu';
 import { ImageBox } from './nodes/ImageBox';
 import { CurrentUserUtils } from '../../server/authentication/models/current_user_utils';
+import { Pulls, Pushes } from '../apis/google_docs/GoogleApiClientUtils';
 const higflyout = require("@hig/flyout");
 export const { anchorPoints } = higflyout;
 export const Flyout = higflyout.default;
@@ -626,7 +627,10 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
         if (!canPush) return (null);
         return (
             <div className={"linkButtonWrapper"}>
-                <div title="Push to Google Docs" className="linkButton-linker" onClick={() => thisDoc.pushToGoogleDocsTrigger = !thisDoc.pushToGoogleDocsTrigger}>
+                <div title="Push to Google Docs" className="linkButton-linker" onClick={() => {
+                    DocumentDecorations.hasPushedHack = false;
+                    thisDoc[Pushes] = NumCast(thisDoc[Pushes]) + 1;
+                }}>
                     <FontAwesomeIcon className="documentdecorations-icon" icon="arrow-alt-circle-up" size="sm" />
                 </div>
             </div>
@@ -639,13 +643,19 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
         if (!canPull) return (null);
         return (
             <div className={"linkButtonWrapper"}>
-                <div title="Pull From Google Docs" className="linkButton-linker" onClick={() => thisDoc.pullFromGoogleDocsTrigger = !thisDoc.pullFromGoogleDocsTrigger}>
+                <div title="Pull From Google Docs" className="linkButton-linker" onClick={() => {
+                    DocumentDecorations.hasPulledHack = false;
+                    thisDoc[Pulls] = NumCast(thisDoc[Pulls]) + 1;
+                }}>
                     <FontAwesomeIcon className="documentdecorations-icon" icon="arrow-alt-circle-down" size="sm" />
                 </div>
             </div>
         );
     }
 
+    public static hasPushedHack = false;
+    public static hasPulledHack = false;
+
     considerTooltip = () => {
         let thisDoc = SelectionManager.SelectedDocuments()[0].props.Document;
         let isTextDoc = thisDoc.data && thisDoc.data instanceof RichTextField;
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index 406c2c4a6..d2eb71350 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -12,7 +12,7 @@ import { EditorView } from "prosemirror-view";
 import { Doc, Opt, DocListCast } from "../../../new_fields/Doc";
 import { Id, Copy } from '../../../new_fields/FieldSymbols';
 import { List } from '../../../new_fields/List';
-import { RichTextField, ToGoogleDocText, FromGoogleDocText } from "../../../new_fields/RichTextField";
+import { RichTextField, ToPlainText, FromPlainText } from "../../../new_fields/RichTextField";
 import { createSchema, listSpec, makeInterface } from "../../../new_fields/Schema";
 import { BoolCast, Cast, NumCast, StrCast, DateCast } from "../../../new_fields/Types";
 import { DocServer } from "../../DocServer";
@@ -30,16 +30,14 @@ import { ContextMenu } from "../../views/ContextMenu";
 import { ContextMenuProps } from '../ContextMenuItem';
 import { DocComponent } from "../DocComponent";
 import { InkingControl } from "../InkingControl";
-import { Templates } from '../Templates';
 import { FieldView, FieldViewProps } from "./FieldView";
 import "./FormattedTextBox.scss";
 import React = require("react");
-import { For } from 'babel-types';
 import { DateField } from '../../../new_fields/DateField';
 import { Utils } from '../../../Utils';
 import { MainOverlayTextBox } from '../MainOverlayTextBox';
-import { GoogleApiClientUtils } from '../../apis/google_docs/GoogleApiClientUtils';
-import * as diff from "diff";
+import { GoogleApiClientUtils, Pulls, Pushes } from '../../apis/google_docs/GoogleApiClientUtils';
+import { DocumentDecorations } from '../DocumentDecorations';
 
 library.add(faEdit);
 library.add(faSmile, faTextHeight, faUpload);
@@ -84,6 +82,8 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
     private _searchReactionDisposer?: Lambda;
     private _textReactionDisposer: Opt<IReactionDisposer>;
     private _proxyReactionDisposer: Opt<IReactionDisposer>;
+    private pullReactionDisposer: Opt<IReactionDisposer>;
+    private pushReactionDisposer: Opt<IReactionDisposer>;
     private dropDisposer?: DragManager.DragDropDisposer;
     public get CurrentDiv(): HTMLDivElement { return this._ref.current!; }
     private isGoogleDocsUpdate = false;
@@ -345,13 +345,25 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
             }
         );
 
-        reaction(() => this.props.Document.pullFromGoogleDocsTrigger, () => {
-            this.pullFromGoogleDoc();
-        });
+        this.pullReactionDisposer = reaction(
+            () => this.props.Document[Pulls],
+            () => {
+                if (!DocumentDecorations.hasPulledHack) {
+                    DocumentDecorations.hasPulledHack = true;
+                    this.pullFromGoogleDoc();
+                }
+            }
+        );
 
-        reaction(() => this.props.Document.pushToGoogleDocsTrigger, () => {
-            this.pushToGoogleDoc();
-        });
+        this.pushReactionDisposer = reaction(
+            () => this.props.Document[Pushes],
+            () => {
+                if (!DocumentDecorations.hasPushedHack) {
+                    DocumentDecorations.hasPushedHack = true;
+                    this.pushToGoogleDoc();
+                }
+            }
+        );
 
         this._textReactionDisposer = reaction(
             () => this.extensionDoc,
@@ -380,13 +392,44 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
                 this.unhighlightSearchTerms();
             }
         }, { fireImmediately: true });
+    }
 
-        ["pushToGoogleDocsTrigger", "pullFromGoogleDocsTrigger"].map(key => {
-            let doc = this.props.Document;
-            if (doc[key] === undefined) {
-                untracked(() => doc[key] = false);
-            }
-        });
+    pushToGoogleDoc = () => {
+        let modes = GoogleApiClientUtils.Docs.WriteMode;
+        let mode = modes.Replace;
+        let reference: Opt<GoogleApiClientUtils.Docs.Reference> = Cast(this.dataDoc[googleDocId], "string");
+        if (!reference) {
+            mode = modes.Insert;
+            reference = {
+                title: StrCast(this.dataDoc.title),
+                handler: id => this.dataDoc[googleDocId] = id
+            };
+        }
+        if (this._editorView) {
+            let data = Cast(this.dataDoc.data, RichTextField);
+            let content = data ? data[ToPlainText]() : this._editorView.state.doc.textContent;
+            GoogleApiClientUtils.Docs.write({ reference, content, mode });
+        }
+    }
+
+    pullFromGoogleDoc = async () => {
+        let dataDoc = Doc.GetProto(this.props.Document);
+        let documentId = StrCast(dataDoc[googleDocId]);
+        if (documentId) {
+            let exportState = await GoogleApiClientUtils.Docs.read({ documentId });
+            UndoManager.RunInBatch(() => {
+                if (exportState && exportState.body && exportState.title) {
+                    let data = Cast(dataDoc.data, RichTextField);
+                    if (data) {
+                        this.isGoogleDocsUpdate = true;
+                        dataDoc.data = new RichTextField(data[FromPlainText](exportState.body));
+                        dataDoc.title = exportState.title;
+                    }
+                } else {
+                    delete dataDoc[googleDocId];
+                }
+            }, Pulls);
+        }
     }
 
     clipboardTextSerializer = (slice: Slice): string => {
@@ -516,6 +559,8 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
         this._reactionDisposer && this._reactionDisposer();
         this._proxyReactionDisposer && this._proxyReactionDisposer();
         this._textReactionDisposer && this._textReactionDisposer();
+        this.pushReactionDisposer && this.pushReactionDisposer();
+        this.pullReactionDisposer && this.pullReactionDisposer();
     }
 
     onPointerDown = (e: React.PointerEvent): void => {
@@ -693,49 +738,6 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
             event: action(() => Doc.GetProto(this.props.Document).autoHeight = !BoolCast(this.props.Document.autoHeight)), icon: "expand-arrows-alt"
         });
         ContextMenu.Instance.addItem({ description: "Text Funcs...", subitems: subitems, icon: "text-height" });
-        if (!(googleDocId in Doc.GetProto(this.props.Document))) {
-            ContextMenu.Instance.addItem({
-                description: "Export to Google Doc...",
-                event: this.pushToGoogleDoc,
-                icon: "upload"
-            });
-        }
-    }
-
-    pushToGoogleDoc = () => {
-        let modes = GoogleApiClientUtils.Docs.WriteMode;
-        let mode = modes.Replace;
-        let reference: Opt<GoogleApiClientUtils.Docs.Reference> = Cast(this.dataDoc[googleDocId], "string");
-        if (!reference) {
-            mode = modes.Insert;
-            reference = {
-                title: StrCast(this.dataDoc.title),
-                handler: id => this.dataDoc[googleDocId] = id
-            };
-        }
-        if (this._editorView) {
-            let data = Cast(this.dataDoc.data, RichTextField);
-            let content = data ? data[ToGoogleDocText]() : this._editorView.state.doc.textContent;
-            GoogleApiClientUtils.Docs.write({ reference, content, mode });
-        }
-    }
-
-    pullFromGoogleDoc = async () => {
-        let dataDoc = Doc.GetProto(this.props.Document);
-        let documentId = StrCast(dataDoc[googleDocId]);
-        if (documentId) {
-            let exportState = await GoogleApiClientUtils.Docs.read({ documentId });
-            if (exportState && exportState.body && exportState.title) {
-                let data = Cast(dataDoc.data, RichTextField);
-                if (data) {
-                    this.isGoogleDocsUpdate = true;
-                    dataDoc.data = new RichTextField(data[FromGoogleDocText](exportState.body));
-                    dataDoc.title = exportState.title;
-                }
-            } else {
-                delete dataDoc[googleDocId];
-            }
-        }
     }
 
 
diff --git a/src/new_fields/RichTextField.ts b/src/new_fields/RichTextField.ts
index ec08293e9..ab58329f9 100644
--- a/src/new_fields/RichTextField.ts
+++ b/src/new_fields/RichTextField.ts
@@ -4,8 +4,8 @@ import { Deserializable } from "../client/util/SerializationHelper";
 import { Copy, ToScriptString } from "./FieldSymbols";
 import { scriptingGlobal } from "../client/util/Scripting";
 
-export const ToGoogleDocText = Symbol("PlainText");
-export const FromGoogleDocText = Symbol("PlainText");
+export const ToPlainText = Symbol("PlainText");
+export const FromPlainText = Symbol("PlainText");
 const delimiter = "\n";
 const joiner = "";
 
@@ -28,7 +28,7 @@ export class RichTextField extends ObjectField {
         return `new RichTextField("${this.Data}")`;
     }
 
-    [ToGoogleDocText]() {
+    [ToPlainText]() {
         // Because we're working with plain text, just concatenate all paragraphs
         let content = JSON.parse(this.Data).doc.content;
         let paragraphs = content.filter((item: any) => item.type === "paragraph");
@@ -43,7 +43,7 @@ export class RichTextField extends ObjectField {
         return textParagraphs.join(joiner).trimEnd(delimiter);
     }
 
-    [FromGoogleDocText](plainText: string) {
+    [FromPlainText](plainText: string) {
         // Remap the text, creating blocks split on newlines
         let elements = plainText.split(delimiter);
 
-- 
cgit v1.2.3-70-g09d2


From c92761206957aebdba1cc2e77f6b662ae42db847 Mon Sep 17 00:00:00 2001
From: Sam Wilkins <samwilkins333@gmail.com>
Date: Tue, 20 Aug 2019 01:44:18 -0400
Subject: interactive ui

---
 src/client/views/DocumentDecorations.tsx    | 84 +++++++++++++++++++++++------
 src/client/views/nodes/FormattedTextBox.tsx | 63 +++++++++++++++-------
 2 files changed, 112 insertions(+), 35 deletions(-)

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

diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 797b43add..1db452b45 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -1,5 +1,5 @@
-import { library } from '@fortawesome/fontawesome-svg-core';
-import { faLink, faTag, faArrowAltCircleDown, faArrowAltCircleUp } from '@fortawesome/free-solid-svg-icons';
+import { library, IconProp } from '@fortawesome/fontawesome-svg-core';
+import { faLink, faTag, faArrowAltCircleDown, faArrowAltCircleUp, faCheckCircle, faStopCircle, faCloudUploadAlt } from '@fortawesome/free-solid-svg-icons';
 import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
 import { action, computed, observable, reaction, runInAction } from "mobx";
 import { observer } from "mobx-react";
@@ -18,7 +18,7 @@ import { CollectionView } from "./collections/CollectionView";
 import './DocumentDecorations.scss';
 import { DocumentView, PositionDocument } from "./nodes/DocumentView";
 import { FieldView } from "./nodes/FieldView";
-import { FormattedTextBox } from "./nodes/FormattedTextBox";
+import { FormattedTextBox, GoogleRef } from "./nodes/FormattedTextBox";
 import { IconBox } from "./nodes/IconBox";
 import { LinkMenu } from "./nodes/LinkMenu";
 import { TemplateMenu } from "./TemplateMenu";
@@ -26,7 +26,6 @@ import { Template, Templates } from "./Templates";
 import React = require("react");
 import { RichTextField } from '../../new_fields/RichTextField';
 import { LinkManager } from '../util/LinkManager';
-import { ObjectField } from '../../new_fields/ObjectField';
 import { MetadataEntryMenu } from './MetadataEntryMenu';
 import { ImageBox } from './nodes/ImageBox';
 import { CurrentUserUtils } from '../../server/authentication/models/current_user_utils';
@@ -39,6 +38,11 @@ library.add(faLink);
 library.add(faTag);
 library.add(faArrowAltCircleDown);
 library.add(faArrowAltCircleUp);
+library.add(faStopCircle);
+library.add(faCheckCircle);
+library.add(faCloudUploadAlt);
+
+const cloud: IconProp = "cloud-upload-alt";
 
 @observer
 export class DocumentDecorations extends React.Component<{}, { value: string }> {
@@ -69,6 +73,52 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
     @observable private _removeIcon = false;
     @observable public Interacting = false;
 
+    @observable public pushIcon: IconProp = "arrow-alt-circle-up";
+    @observable public pullIcon: IconProp = "arrow-alt-circle-down";
+    @observable public pullColor: string = "white";
+    public pullColorAnimating = false;
+
+    private pullAnimating = false;
+    private pushAnimating = false;
+
+    public startPullOutcome = action((success: boolean) => {
+        if (this.pullAnimating) {
+            return;
+        }
+        this.pullAnimating = true;
+        this.pullIcon = success ? "check-circle" : "stop-circle";
+        setTimeout(() => runInAction(() => {
+            this.pullIcon = "arrow-alt-circle-down";
+            this.pullAnimating = false;
+        }), 1000);
+    });
+
+    public startPushOutcome = action((success: boolean) => {
+        if (this.pushAnimating) {
+            return;
+        }
+        this.pushAnimating = true;
+        this.pushIcon = success ? "check-circle" : "stop-circle";
+        setTimeout(() => runInAction(() => {
+            this.pushIcon = "arrow-alt-circle-up";
+            this.pushAnimating = false;
+        }), 1000);
+    });
+
+    public setPullState = (unchanged: boolean) => {
+        if (this.pullColorAnimating) {
+            return;
+        }
+        this.pullColorAnimating = true;
+        this.pullColor = unchanged ? "lawngreen" : "red";
+        setTimeout(() => {
+            runInAction(() => {
+                this.pullColor = "white";
+                this.pullColorAnimating = false;
+            });
+        }, 2000);
+    }
+
     constructor(props: Readonly<{}>) {
         super(props);
         DocumentDecorations.Instance = this;
@@ -621,33 +671,37 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
         );
     }
 
+    private get targetDoc() {
+        return SelectionManager.SelectedDocuments()[0].props.Document;
+    }
+
     considerGoogleDocsPush = () => {
-        let thisDoc = SelectionManager.SelectedDocuments()[0].props.Document;
-        let canPush = thisDoc.data && thisDoc.data instanceof RichTextField;
+        let canPush = this.targetDoc.data && this.targetDoc.data instanceof RichTextField;
         if (!canPush) return (null);
+        let published = Doc.GetProto(this.targetDoc)[GoogleRef] !== undefined;
+        let icon: IconProp = published ? (this.pushIcon as any) : (cloud as any);
         return (
             <div className={"linkButtonWrapper"}>
-                <div title="Push to Google Docs" className="linkButton-linker" onClick={() => {
+                <div title={`${published ? "Push" : "Publish"} to Google Docs`} className="linkButton-linker" onClick={() => {
                     DocumentDecorations.hasPushedHack = false;
-                    thisDoc[Pushes] = NumCast(thisDoc[Pushes]) + 1;
+                    this.targetDoc[Pushes] = NumCast(this.targetDoc[Pushes]) + 1;
                 }}>
-                    <FontAwesomeIcon className="documentdecorations-icon" icon="arrow-alt-circle-up" size="sm" />
+                    <FontAwesomeIcon className="documentdecorations-icon" icon={icon} size={published ? "sm" : "xs"} />
                 </div>
             </div>
         );
     }
 
     considerGoogleDocsPull = () => {
-        let thisDoc = SelectionManager.SelectedDocuments()[0].props.Document;
-        let canPull = thisDoc.data && thisDoc.data instanceof RichTextField;
-        if (!canPull) return (null);
+        let canPull = this.targetDoc.data && this.targetDoc.data instanceof RichTextField;
+        if (!canPull || !Doc.GetProto(this.targetDoc)[GoogleRef]) return (null);
         return (
             <div className={"linkButtonWrapper"}>
-                <div title="Pull From Google Docs" className="linkButton-linker" onClick={() => {
+                <div style={{ backgroundColor: this.pullColor, transition: "1s ease all" }} title="Pull From Google Docs" className="linkButton-linker" onClick={() => {
                     DocumentDecorations.hasPulledHack = false;
-                    thisDoc[Pulls] = NumCast(thisDoc[Pulls]) + 1;
+                    this.targetDoc[Pulls] = NumCast(this.targetDoc[Pulls]) + 1;
                 }}>
-                    <FontAwesomeIcon className="documentdecorations-icon" icon="arrow-alt-circle-down" size="sm" />
+                    <FontAwesomeIcon className="documentdecorations-icon" icon={this.pullIcon} size="sm" />
                 </div>
             </div>
         );
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index d2eb71350..33d222813 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -60,11 +60,13 @@ const richTextSchema = createSchema({
     documentText: "string"
 });
 
-const googleDocId = "googleDocId";
+export const GoogleRef = "googleDocId";
 
 type RichTextDocument = makeInterface<[typeof richTextSchema]>;
 const RichTextDocument = makeInterface(richTextSchema);
 
+type PullHandler = (exportState: GoogleApiClientUtils.Docs.ReadResult, dataDoc: Doc) => void;
+
 @observer
 export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTextBoxProps), RichTextDocument>(RichTextDocument) {
     public static LayoutString(fieldStr: string = "data") {
@@ -324,6 +326,8 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
                 }, { fireImmediately: true });
         }
 
+        this.pullFromGoogleDoc(this.checkState);
+
         this._reactionDisposer = reaction(
             () => {
                 const field = this.dataDoc ? Cast(this.dataDoc[this.props.fieldKey], RichTextField) : undefined;
@@ -350,7 +354,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
             () => {
                 if (!DocumentDecorations.hasPulledHack) {
                     DocumentDecorations.hasPulledHack = true;
-                    this.pullFromGoogleDoc();
+                    this.pullFromGoogleDoc(this.updateState);
                 }
             }
         );
@@ -394,44 +398,63 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
         }, { fireImmediately: true });
     }
 
-    pushToGoogleDoc = () => {
+    pushToGoogleDoc = async () => {
         let modes = GoogleApiClientUtils.Docs.WriteMode;
         let mode = modes.Replace;
-        let reference: Opt<GoogleApiClientUtils.Docs.Reference> = Cast(this.dataDoc[googleDocId], "string");
+        let reference: Opt<GoogleApiClientUtils.Docs.Reference> = Cast(this.dataDoc[GoogleRef], "string");
         if (!reference) {
             mode = modes.Insert;
             reference = {
                 title: StrCast(this.dataDoc.title),
-                handler: id => this.dataDoc[googleDocId] = id
+                handler: id => this.dataDoc[GoogleRef] = id
             };
         }
         if (this._editorView) {
             let data = Cast(this.dataDoc.data, RichTextField);
             let content = data ? data[ToPlainText]() : this._editorView.state.doc.textContent;
-            GoogleApiClientUtils.Docs.write({ reference, content, mode });
+            let response = await GoogleApiClientUtils.Docs.write({ reference, content, mode });
+            let pushSuccess = response !== undefined && !("errors" in response);
+            DocumentDecorations.Instance.startPushOutcome(pushSuccess);
         }
     }
 
-    pullFromGoogleDoc = async () => {
+    pullFromGoogleDoc = async (handler: PullHandler) => {
         let dataDoc = Doc.GetProto(this.props.Document);
-        let documentId = StrCast(dataDoc[googleDocId]);
+        let documentId = StrCast(dataDoc[GoogleRef]);
         if (documentId) {
             let exportState = await GoogleApiClientUtils.Docs.read({ documentId });
-            UndoManager.RunInBatch(() => {
-                if (exportState && exportState.body && exportState.title) {
-                    let data = Cast(dataDoc.data, RichTextField);
-                    if (data) {
-                        this.isGoogleDocsUpdate = true;
-                        dataDoc.data = new RichTextField(data[FromPlainText](exportState.body));
-                        dataDoc.title = exportState.title;
-                    }
-                } else {
-                    delete dataDoc[googleDocId];
-                }
-            }, Pulls);
+            UndoManager.RunInBatch(() => handler(exportState, dataDoc), Pulls);
         }
     }
 
+    updateState = (exportState: GoogleApiClientUtils.Docs.ReadResult, dataDoc: Doc) => {
+        let pullSuccess = false;
+        if (exportState !== undefined && exportState.body !== undefined && exportState.title !== undefined) {
+            let data = Cast(dataDoc.data, RichTextField);
+            if (data) {
+                pullSuccess = true;
+                this.isGoogleDocsUpdate = true;
+                dataDoc.data = new RichTextField(data[FromPlainText](exportState.body));
+                dataDoc.title = exportState.title;
+            }
+        } else {
+            delete dataDoc[GoogleRef];
+        }
+        DocumentDecorations.Instance.startPullOutcome(pullSuccess);
+    }
+
+    checkState = (exportState: GoogleApiClientUtils.Docs.ReadResult, dataDoc: Doc) => {
+        if (exportState !== undefined && exportState.body !== undefined && exportState.title !== undefined) {
+            let data = Cast(dataDoc.data, RichTextField);
+            if (data) {
+                let storedPlainText = data[ToPlainText]() + "\n";
+                let receivedPlainText = exportState.body;
+                DocumentDecorations.Instance.setPullState(storedPlainText === receivedPlainText);
+            }
+        }
+    }
+
+
     clipboardTextSerializer = (slice: Slice): string => {
         let text = "", separated = true;
         const from = 0, to = slice.content.size;
-- 
cgit v1.2.3-70-g09d2


From aa0e6f5ffc30fdffc3be13a1948981b754544a01 Mon Sep 17 00:00:00 2001
From: Sam Wilkins <samwilkins333@gmail.com>
Date: Tue, 20 Aug 2019 12:03:50 -0400
Subject: automation of pull push button visibility

---
 src/client/views/DocumentDecorations.tsx    |  3 +-
 src/client/views/nodes/FormattedTextBox.tsx | 61 +++++++++++++++++++----------
 2 files changed, 42 insertions(+), 22 deletions(-)

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

diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 1db452b45..05239073b 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -694,7 +694,8 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
 
     considerGoogleDocsPull = () => {
         let canPull = this.targetDoc.data && this.targetDoc.data instanceof RichTextField;
-        if (!canPull || !Doc.GetProto(this.targetDoc)[GoogleRef]) return (null);
+        let dataDoc = Doc.GetProto(this.targetDoc);
+        if (!canPull || !dataDoc[GoogleRef] || dataDoc.unchanged) return (null);
         return (
             <div className={"linkButtonWrapper"}>
                 <div style={{ backgroundColor: this.pullColor, transition: "1s ease all" }} title="Pull From Google Docs" className="linkButton-linker" onClick={() => {
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index 33d222813..8a355a425 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -1,6 +1,6 @@
 import { library } from '@fortawesome/fontawesome-svg-core';
 import { faEdit, faSmile, faTextHeight, faUpload } from '@fortawesome/free-solid-svg-icons';
-import { action, IReactionDisposer, observable, reaction, runInAction, computed, Lambda, trace, untracked } from "mobx";
+import { action, IReactionDisposer, observable, reaction, runInAction, computed, Lambda, trace, untracked, autorun } from "mobx";
 import { observer } from "mobx-react";
 import { baseKeymap } from "prosemirror-commands";
 import { history } from "prosemirror-history";
@@ -399,32 +399,46 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
     }
 
     pushToGoogleDoc = async () => {
-        let modes = GoogleApiClientUtils.Docs.WriteMode;
-        let mode = modes.Replace;
-        let reference: Opt<GoogleApiClientUtils.Docs.Reference> = Cast(this.dataDoc[GoogleRef], "string");
-        if (!reference) {
-            mode = modes.Insert;
-            reference = {
-                title: StrCast(this.dataDoc.title),
-                handler: id => this.dataDoc[GoogleRef] = id
+        this.pullFromGoogleDoc(async (exportState: GoogleApiClientUtils.Docs.ReadResult, dataDoc: Doc) => {
+            let modes = GoogleApiClientUtils.Docs.WriteMode;
+            let mode = modes.Replace;
+            let reference: Opt<GoogleApiClientUtils.Docs.Reference> = Cast(this.dataDoc[GoogleRef], "string");
+            if (!reference) {
+                mode = modes.Insert;
+                reference = {
+                    title: StrCast(this.dataDoc.title),
+                    handler: id => this.dataDoc[GoogleRef] = id
+                };
+            }
+            let redo = async () => {
+                if (this._editorView && reference) {
+                    let data = Cast(this.dataDoc.data, RichTextField);
+                    let content = data ? data[ToPlainText]() : this._editorView.state.doc.textContent;
+                    let response = await GoogleApiClientUtils.Docs.write({ reference, content, mode });
+                    let pushSuccess = response !== undefined && !("errors" in response);
+                    dataDoc.unchanged = pushSuccess;
+                    DocumentDecorations.Instance.startPushOutcome(pushSuccess);
+                }
             };
-        }
-        if (this._editorView) {
-            let data = Cast(this.dataDoc.data, RichTextField);
-            let content = data ? data[ToPlainText]() : this._editorView.state.doc.textContent;
-            let response = await GoogleApiClientUtils.Docs.write({ reference, content, mode });
-            let pushSuccess = response !== undefined && !("errors" in response);
-            DocumentDecorations.Instance.startPushOutcome(pushSuccess);
-        }
+            let undo = () => {
+                let content = exportState.body;
+                if (reference && content) {
+                    GoogleApiClientUtils.Docs.write({ reference, content, mode });
+                }
+            };
+            UndoManager.AddEvent({ undo, redo });
+            redo();
+        });
     }
 
     pullFromGoogleDoc = async (handler: PullHandler) => {
-        let dataDoc = Doc.GetProto(this.props.Document);
+        let dataDoc = this.dataDoc;
         let documentId = StrCast(dataDoc[GoogleRef]);
+        let exportState: GoogleApiClientUtils.Docs.ReadResult = {};
         if (documentId) {
-            let exportState = await GoogleApiClientUtils.Docs.read({ documentId });
-            UndoManager.RunInBatch(() => handler(exportState, dataDoc), Pulls);
+            exportState = await GoogleApiClientUtils.Docs.read({ documentId });
         }
+        UndoManager.RunInBatch(() => handler(exportState, dataDoc), Pulls);
     }
 
     updateState = (exportState: GoogleApiClientUtils.Docs.ReadResult, dataDoc: Doc) => {
@@ -436,6 +450,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
                 this.isGoogleDocsUpdate = true;
                 dataDoc.data = new RichTextField(data[FromPlainText](exportState.body));
                 dataDoc.title = exportState.title;
+                dataDoc.unchanged = true;
             }
         } else {
             delete dataDoc[GoogleRef];
@@ -449,7 +464,11 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
             if (data) {
                 let storedPlainText = data[ToPlainText]() + "\n";
                 let receivedPlainText = exportState.body;
-                DocumentDecorations.Instance.setPullState(storedPlainText === receivedPlainText);
+                let storedTitle = dataDoc.title;
+                let receivedTitle = exportState.title;
+                let unchanged = storedPlainText === receivedPlainText && storedTitle === receivedTitle;
+                dataDoc.unchanged = unchanged;
+                DocumentDecorations.Instance.setPullState(unchanged);
             }
         }
     }
-- 
cgit v1.2.3-70-g09d2


From dc587b0fb493d869e6cd38fa94b81105da4fbaab Mon Sep 17 00:00:00 2001
From: Sam Wilkins <samwilkins333@gmail.com>
Date: Tue, 20 Aug 2019 20:03:08 -0400
Subject: finished UI

---
 src/client/views/DocumentDecorations.scss   |  6 +-
 src/client/views/DocumentDecorations.tsx    | 92 ++++++++++++++++++-----------
 src/client/views/nodes/FormattedTextBox.tsx |  3 +-
 src/new_fields/RichTextField.ts             |  7 ++-
 4 files changed, 67 insertions(+), 41 deletions(-)

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

diff --git a/src/client/views/DocumentDecorations.scss b/src/client/views/DocumentDecorations.scss
index 0b7411fca..ef7159370 100644
--- a/src/client/views/DocumentDecorations.scss
+++ b/src/client/views/DocumentDecorations.scss
@@ -255,4 +255,8 @@ $linkGap : 3px;
     input {
         margin-right: 10px;
     }
-}
\ No newline at end of file
+}
+
+@-moz-keyframes spin { 100% { -moz-transform: rotate(360deg); } }
+@-webkit-keyframes spin { 100% { -webkit-transform: rotate(360deg); } }
+@keyframes spin { 100% { -webkit-transform: rotate(360deg); transform: rotate(360deg); } }
\ No newline at end of file
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 05239073b..7645af054 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -1,5 +1,5 @@
 import { library, IconProp } from '@fortawesome/fontawesome-svg-core';
-import { faLink, faTag, faArrowAltCircleDown, faArrowAltCircleUp, faCheckCircle, faStopCircle, faCloudUploadAlt } from '@fortawesome/free-solid-svg-icons';
+import { faLink, faTag, faArrowAltCircleDown, faArrowAltCircleUp, faCheckCircle, faStopCircle, faCloudUploadAlt, faSyncAlt } from '@fortawesome/free-solid-svg-icons';
 import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
 import { action, computed, observable, reaction, runInAction } from "mobx";
 import { observer } from "mobx-react";
@@ -41,8 +41,10 @@ library.add(faArrowAltCircleUp);
 library.add(faStopCircle);
 library.add(faCheckCircle);
 library.add(faCloudUploadAlt);
+library.add(faSyncAlt);
 
 const cloud: IconProp = "cloud-upload-alt";
+const fetch: IconProp = "sync-alt";
 
 @observer
 export class DocumentDecorations extends React.Component<{}, { value: string }> {
@@ -76,48 +78,47 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
     @observable public pushIcon: IconProp = "arrow-alt-circle-up";
     @observable public pullIcon: IconProp = "arrow-alt-circle-down";
     @observable public pullColor: string = "white";
+    @observable public isAnimatingFetch = false;
     public pullColorAnimating = false;
 
     private pullAnimating = false;
     private pushAnimating = false;
 
     public startPullOutcome = action((success: boolean) => {
-        if (this.pullAnimating) {
-            return;
+        if (!this.pullAnimating) {
+            this.pullAnimating = true;
+            this.pullIcon = success ? "check-circle" : "stop-circle";
+            setTimeout(() => runInAction(() => {
+                this.pullIcon = "arrow-alt-circle-down";
+                this.pullAnimating = false;
+            }), 1000);
         }
-        this.pullAnimating = true;
-        this.pullIcon = success ? "check-circle" : "stop-circle";
-        setTimeout(() => runInAction(() => {
-            this.pullIcon = "arrow-alt-circle-down";
-            this.pullAnimating = false;
-        }), 1000);
     });
 
     public startPushOutcome = action((success: boolean) => {
-        if (this.pushAnimating) {
-            return;
+        if (!this.pushAnimating) {
+            this.pushAnimating = true;
+            this.pushIcon = success ? "check-circle" : "stop-circle";
+            setTimeout(() => runInAction(() => {
+                this.pushIcon = "arrow-alt-circle-up";
+                this.pushAnimating = false;
+            }), 1000);
         }
-        this.pushAnimating = true;
-        this.pushIcon = success ? "check-circle" : "stop-circle";
-        setTimeout(() => runInAction(() => {
-            this.pushIcon = "arrow-alt-circle-up";
-            this.pushAnimating = false;
-        }), 1000);
     });
 
-    public setPullState = (unchanged: boolean) => {
-        if (this.pullColorAnimating) {
-            return;
+    public setPullState = action((unchanged: boolean) => {
+        this.isAnimatingFetch = false;
+        if (!this.pullColorAnimating) {
+            this.pullColorAnimating = true;
+            this.pullColor = unchanged ? "lawngreen" : "red";
+            setTimeout(this.clearPullColor, 1000);
         }
-        this.pullColorAnimating = true;
-        this.pullColor = unchanged ? "lawngreen" : "red";
-        setTimeout(() => {
-            runInAction(() => {
-                this.pullColor = "white";
-                this.pullColorAnimating = false;
-            });
-        }, 2000);
-    }
+    });
+
+    private clearPullColor = action(() => {
+        this.pullColor = "white";
+        this.pullColorAnimating = false;
+    });
 
     constructor(props: Readonly<{}>) {
         super(props);
@@ -679,7 +680,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
         let canPush = this.targetDoc.data && this.targetDoc.data instanceof RichTextField;
         if (!canPush) return (null);
         let published = Doc.GetProto(this.targetDoc)[GoogleRef] !== undefined;
-        let icon: IconProp = published ? (this.pushIcon as any) : (cloud as any);
+        let icon: IconProp = published ? (this.pushIcon as any) : cloud;
         return (
             <div className={"linkButtonWrapper"}>
                 <div title={`${published ? "Push" : "Publish"} to Google Docs`} className="linkButton-linker" onClick={() => {
@@ -695,14 +696,33 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
     considerGoogleDocsPull = () => {
         let canPull = this.targetDoc.data && this.targetDoc.data instanceof RichTextField;
         let dataDoc = Doc.GetProto(this.targetDoc);
-        if (!canPull || !dataDoc[GoogleRef] || dataDoc.unchanged) return (null);
+        if (!canPull || !dataDoc[GoogleRef]) return (null);
+        let icon = !dataDoc.unchanged ? (this.pullIcon as any) : fetch;
+        let animation = this.isAnimatingFetch ? "spin 0.5s linear infinite" : "none";
         return (
             <div className={"linkButtonWrapper"}>
-                <div style={{ backgroundColor: this.pullColor, transition: "1s ease all" }} title="Pull From Google Docs" className="linkButton-linker" onClick={() => {
-                    DocumentDecorations.hasPulledHack = false;
-                    this.targetDoc[Pulls] = NumCast(this.targetDoc[Pulls]) + 1;
-                }}>
-                    <FontAwesomeIcon className="documentdecorations-icon" icon={this.pullIcon} size="sm" />
+                <div
+                    title="Pull From Google Docs"
+                    className="linkButton-linker"
+                    style={{
+                        backgroundColor: this.pullColor,
+                        transition: "0.2s ease all"
+                    }}
+                    onClick={() => {
+                        this.clearPullColor();
+                        DocumentDecorations.hasPulledHack = false;
+                        this.targetDoc[Pulls] = NumCast(this.targetDoc[Pulls]) + 1;
+                        dataDoc.unchanged && runInAction(() => this.isAnimatingFetch = true);
+                    }}>
+                    <FontAwesomeIcon
+                        style={{
+                            WebkitAnimation: animation,
+                            MozAnimation: animation
+                        }}
+                        className="documentdecorations-icon"
+                        icon={icon}
+                        size="sm"
+                    />
                 </div>
             </div>
         );
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index 8a355a425..e06be7079 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -354,7 +354,8 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
             () => {
                 if (!DocumentDecorations.hasPulledHack) {
                     DocumentDecorations.hasPulledHack = true;
-                    this.pullFromGoogleDoc(this.updateState);
+                    let unchanged = this.dataDoc.unchanged;
+                    this.pullFromGoogleDoc(unchanged ? this.checkState : this.updateState);
                 }
             }
         );
diff --git a/src/new_fields/RichTextField.ts b/src/new_fields/RichTextField.ts
index ab58329f9..cae5623e6 100644
--- a/src/new_fields/RichTextField.ts
+++ b/src/new_fields/RichTextField.ts
@@ -38,9 +38,10 @@ export class RichTextField extends ObjectField {
         let blockText = (block: any) => block.text;
         let concatenateParagraph = (p: any) => (p.content ? p.content.map(blockText).join(joiner) : "") + delimiter;
 
-        // Concatentate paragraphs and string the result together. Trim the last newline, an artifact.
-        let textParagraphs = paragraphs.map(concatenateParagraph);
-        return textParagraphs.join(joiner).trimEnd(delimiter);
+        // Concatentate paragraphs and string the result together
+        let textParagraphs: string[] = paragraphs.map(concatenateParagraph);
+        let plainText = textParagraphs.join(joiner);
+        return plainText.substring(0, plainText.length - 1);
     }
 
     [FromPlainText](plainText: string) {
-- 
cgit v1.2.3-70-g09d2


From bd734ec84fa8fae36fceebf74da44216a0746b23 Mon Sep 17 00:00:00 2001
From: Sam Wilkins <samwilkins333@gmail.com>
Date: Tue, 20 Aug 2019 21:02:05 -0400
Subject: open remote document on ctrl click

---
 src/client/views/DocumentDecorations.tsx | 24 +++++++++++++++++-------
 1 file changed, 17 insertions(+), 7 deletions(-)

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

diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index d5d59a8ee..3482fd043 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -1,5 +1,5 @@
 import { library, IconProp } from '@fortawesome/fontawesome-svg-core';
-import { faLink, faTag, faTimes, faArrowAltCircleDown, faArrowAltCircleUp, faCheckCircle, faStopCircle, faCloudUploadAlt, faSyncAlt } from '@fortawesome/free-solid-svg-icons';
+import { faLink, faTag, faTimes, faArrowAltCircleDown, faArrowAltCircleUp, faCheckCircle, faStopCircle, faCloudUploadAlt, faSyncAlt, faShare } from '@fortawesome/free-solid-svg-icons';
 import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
 import { action, computed, observable, reaction, runInAction } from "mobx";
 import { observer } from "mobx-react";
@@ -43,6 +43,7 @@ library.add(faStopCircle);
 library.add(faCheckCircle);
 library.add(faCloudUploadAlt);
 library.add(faSyncAlt);
+library.add(faShare);
 
 const cloud: IconProp = "cloud-upload-alt";
 const fetch: IconProp = "sync-alt";
@@ -81,6 +82,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
     @observable public pullIcon: IconProp = "arrow-alt-circle-down";
     @observable public pullColor: string = "white";
     @observable public isAnimatingFetch = false;
+    @observable public openHover = false;
     public pullColorAnimating = false;
 
     private pullAnimating = false;
@@ -710,21 +712,29 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
         let dataDoc = Doc.GetProto(this.targetDoc);
         if (!canPull || !dataDoc[GoogleRef]) return (null);
         let icon = !dataDoc.unchanged ? (this.pullIcon as any) : fetch;
+        icon = this.openHover ? "share" : icon;
         let animation = this.isAnimatingFetch ? "spin 0.5s linear infinite" : "none";
+        let title = `${!dataDoc.unchanged ? "Pull from" : "Fetch"} Google Docs`;
         return (
             <div className={"linkButtonWrapper"}>
                 <div
-                    title="Pull From Google Docs"
+                    title={title}
                     className="linkButton-linker"
                     style={{
                         backgroundColor: this.pullColor,
                         transition: "0.2s ease all"
                     }}
-                    onClick={() => {
-                        this.clearPullColor();
-                        DocumentDecorations.hasPulledHack = false;
-                        this.targetDoc[Pulls] = NumCast(this.targetDoc[Pulls]) + 1;
-                        dataDoc.unchanged && runInAction(() => this.isAnimatingFetch = true);
+                    onPointerEnter={e => e.ctrlKey && runInAction(() => this.openHover = true)}
+                    onPointerLeave={() => runInAction(() => this.openHover = false)}
+                    onClick={e => {
+                        if (e.ctrlKey) {
+                            window.open(`https://docs.google.com/document/d/${dataDoc[GoogleRef]}/edit`);
+                        } else {
+                            this.clearPullColor();
+                            DocumentDecorations.hasPulledHack = false;
+                            this.targetDoc[Pulls] = NumCast(this.targetDoc[Pulls]) + 1;
+                            dataDoc.unchanged && runInAction(() => this.isAnimatingFetch = true);
+                        }
                     }}>
                     <FontAwesomeIcon
                         style={{
-- 
cgit v1.2.3-70-g09d2


From f9835d0a751e76d09cb81dc9de81972e7bc73bf1 Mon Sep 17 00:00:00 2001
From: Sam Wilkins <samwilkins333@gmail.com>
Date: Tue, 20 Aug 2019 21:05:25 -0400
Subject: auto height set on google docs publishing

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

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

diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 3482fd043..891fd7847 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -694,6 +694,9 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
         let canPush = this.targetDoc.data && this.targetDoc.data instanceof RichTextField;
         if (!canPush) return (null);
         let published = Doc.GetProto(this.targetDoc)[GoogleRef] !== undefined;
+        if (!published) {
+            this.targetDoc.autoHeight = true;
+        }
         let icon: IconProp = published ? (this.pushIcon as any) : cloud;
         return (
             <div className={"linkButtonWrapper"}>
-- 
cgit v1.2.3-70-g09d2


From d260d9abc13ae60c3206c3110f2d7f23ceeb2449 Mon Sep 17 00:00:00 2001
From: Sam Wilkins <samwilkins333@gmail.com>
Date: Wed, 21 Aug 2019 05:29:07 -0400
Subject: prosemirror bulleted list dictation integration

---
 src/client/util/DictationManager.ts         | 48 ++++++++++--------
 src/client/views/DocumentDecorations.tsx    |  3 +-
 src/client/views/nodes/FormattedTextBox.tsx | 76 ++++++++++++++++++++++++-----
 3 files changed, 93 insertions(+), 34 deletions(-)

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

diff --git a/src/client/util/DictationManager.ts b/src/client/util/DictationManager.ts
index 758482eae..781e5e465 100644
--- a/src/client/util/DictationManager.ts
+++ b/src/client/util/DictationManager.ts
@@ -45,7 +45,7 @@ export namespace DictationManager {
 
     export namespace Controls {
 
-        const infringe = "unable to process: dictation manager still involved in previous session";
+        export const Infringed = "unable to process: dictation manager still involved in previous session";
         const intraSession = ". ";
         const interSession = " ... ";
 
@@ -69,30 +69,32 @@ export namespace DictationManager {
             delimiters: DelimiterArgs;
             interimHandler: InterimResultHandler;
             tryExecute: boolean;
+            terminators: string[];
         }
 
         export const listen = async (options?: Partial<ListeningOptions>) => {
             let results: string | undefined;
-            let main = MainView.Instance;
+            // let main = MainView.Instance;
 
-            main.dictationOverlayVisible = true;
-            main.isListening = { interim: false };
+            // main.dictationOverlayVisible = true;
+            // main.isListening = { interim: false };
 
             try {
                 results = await listenImpl(options);
                 if (results) {
                     Utils.CopyText(results);
-                    main.isListening = false;
-                    let execute = options && options.tryExecute;
-                    main.dictatedPhrase = execute ? results.toLowerCase() : results;
-                    main.dictationSuccess = execute ? await DictationManager.Commands.execute(results) : true;
+                    // main.isListening = false;
+                    // let execute = options && options.tryExecute;
+                    // main.dictatedPhrase = execute ? results.toLowerCase() : results;
+                    // main.dictationSuccess = execute ? await DictationManager.Commands.execute(results) : true;
+                    options && options.tryExecute && await DictationManager.Commands.execute(results);
                 }
             } catch (e) {
-                main.isListening = false;
-                main.dictatedPhrase = results = `dictation error: ${"error" in e ? e.error : "unknown error"}`;
-                main.dictationSuccess = false;
+                // main.isListening = false;
+                // main.dictatedPhrase = results = `dictation error: ${"error" in e ? e.error : "unknown error"}`;
+                // main.dictationSuccess = false;
             } finally {
-                main.initiateDictationFade();
+                // main.initiateDictationFade();
             }
 
             return results;
@@ -100,7 +102,7 @@ export namespace DictationManager {
 
         const listenImpl = (options?: Partial<ListeningOptions>) => {
             if (isListening) {
-                return infringe;
+                return Infringed;
             }
             isListening = true;
 
@@ -128,6 +130,12 @@ export namespace DictationManager {
 
                 recognizer.onresult = (e: SpeechRecognitionEvent) => {
                     current = synthesize(e, intra);
+                    let matchedTerminator: string | undefined;
+                    if (options && options.terminators && (matchedTerminator = options.terminators.find(end => current ? current.trim().toLowerCase().endsWith(end.toLowerCase()) : false))) {
+                        current = matchedTerminator;
+                        recognizer.abort();
+                        return complete();
+                    }
                     handler && handler(current);
                     isManuallyStopped && complete();
                 };
@@ -163,13 +171,13 @@ export namespace DictationManager {
             }
             isManuallyStopped = true;
             salvageSession ? recognizer.stop() : recognizer.abort();
-            let main = MainView.Instance;
-            if (main.dictationOverlayVisible) {
-                main.cancelDictationFade();
-                main.dictationOverlayVisible = false;
-                main.dictationSuccess = undefined;
-                setTimeout(() => main.dictatedPhrase = placeholder, 500);
-            }
+            // let main = MainView.Instance;
+            // if (main.dictationOverlayVisible) {
+            // main.cancelDictationFade();
+            // main.dictationOverlayVisible = false;
+            // main.dictationSuccess = undefined;
+            // setTimeout(() => main.dictatedPhrase = placeholder, 500);
+            // }
         };
 
         const synthesize = (e: SpeechRecognitionEvent, delimiter?: string) => {
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 891fd7847..cc8e57af9 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -730,7 +730,8 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
                     onPointerEnter={e => e.ctrlKey && runInAction(() => this.openHover = true)}
                     onPointerLeave={() => runInAction(() => this.openHover = false)}
                     onClick={e => {
-                        if (e.ctrlKey) {
+                        if (e.altKey) {
+                            e.preventDefault();
                             window.open(`https://docs.google.com/document/d/${dataDoc[GoogleRef]}/edit`);
                         } else {
                             this.clearPullColor();
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index e460e3e50..222ebed8f 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -6,7 +6,7 @@ import { baseKeymap } from "prosemirror-commands";
 import { history } from "prosemirror-history";
 import { keymap } from "prosemirror-keymap";
 import { Fragment, Node, Node as ProsNode, NodeType, Slice } from "prosemirror-model";
-import { EditorState, Plugin, Transaction } from "prosemirror-state";
+import { EditorState, Plugin, Transaction, TextSelection } from "prosemirror-state";
 import { EditorView } from "prosemirror-view";
 import { DateField } from '../../../new_fields/DateField';
 import { Doc, DocListCast, Opt, WidthSym } from "../../../new_fields/Doc";
@@ -36,6 +36,7 @@ import { GoogleApiClientUtils, Pulls, Pushes } from '../../apis/google_docs/Goog
 import { DocumentDecorations } from '../DocumentDecorations';
 import { MainOverlayTextBox } from '../MainOverlayTextBox';
 import { DictationManager } from '../../util/DictationManager';
+import { ReplaceStep } from 'prosemirror-transform';
 
 library.add(faEdit);
 library.add(faSmile, faTextHeight, faUpload);
@@ -181,6 +182,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
                 const marks = tx.storedMarks;
                 if (marks) { FormattedTextBox._toolTipTextMenu.mark_key_pressed(marks); }
             }
+
             this._applyingChange = true;
             const fieldkey = "preview";
             if (this.extensionDoc) this.extensionDoc.text = state.doc.textBetween(0, state.doc.content.size, "\n\n");
@@ -268,23 +270,70 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
 
     setCurrentBulletContent = (value: string) => {
         if (this._editorView) {
-            this._editorView.state;
+            // let next = value.endsWith("bullet");
+            // if (next) {
+            //     value = value.split("bullet")[0];
+            // }
+            let state = this._editorView.state;
+            let from = state.selection.from;
+            let to = state.selection.to;
+            this._editorView.dispatch(state.tr.insertText(value, from, to));
+            state = this._editorView.state;
+            let updated = TextSelection.create(state.doc, from, from + value.length);
+            this._editorView.dispatch(state.tr.setSelection(updated));
+            // if (next) {
+            //     this.nextBullet(this._editorView.state.selection.to);
+            // }
         }
     }
 
-    considerRecordingSession = (e: KeyboardEvent) => {
-        if (e.which === 82 && e.altKey) {
-            let continuous = { indefinite: true };
-            DictationManager.Controls.listen({
-                interimHandler: this.setCurrentBulletContent,
-                continuous
-            });
+    nextBullet = (pos: number) => {
+        if (this._editorView) {
+            // DictationManager.Controls.stop(false);
+            let frag = Fragment.fromArray(this.newListItems(2));
+            let slice = new Slice(frag, 2, 2);
+            let state = this._editorView.state;
+            this._editorView.dispatch(state.tr.step(new ReplaceStep(pos, pos, slice)));
+            pos += 4;
+            state = this._editorView.state;
+            this._editorView.dispatch(state.tr.setSelection(TextSelection.create(this._editorView.state.doc, pos, pos)));
+            // this.recordSession();
+        }
+    }
+
+    private newListItems = (count: number) => {
+        let listItems: any[] = [];
+        for (let i = 0; i < count; i++) {
+            listItems.push(schema.nodes.list_item.create(null, schema.nodes.paragraph.create(null)));
         }
+        return listItems;
+    }
+
+    recordSession = async () => {
+        let completedCue = "end session";
+        let results = await DictationManager.Controls.listen({
+            interimHandler: this.setCurrentBulletContent,
+            continuous: { indefinite: false },
+            terminators: [completedCue, "bullet", "next"]
+        });
+        if (results && [DictationManager.Controls.Infringed, completedCue].includes(results)) {
+            DictationManager.Controls.stop();
+            return;
+        }
+        this.nextBullet(this._editorView!.state.selection.to);
+        setTimeout(this.recordSession, 2000);
+    }
+
+    recordKeyHandler = (e: KeyboardEvent) => {
+        if (this.props.Document !== SelectionManager.SelectedDocuments()[0].props.Document) {
+            return;
+        }
+        e.stopPropagation();
+        e.preventDefault();
+        e.which === 174 && e.altKey && this.recordSession();
     }
 
     componentDidMount() {
-        document.removeEventListener("keypress", this.considerRecordingSession);
-        document.addEventListener("keypress", this.considerRecordingSession);
         const config = {
             schema,
             inpRules, //these currently don't do anything, but could eventually be helpful
@@ -335,7 +384,6 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
                         updatedState.selection = { type: "text", anchor: end, head: end };
                         this._editorView.updateState(EditorState.fromJSON(config, updatedState));
                     }
-                    this.tryUpdateHeight();
                 }
             }
         );
@@ -595,7 +643,6 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
     }
 
     componentWillUnmount() {
-        document.removeEventListener("keypress", this.considerRecordingSession);
         this._editorView && this._editorView.destroy();
         this._reactionDisposer && this._reactionDisposer();
         this._proxyReactionDisposer && this._proxyReactionDisposer();
@@ -678,6 +725,8 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
 
     @action
     onFocused = (e: React.FocusEvent): void => {
+        document.removeEventListener("keypress", this.recordKeyHandler);
+        document.addEventListener("keypress", this.recordKeyHandler);
         if (!this.props.isOverlay) {
             FormattedTextBox.InputBoxOverlay = this;
         } else {
@@ -727,6 +776,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
         });
     }
     onBlur = (e: any) => {
+        document.removeEventListener("keypress", this.recordKeyHandler);
         if (this._undoTyping) {
             this._undoTyping.end();
             this._undoTyping = undefined;
-- 
cgit v1.2.3-70-g09d2


From 88454c8163115b1396a34f4836b5f6f04817a3f0 Mon Sep 17 00:00:00 2001
From: Sam Wilkins <samwilkins333@gmail.com>
Date: Wed, 21 Aug 2019 16:11:20 -0400
Subject: tryupdateheight fixes

---
 src/client/views/DocumentDecorations.tsx    |  4 ++--
 src/client/views/nodes/FormattedTextBox.tsx | 26 +++++++++++++-------------
 2 files changed, 15 insertions(+), 15 deletions(-)

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

diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index cc8e57af9..a28088032 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -714,7 +714,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
         let canPull = this.targetDoc.data && this.targetDoc.data instanceof RichTextField;
         let dataDoc = Doc.GetProto(this.targetDoc);
         if (!canPull || !dataDoc[GoogleRef]) return (null);
-        let icon = !dataDoc.unchanged ? (this.pullIcon as any) : fetch;
+        let icon = dataDoc.unchanged === false ? (this.pullIcon as any) : fetch;
         icon = this.openHover ? "share" : icon;
         let animation = this.isAnimatingFetch ? "spin 0.5s linear infinite" : "none";
         let title = `${!dataDoc.unchanged ? "Pull from" : "Fetch"} Google Docs`;
@@ -727,7 +727,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
                         backgroundColor: this.pullColor,
                         transition: "0.2s ease all"
                     }}
-                    onPointerEnter={e => e.ctrlKey && runInAction(() => this.openHover = true)}
+                    onPointerEnter={e => e.altKey && runInAction(() => this.openHover = true)}
                     onPointerLeave={() => runInAction(() => this.openHover = false)}
                     onClick={e => {
                         if (e.altKey) {
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index 1e0975b4b..c8722c6e3 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -1,6 +1,6 @@
 import { library } from '@fortawesome/fontawesome-svg-core';
 import { faEdit, faSmile, faTextHeight, faUpload } from '@fortawesome/free-solid-svg-icons';
-import { action, computed, IReactionDisposer, Lambda, observable, reaction, runInAction } from "mobx";
+import { action, computed, IReactionDisposer, Lambda, observable, reaction, runInAction, autorun } from "mobx";
 import { observer } from "mobx-react";
 import { baseKeymap } from "prosemirror-commands";
 import { history } from "prosemirror-history";
@@ -87,7 +87,6 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
     private pushReactionDisposer: Opt<IReactionDisposer>;
     private dropDisposer?: DragManager.DragDropDisposer;
     public get CurrentDiv(): HTMLDivElement { return this._ref.current!; }
-    private isGoogleDocsUpdate = false;
     @observable _entered = false;
 
     @observable public static InputBoxOverlay?: FormattedTextBox = undefined;
@@ -321,7 +320,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
     private newListItems = (count: number) => {
         let listItems: any[] = [];
         for (let i = 0; i < count; i++) {
-            listItems.push(schema.nodes.list_item.create(null, schema.nodes.paragraph.create(null)));
+            listItems.push(schema.nodes.list_item.create(undefined, schema.nodes.paragraph.create()));
         }
         return listItems;
     }
@@ -359,7 +358,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
         }
 
         this.pullFromGoogleDoc(this.checkState);
-        runInAction(() => DocumentDecorations.Instance.isAnimatingFetch = true);
+        this.dataDoc[GoogleRef] && this.dataDoc.unchanged && runInAction(() => DocumentDecorations.Instance.isAnimatingFetch = true);
 
         this._reactionDisposer = reaction(
             () => {
@@ -370,13 +369,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
                 if (this._editorView && !this._applyingChange) {
                     let updatedState = JSON.parse(incomingValue);
                     this._editorView.updateState(EditorState.fromJSON(config, updatedState));
-                    // manually sets cursor selection at the end of the text on focus
-                    if (this.isGoogleDocsUpdate) {
-                        this.isGoogleDocsUpdate = false;
-                        let end = this._editorView.state.doc.content.size - 1;
-                        updatedState.selection = { type: "text", anchor: end, head: end };
-                        this._editorView.updateState(EditorState.fromJSON(config, updatedState));
-                    }
+                    this.tryUpdateHeight();
                 }
             }
         );
@@ -485,16 +478,22 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
             const data = Cast(dataDoc.data, RichTextField);
             if (data instanceof RichTextField) {
                 pullSuccess = true;
-                this.isGoogleDocsUpdate = true;
                 dataDoc.data = new RichTextField(data[FromPlainText](exportState.body));
+                setTimeout(() => {
+                    if (this._editorView) {
+                        let state = this._editorView.state;
+                        let end = state.doc.content.size - 1;
+                        this._editorView.dispatch(state.tr.setSelection(TextSelection.create(state.doc, end, end)));
+                    }
+                }, 0);
                 dataDoc.title = exportState.title;
+                this.Document.customTitle = true;
                 dataDoc.unchanged = true;
             }
         } else {
             delete dataDoc[GoogleRef];
         }
         DocumentDecorations.Instance.startPullOutcome(pullSuccess);
-        setTimeout(() => this.tryUpdateHeight(), 0);
     }
 
     checkState = (exportState: GoogleApiClientUtils.Docs.ReadResult, dataDoc: Doc) => {
@@ -719,6 +718,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
     onFocused = (e: React.FocusEvent): void => {
         document.removeEventListener("keypress", this.recordKeyHandler);
         document.addEventListener("keypress", this.recordKeyHandler);
+        this.tryUpdateHeight();
         if (!this.props.isOverlay) {
             FormattedTextBox.InputBoxOverlay = this;
         } else {
-- 
cgit v1.2.3-70-g09d2

