From d2c9550f23c4e5654822ac01b973bb965e3f6dec Mon Sep 17 00:00:00 2001
From: Sam Wilkins <samuel_wilkins@brown.edu>
Date: Fri, 14 Jun 2019 20:49:12 -0400
Subject: cleaned up Docs namespace and thoroughly documented
 DocServer.GetRefFields

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

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

diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx
index 3d9750a85..98b14f9c8 100644
--- a/src/client/views/Main.tsx
+++ b/src/client/views/Main.tsx
@@ -5,7 +5,7 @@ import * as ReactDOM from 'react-dom';
 import * as React from 'react';
 
 (async () => {
-    await Docs.initProtos();
+    await Docs.Prototypes.initialize();
     await CurrentUserUtils.loadCurrentUser();
     ReactDOM.render(<MainView />, document.getElementById('root'));
 })();
-- 
cgit v1.2.3-70-g09d2


From 87a3063fa0d0726334125209e74930ebf8422a44 Mon Sep 17 00:00:00 2001
From: Sam Wilkins <samwilkins333@gmail.com>
Date: Sat, 6 Jul 2019 03:50:33 -0400
Subject: fixed formatting, reset and document persistence

---
 .../util/Import & Export/DirectoryImportBox.tsx    | 129 ++++++++++++++-------
 .../util/Import & Export/ImportMetadataEntry.tsx   |  42 ++++++-
 src/client/views/Main.tsx                          |   2 +-
 3 files changed, 125 insertions(+), 48 deletions(-)

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

diff --git a/src/client/util/Import & Export/DirectoryImportBox.tsx b/src/client/util/Import & Export/DirectoryImportBox.tsx
index 88ce9c0e9..b75944745 100644
--- a/src/client/util/Import & Export/DirectoryImportBox.tsx	
+++ b/src/client/util/Import & Export/DirectoryImportBox.tsx	
@@ -1,9 +1,9 @@
 import "fs";
 import React = require("react");
-import { Doc, Opt } from "../../../new_fields/Doc";
+import { Doc, Opt, DocListCast, DocListCastAsync } from "../../../new_fields/Doc";
 import { DocServer } from "../../DocServer";
 import { RouteStore } from "../../../server/RouteStore";
-import { action, observable, autorun, runInAction } from "mobx";
+import { action, observable, autorun, runInAction, computed } from "mobx";
 import { FieldViewProps, FieldView } from "../../views/nodes/FieldView";
 import Measure, { ContentRect } from "react-measure";
 import { library } from '@fortawesome/fontawesome-svg-core';
@@ -11,9 +11,13 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { faArrowUp, faTag, faPlus } from '@fortawesome/free-solid-svg-icons';
 import { Docs, DocumentOptions } from "../../documents/Documents";
 import { observer } from "mobx-react";
-import ImportMetadataEntry from "./ImportMetadataEntry";
+import ImportMetadataEntry, { keyPlaceholder, valuePlaceholder } from "./ImportMetadataEntry";
 import { Utils } from "../../../Utils";
 import { DocumentManager } from "../DocumentManager";
+import { Id } from "../../../new_fields/FieldSymbols";
+import { List } from "../../../new_fields/List";
+import { Cast, BoolCast, NumCast } from "../../../new_fields/Types";
+import { listSpec } from "../../../new_fields/Schema";
 
 @observer
 export default class DirectoryImportBox extends React.Component<FieldViewProps> {
@@ -22,27 +26,45 @@ export default class DirectoryImportBox extends React.Component<FieldViewProps>
     @observable private left = 0;
     private dimensions = 50;
 
-    @observable private editingMetadata = false;
-    @observable private metadata_guids: string[] = [];
     @observable private entries: ImportMetadataEntry[] = [];
 
     @observable private quota = 1;
     @observable private remaining = 1;
 
-    @observable private uploadBegun = false;
+    @observable private uploading = false;
     @observable private removeHover = false;
-    @observable private shouldKeep = false;
 
     public static LayoutString() { return FieldView.LayoutString(DirectoryImportBox); }
 
     constructor(props: FieldViewProps) {
         super(props);
         library.add(faArrowUp, faTag, faPlus);
+        let doc = this.props.Document;
+        this.editingMetadata = this.editingMetadata || false;
+        this.persistent = this.persistent || false;
+        !Cast(doc.data, listSpec(Doc)) && (doc.data = new List<Doc>());
+    }
+
+    @computed
+    private get editingMetadata() {
+        return BoolCast(this.props.Document.editingMetadata);
+    }
+
+    private set editingMetadata(value: boolean) {
+        this.props.Document.editingMetadata = value;
+    }
+
+    @computed
+    private get persistent() {
+        return BoolCast(this.props.Document.persistent);
+    }
+
+    private set persistent(value: boolean) {
+        this.props.Document.persistent = value;
     }
 
-    @action
     handleSelection = async (e: React.ChangeEvent<HTMLInputElement>) => {
-        this.uploadBegun = true;
+        runInAction(() => this.uploading = true);
 
         let promises: Promise<void>[] = [];
         let docs: Doc[] = [];
@@ -58,7 +80,7 @@ export default class DirectoryImportBox extends React.Component<FieldViewProps>
             file && validated.push(file);
         }
 
-        this.quota = validated.length;
+        runInAction(() => this.quota = validated.length);
 
         for (let uploaded_file of validated) {
             if (!uploaded_file) {
@@ -70,15 +92,14 @@ export default class DirectoryImportBox extends React.Component<FieldViewProps>
             let dropFileName = uploaded_file ? uploaded_file.name : "-empty-";
             let type = uploaded_file.type;
 
-            this.remaining++;
+            runInAction(() => this.remaining++);
 
             let prom = fetch(DocServer.prepend(RouteStore.upload), {
                 method: 'POST',
                 body: formData
             }).then(async (res: Response) => {
                 (await res.json()).map(action((file: any) => {
-                    let path = DocServer.prepend(file);
-                    let docPromise = Docs.getDocumentFromType(type, path, { nativeWidth: 300, width: 300, title: dropFileName });
+                    let docPromise = Docs.getDocumentFromType(type, DocServer.prepend(file), { nativeWidth: 300, width: 300, title: dropFileName });
                     docPromise.then(doc => {
                         doc && docs.push(doc) && runInAction(() => this.remaining--);
                     });
@@ -97,24 +118,30 @@ export default class DirectoryImportBox extends React.Component<FieldViewProps>
         });
 
         let doc = this.props.Document;
+        let height: number = NumCast(doc.height) || 0;
+        let offset: number = this.persistent ? (height === 0 ? 0 : height + 30) : 0;
         let options: DocumentOptions = {
             title: `Import of ${directory}`,
             width: 1105,
             height: 500,
-            x: Doc.GetT(doc, "x", "number"),
-            y: Doc.GetT(doc, "y", "number")
+            x: NumCast(doc.x),
+            y: NumCast(doc.y) + offset
         };
         let parent = this.props.ContainingCollectionView;
         if (parent) {
             let importContainer = Docs.StackingDocument(docs, options);
             importContainer.singleColumn = false;
             Doc.AddDocToList(Doc.GetProto(parent.props.Document), "data", importContainer);
-            !this.shouldKeep && this.props.removeDocument && this.props.removeDocument(doc);
+            !this.persistent && this.props.removeDocument && this.props.removeDocument(doc);
             DocumentManager.Instance.jumpToDocument(importContainer, true);
-            this.uploadBegun = false;
+
+        }
+
+        runInAction(() => {
+            this.uploading = false;
             this.quota = 1;
             this.remaining = 1;
-        }
+        });
     }
 
     componentDidMount() {
@@ -134,30 +161,43 @@ export default class DirectoryImportBox extends React.Component<FieldViewProps>
     }
 
     @action
-    addMetadataEntry = () => {
-        this.metadata_guids.push(Utils.GenerateGuid());
+    addMetadataEntry = async () => {
+        let entryDoc = new Doc();
+        entryDoc.checked = false;
+        entryDoc.key = keyPlaceholder;
+        entryDoc.value = valuePlaceholder;
+        Doc.AddDocToList(this.props.Document, "data", entryDoc);
     }
 
     @action
-    remove = (entry: ImportMetadataEntry) => {
-        let index = this.entries.indexOf(entry);
-        let key = entry.key;
-        this.entries.splice(index, 1);
-        this.metadata_guids.splice(this.metadata_guids.indexOf(key), 1);
+    remove = async (entry: ImportMetadataEntry) => {
+        let metadata = await DocListCastAsync(this.props.Document.data);
+        if (metadata) {
+            let index = this.entries.indexOf(entry);
+            if (index !== -1) {
+                runInAction(() => this.entries.splice(index, 1));
+                index = metadata.indexOf(entry.props.Document);
+                if (index !== -1) {
+                    metadata.splice(index, 1);
+                }
+            }
+
+        }
     }
 
     render() {
         let dimensions = 50;
-        let guids = this.metadata_guids.map(el => el);
+        let entries = DocListCast(this.props.Document.data);
         let isEditing = this.editingMetadata;
         let remaining = this.remaining;
         let quota = this.quota;
-        let percent = `${100 - (remaining / quota * 100)}`;
-        let uploadBegun = this.uploadBegun;
+        let uploading = this.uploading;
         let showRemoveLabel = this.removeHover;
-        let keep = this.shouldKeep;
+        let persistent = this.persistent;
+        let percent = `${100 - (remaining / quota * 100)}`;
         percent = percent.split(".")[0];
         percent = percent.startsWith("100") ? "99" : percent;
+        let marginOffset = (percent.length === 1 ? 5 : 0) - 1.6;
         return (
             <Measure offset onResize={this.preserveCentering}>
                 {({ measureRef }) =>
@@ -192,7 +232,7 @@ export default class DirectoryImportBox extends React.Component<FieldViewProps>
                                 position: "absolute",
                                 left: this.left + 12.6,
                                 top: this.top + 11,
-                                opacity: uploadBegun ? 0 : 1,
+                                opacity: uploading ? 0 : 1,
                                 transition: "0.4s opacity ease"
                             }}>
                                 <FontAwesomeIcon icon={faArrowUp} color="#FFFFFF" size={"2x"} />
@@ -202,7 +242,7 @@ export default class DirectoryImportBox extends React.Component<FieldViewProps>
                                     width: 80,
                                     height: 80,
                                     transition: "0.4s opacity ease",
-                                    opacity: uploadBegun ? 0.7 : 0,
+                                    opacity: uploading ? 0.7 : 0,
                                     position: "absolute",
                                     top: this.top - 15,
                                     left: this.left - 15
@@ -211,16 +251,17 @@ export default class DirectoryImportBox extends React.Component<FieldViewProps>
                         </label>
                         <input
                             type={"checkbox"}
-                            onChange={e => runInAction(() => this.shouldKeep = e.target.checked)}
+                            onChange={e => runInAction(() => this.persistent = e.target.checked)}
                             style={{
                                 margin: 0,
                                 position: "absolute",
                                 left: 10,
                                 bottom: 10,
-                                opacity: isEditing ? 0 : 1,
+                                opacity: isEditing || uploading ? 0 : 1,
                                 transition: "0.4s opacity ease",
-                                pointerEvents: isEditing ? "none" : "all"
+                                pointerEvents: isEditing || uploading ? "none" : "all"
                             }}
+                            checked={this.persistent}
                             onPointerEnter={action(() => this.removeHover = true)}
                             onPointerLeave={action(() => this.removeHover = false)}
                         />
@@ -232,18 +273,18 @@ export default class DirectoryImportBox extends React.Component<FieldViewProps>
                                 fontSize: 12,
                                 opacity: showRemoveLabel ? 1 : 0,
                                 transition: "0.4s opacity ease"
-                            }}>Template will be {keep ? "kept" : "removed"} after upload</p>
+                            }}>Template will be <span style={{ textDecoration: "underline", textDecorationColor: persistent ? "green" : "red", color: persistent ? "green" : "red" }}>{persistent ? "kept" : "removed"}</span> after upload</p>
                         <div
                             style={{
                                 transition: "0.4s opacity ease",
-                                opacity: uploadBegun ? 1 : 0,
+                                opacity: uploading ? 1 : 0,
                                 pointerEvents: "none",
                                 position: "absolute",
                                 left: 10,
                                 top: this.top + 12.3,
                                 fontSize: 18,
                                 color: "white",
-                                marginLeft: this.left - 1.6
+                                marginLeft: this.left + marginOffset
                             }}>{percent}%</div>
                         <div
                             style={{
@@ -253,7 +294,10 @@ export default class DirectoryImportBox extends React.Component<FieldViewProps>
                                 borderRadius: "50%",
                                 width: 25,
                                 height: 25,
-                                background: "black"
+                                background: "black",
+                                pointerEvents: uploading ? "none" : "all",
+                                opacity: uploading ? 0 : 1,
+                                transition: "0.4s opacity ease"
                             }}
                             title={isEditing ? "Back to Upload" : "Add Metadata"}
                             onClick={action(() => this.editingMetadata = !this.editingMetadata)}
@@ -263,7 +307,9 @@ export default class DirectoryImportBox extends React.Component<FieldViewProps>
                                 pointerEvents: "none",
                                 position: "absolute",
                                 right: isEditing ? 16.3 : 14.5,
-                                top: isEditing ? 15.4 : 16
+                                top: isEditing ? 15.4 : 16,
+                                opacity: uploading ? 0 : 1,
+                                transition: "0.4s opacity ease"
                             }}
                             icon={isEditing ? faArrowUp : faTag}
                             color="#FFFFFF"
@@ -304,9 +350,10 @@ export default class DirectoryImportBox extends React.Component<FieldViewProps>
                             </div>
                             <p style={{ paddingLeft: 10, paddingTop: 8, paddingBottom: 7 }} >Add metadata to your import...</p>
                             <hr style={{ margin: "6px 10px 12px 10px" }} />
-                            {guids.map(guid =>
+                            {entries.map(doc =>
                                 <ImportMetadataEntry
-                                    key={guid}
+                                    Document={doc}
+                                    key={doc[Id]}
                                     remove={this.remove}
                                     ref={(el) => { if (el) this.entries.push(el); }}
                                     next={this.addMetadataEntry}
diff --git a/src/client/util/Import & Export/ImportMetadataEntry.tsx b/src/client/util/Import & Export/ImportMetadataEntry.tsx
index 3b2a6ebb5..f5198c39b 100644
--- a/src/client/util/Import & Export/ImportMetadataEntry.tsx	
+++ b/src/client/util/Import & Export/ImportMetadataEntry.tsx	
@@ -5,20 +5,20 @@ import { observable, action, computed } from "mobx";
 import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
 import { faPlus } from "@fortawesome/free-solid-svg-icons";
 import { library } from '@fortawesome/fontawesome-svg-core';
-import { Opt } from "../../../new_fields/Doc";
+import { Opt, Doc } from "../../../new_fields/Doc";
+import { StrCast, BoolCast } from "../../../new_fields/Types";
 
 interface KeyValueProps {
+    Document: Doc;
     remove: (self: ImportMetadataEntry) => void;
     next: () => void;
 }
 
-const keyPlaceholder = "Key";
-const valuePlaceholder = "Value";
+export const keyPlaceholder = "Key";
+export const valuePlaceholder = "Value";
 
 @observer
 export default class ImportMetadataEntry extends React.Component<KeyValueProps> {
-    @observable public key = keyPlaceholder;
-    @observable public value = valuePlaceholder;
 
     private keyRef = React.createRef<EditableView>();
     private valueRef = React.createRef<EditableView>();
@@ -34,8 +34,36 @@ export default class ImportMetadataEntry extends React.Component<KeyValueProps>
         return (this.key.length > 0 && this.key !== keyPlaceholder) && (this.value.length > 0 && this.value !== valuePlaceholder);
     }
 
+    @computed
+    private get backing() {
+        return this.props.Document;
+    }
+
+    @computed
     public get onDataDoc() {
-        return this.checkRef.current && this.checkRef.current.checked;
+        return BoolCast(this.backing.checked);
+    }
+
+    public set onDataDoc(value: boolean) {
+        this.backing.checked = value;
+    }
+
+    @computed
+    public get key() {
+        return StrCast(this.backing.key);
+    }
+
+    public set key(value: string) {
+        this.backing.key = value;
+    }
+
+    @computed
+    public get value() {
+        return StrCast(this.backing.value);
+    }
+
+    public set value(value: string) {
+        this.backing.value = value;
     }
 
     @action
@@ -75,10 +103,12 @@ export default class ImportMetadataEntry extends React.Component<KeyValueProps>
                 }}
             >
                 <input
+                    onChange={e => this.onDataDoc = e.target.checked}
                     ref={this.checkRef}
                     style={{ margin: "0 10px 0 15px" }}
                     type="checkbox"
                     title={"Add to Data Document?"}
+                    checked={this.onDataDoc}
                 />
                 <div className={"key_container"} style={keyValueStyle}>
                     <EditableView
diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx
index 3d9750a85..281d9159b 100644
--- a/src/client/views/Main.tsx
+++ b/src/client/views/Main.tsx
@@ -8,4 +8,4 @@ import * as React from 'react';
     await Docs.initProtos();
     await CurrentUserUtils.loadCurrentUser();
     ReactDOM.render(<MainView />, document.getElementById('root'));
-})();
+})();
\ No newline at end of file
-- 
cgit v1.2.3-70-g09d2


From 3daca894b6eaf1eb8590f54b1a5bf5feca663a08 Mon Sep 17 00:00:00 2001
From: yipstanley <stanley_yip@brown.edu>
Date: Fri, 12 Jul 2019 22:34:09 -0400
Subject: links now go between users

---
 src/client/documents/Documents.ts | 18 +++++++++++++++++-
 src/client/util/LinkManager.ts    |  5 ++++-
 src/client/views/Main.tsx         | 23 +++++++++++++++++++++++
 3 files changed, 44 insertions(+), 2 deletions(-)

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

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 2a3827782..638ba287f 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -54,7 +54,8 @@ export enum DocumentType {
     PDF = "pdf",
     ICON = "icon",
     IMPORT = "import",
-    LINK = "link"
+    LINK = "link",
+    LINKDOC = "linkdoc"
 }
 
 export interface DocumentOptions {
@@ -85,6 +86,12 @@ export interface DocumentOptions {
     // [key: string]: Opt<Field>;
 }
 
+class EmptyBox {
+    public static LayoutString() {
+        return "";
+    }
+}
+
 export namespace Docs {
 
     export namespace Prototypes {
@@ -148,6 +155,11 @@ export namespace Docs {
             [DocumentType.IMPORT, {
                 layout: { view: DirectoryImportBox },
                 options: { height: 150 }
+            }],
+            [DocumentType.LINKDOC, {
+                data: new List<Doc>(),
+                layout: { view: EmptyBox },
+                options: {}
             }]
         ]);
 
@@ -195,6 +207,10 @@ export namespace Docs {
             return PrototypeMap.get(type)!;
         }
 
+        export function MainLinkDocument() {
+            return Prototypes.get(DocumentType.LINKDOC);
+        }
+
         /**
          * This is a convenience method that is used to initialize
          * prototype documents for the first time.
diff --git a/src/client/util/LinkManager.ts b/src/client/util/LinkManager.ts
index 1ed040aa4..a647f22c1 100644
--- a/src/client/util/LinkManager.ts
+++ b/src/client/util/LinkManager.ts
@@ -5,6 +5,7 @@ import { listSpec } from "../../new_fields/Schema";
 import { List } from "../../new_fields/List";
 import { Id } from "../../new_fields/FieldSymbols";
 import { CurrentUserUtils } from "../../server/authentication/models/current_user_utils";
+import { Docs } from "../documents/Documents";
 
 
 /* 
@@ -35,7 +36,9 @@ export class LinkManager {
     // the linkmanagerdoc stores a list of docs representing all linkdocs in 'allLinks' and a list of strings representing all group types in 'allGroupTypes'
     // lists of strings representing the metadata keys for each group type is stored under a key that is the same as the group type 
     public get LinkManagerDoc(): Doc | undefined {
-        return FieldValue(Cast(CurrentUserUtils.UserDocument.linkManagerDoc, Doc));
+        // return FieldValue(Cast(CurrentUserUtils.UserDocument.linkManagerDoc, Doc));
+
+        return Docs.Prototypes.MainLinkDocument();
     }
 
     public getAllLinks(): Doc[] {
diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx
index 932a6375f..1b9a45f0b 100644
--- a/src/client/views/Main.tsx
+++ b/src/client/views/Main.tsx
@@ -3,9 +3,32 @@ import { Docs } from "../documents/Documents";
 import { CurrentUserUtils } from "../../server/authentication/models/current_user_utils";
 import * as ReactDOM from 'react-dom';
 import * as React from 'react';
+import { Cast } from "../../new_fields/Types";
+import { Doc, DocListCastAsync } from "../../new_fields/Doc";
+import { List } from "../../new_fields/List";
+
+let swapDocs = async () => {
+    let oldDoc = await Cast(CurrentUserUtils.UserDocument.linkManagerDoc, Doc);
+    Docs.Prototypes.MainLinkDocument().allLinks = new List<Doc>();
+    if (oldDoc) {
+        let links = await DocListCastAsync(oldDoc.allLinks);
+        // if (links && DocListCast(links)) {
+        if (links && links.length) {
+            let data = await DocListCastAsync(Docs.Prototypes.MainLinkDocument().allLinks);
+            if (data) {
+                data.push(...links);
+            }
+            else {
+                Docs.Prototypes.MainLinkDocument().allLinks = new List<Doc>(links);
+            }
+        }
+        CurrentUserUtils.UserDocument.LinkManagerDoc = undefined;
+    }
+}
 
 (async () => {
     await Docs.Prototypes.initialize();
     await CurrentUserUtils.loadCurrentUser();
+    await swapDocs();
     ReactDOM.render(<MainView />, document.getElementById('root'));
 })();
\ No newline at end of file
-- 
cgit v1.2.3-70-g09d2


From 3c6c4e6da942ef4c1e7faebdc165eb4fcaa7bee4 Mon Sep 17 00:00:00 2001
From: yipstanley <stanley_yip@brown.edu>
Date: Sat, 13 Jul 2019 16:34:42 -0400
Subject: oops

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

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

diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx
index 1b9a45f0b..971658473 100644
--- a/src/client/views/Main.tsx
+++ b/src/client/views/Main.tsx
@@ -9,7 +9,7 @@ import { List } from "../../new_fields/List";
 
 let swapDocs = async () => {
     let oldDoc = await Cast(CurrentUserUtils.UserDocument.linkManagerDoc, Doc);
-    Docs.Prototypes.MainLinkDocument().allLinks = new List<Doc>();
+    // Docs.Prototypes.MainLinkDocument().allLinks = new List<Doc>();
     if (oldDoc) {
         let links = await DocListCastAsync(oldDoc.allLinks);
         // if (links && DocListCast(links)) {
-- 
cgit v1.2.3-70-g09d2


From 4390106eb59a90283395ae5a18a0451e43166889 Mon Sep 17 00:00:00 2001
From: yipstanley <stanley_yip@brown.edu>
Date: Sat, 13 Jul 2019 17:42:26 -0400
Subject: oops duplicates

---
 src/client/views/Main.tsx | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

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

diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx
index 971658473..589542806 100644
--- a/src/client/views/Main.tsx
+++ b/src/client/views/Main.tsx
@@ -16,13 +16,14 @@ let swapDocs = async () => {
         if (links && links.length) {
             let data = await DocListCastAsync(Docs.Prototypes.MainLinkDocument().allLinks);
             if (data) {
-                data.push(...links);
+                data.push(...links.filter(i => data!.indexOf(i) === -1));
+                Docs.Prototypes.MainLinkDocument().allLinks = new List<Doc>(data.filter((i, idx) => data!.indexOf(i) === idx));
             }
             else {
                 Docs.Prototypes.MainLinkDocument().allLinks = new List<Doc>(links);
             }
         }
-        CurrentUserUtils.UserDocument.LinkManagerDoc = undefined;
+        CurrentUserUtils.UserDocument.linkManagerDoc = undefined;
     }
 }
 
-- 
cgit v1.2.3-70-g09d2


From e302a00b20ae0f44393548c7da27af60fd56c92b Mon Sep 17 00:00:00 2001
From: Tyler Schicke <tyler_schicke@brown.edu>
Date: Sun, 14 Jul 2019 18:49:42 -0400
Subject: Added whosOnline route to server

---
 src/client/DocServer.ts                            | 97 +++++++++++++---------
 src/client/views/Main.tsx                          |  5 +-
 src/client/views/MetadataEntryMenu.tsx             |  1 -
 src/debug/Repl.tsx                                 |  6 +-
 src/debug/Viewer.tsx                               | 15 ++--
 .../authentication/models/current_user_utils.ts    | 17 ++--
 src/server/index.ts                                | 45 +++++++++-
 7 files changed, 128 insertions(+), 58 deletions(-)

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

diff --git a/src/client/DocServer.ts b/src/client/DocServer.ts
index d05793ea2..6737657c8 100644
--- a/src/client/DocServer.ts
+++ b/src/client/DocServer.ts
@@ -5,6 +5,7 @@ import { Utils, emptyFunction } from '../Utils';
 import { SerializationHelper } from './util/SerializationHelper';
 import { RefField } from '../new_fields/RefField';
 import { Id, HandleUpdate } from '../new_fields/FieldSymbols';
+import { CurrentUserUtils } from '../server/authentication/models/current_user_utils';
 
 /**
  * This class encapsulates the transfer and cross-client synchronization of
@@ -21,12 +22,31 @@ import { Id, HandleUpdate } from '../new_fields/FieldSymbols';
  */
 export namespace DocServer {
     let _cache: { [id: string]: RefField | Promise<Opt<RefField>> } = {};
-    const _socket = OpenSocket(`${window.location.protocol}//${window.location.hostname}:4321`);
+    let _socket: SocketIOClient.Socket;
     // this client's distinct GUID created at initialization
-    const GUID: string = Utils.GenerateGuid();
+    let GUID: string;
     // indicates whether or not a document is currently being udpated, and, if so, its id
     let updatingId: string | undefined;
 
+    export function init(protocol: string, hostname: string, port: number, identifier: string) {
+        _cache = {};
+        GUID = identifier;
+        _socket = OpenSocket(`${protocol}//${hostname}:${port}`);
+
+        _GetRefField = _GetRefFieldImpl;
+        _GetRefFields = _GetRefFieldsImpl;
+        _CreateField = _CreateFieldImpl;
+        _UpdateField = _UpdateFieldImpl;
+
+        /**
+         * Whenever the server sends us its handshake message on our
+         * websocket, we use the above function to return the handshake.
+         */
+        Utils.AddServerHandler(_socket, MessageStore.Foo, onConnection);
+        Utils.AddServerHandler(_socket, MessageStore.UpdateField, respondToUpdate);
+        Utils.AddServerHandler(_socket, MessageStore.DeleteField, respondToDelete);
+        Utils.AddServerHandler(_socket, MessageStore.DeleteFields, respondToDelete);
+    }
     /**
      * A convenience method. Prepends the full path (i.e. http://localhost:1050) to the
      * requested extension
@@ -36,6 +56,10 @@ export namespace DocServer {
         return window.location.origin + extension;
     }
 
+    function errorFunc(): never {
+        throw new Error("Can't use DocServer without calling init first");
+    }
+
     export namespace Control {
 
         let _isReadOnly = false;
@@ -63,22 +87,16 @@ export namespace DocServer {
 
     }
 
-    export namespace Util {
-
-        /**
-         * Whenever the server sends us its handshake message on our
-         * websocket, we use the above function to return the handshake.
-         */
-        Utils.AddServerHandler(_socket, MessageStore.Foo, onConnection);
+    /**
+     * This function emits a message (with this client's
+     * unique GUID) to the server
+     * indicating that this client has connected
+     */
+    function onConnection() {
+        _socket.emit(MessageStore.Bar.Message, GUID);
+    }
 
-        /**
-         * This function emits a message (with this client's
-         * unique GUID) to the server
-         * indicating that this client has connected
-         */
-        function onConnection() {
-            _socket.emit(MessageStore.Bar.Message, GUID);
-        }
+    export namespace Util {
 
         /**
          * Emits a message to the server that wipes
@@ -98,7 +116,7 @@ export namespace DocServer {
      * the server if the document has not been cached.
      * @param id the id of the requested document
      */
-    export async function GetRefField(id: string): Promise<Opt<RefField>> {
+    const _GetRefFieldImpl = (id: string): Promise<Opt<RefField>> => {
         // an initial pass through the cache to determine whether the document needs to be fetched,
         // is already in the process of being fetched or already exists in the
         // cache
@@ -139,8 +157,14 @@ export namespace DocServer {
             return cached;
         } else {
             // CACHED => great, let's just return the cached field we have
-            return cached;
+            return Promise.resolve(cached);
         }
+    };
+
+    let _GetRefField: (id: string) => Promise<Opt<RefField>> = errorFunc;
+
+    export function GetRefField(id: string): Promise<Opt<RefField>> {
+        return _GetRefField(id);
     }
 
     /**
@@ -149,7 +173,7 @@ export namespace DocServer {
      * the server if the document has not been cached.
      * @param ids the ids that map to the reqested documents
      */
-    export async function GetRefFields(ids: string[]): Promise<{ [id: string]: Opt<RefField> }> {
+    const _GetRefFieldsImpl = async (ids: string[]): Promise<{ [id: string]: Opt<RefField> }> => {
         const requestedIds: string[] = [];
         const waitingIds: string[] = [];
         const promises: Promise<Opt<RefField>>[] = [];
@@ -245,16 +269,13 @@ export namespace DocServer {
         // argument to the caller's promise (i.e. GetRefFields(["_id1_", "_id2_", "_id3_"]).then(map => //do something with map...))
         // or it is the direct return result if the promise is awaited (i.e. let fields = await GetRefFields(["_id1_", "_id2_", "_id3_"])).
         return map;
-    }
+    };
 
-    function _UpdateFieldImpl(id: string, diff: any) {
-        if (id === updatingId) {
-            return;
-        }
-        Utils.Emit(_socket, MessageStore.UpdateField, { id, diff });
-    }
+    let _GetRefFields: (ids: string[]) => Promise<{ [id: string]: Opt<RefField> }> = errorFunc;
 
-    let _UpdateField = _UpdateFieldImpl;
+    export function GetRefFields(ids: string[]) {
+        return _GetRefFields(ids);
+    }
 
     // WRITE A NEW DOCUMENT TO THE SERVER
 
@@ -274,7 +295,7 @@ export namespace DocServer {
         Utils.Emit(_socket, MessageStore.CreateField, initialState);
     }
 
-    let _CreateField = _CreateFieldImpl;
+    let _CreateField: (field: RefField) => void = errorFunc;
 
     // NOTIFY THE SERVER OF AN UPDATE TO A DOC'S STATE
 
@@ -290,6 +311,15 @@ export namespace DocServer {
         _UpdateField(id, updatedState);
     }
 
+    function _UpdateFieldImpl(id: string, diff: any) {
+        if (id === updatingId) {
+            return;
+        }
+        Utils.Emit(_socket, MessageStore.UpdateField, { id, diff });
+    }
+
+    let _UpdateField: (id: string, diff: any) => void = errorFunc;
+
     function _respondToUpdateImpl(diff: any) {
         const id = diff.id;
         // to be valid, the Diff object must reference
@@ -355,13 +385,4 @@ export namespace DocServer {
     function respondToDelete(ids: string | string[]) {
         _respondToDelete(ids);
     }
-
-    function connected() {
-        _socket.emit(MessageStore.Bar.Message, GUID);
-    }
-
-    Utils.AddServerHandler(_socket, MessageStore.Foo, connected);
-    Utils.AddServerHandler(_socket, MessageStore.UpdateField, respondToUpdate);
-    Utils.AddServerHandler(_socket, MessageStore.DeleteField, respondToDelete);
-    Utils.AddServerHandler(_socket, MessageStore.DeleteFields, respondToDelete);
 }
\ No newline at end of file
diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx
index 589542806..80399e24b 100644
--- a/src/client/views/Main.tsx
+++ b/src/client/views/Main.tsx
@@ -6,6 +6,7 @@ import * as React from 'react';
 import { Cast } from "../../new_fields/Types";
 import { Doc, DocListCastAsync } from "../../new_fields/Doc";
 import { List } from "../../new_fields/List";
+import { DocServer } from "../DocServer";
 
 let swapDocs = async () => {
     let oldDoc = await Cast(CurrentUserUtils.UserDocument.linkManagerDoc, Doc);
@@ -28,8 +29,10 @@ let swapDocs = async () => {
 }
 
 (async () => {
+    const info = await CurrentUserUtils.loadCurrentUser();
+    DocServer.init(window.location.protocol, window.location.hostname, 4321, info.email);
     await Docs.Prototypes.initialize();
-    await CurrentUserUtils.loadCurrentUser();
+    await CurrentUserUtils.loadUserDocument(info);
     await swapDocs();
     ReactDOM.render(<MainView />, document.getElementById('root'));
 })();
\ No newline at end of file
diff --git a/src/client/views/MetadataEntryMenu.tsx b/src/client/views/MetadataEntryMenu.tsx
index 08abb9887..5ee661944 100644
--- a/src/client/views/MetadataEntryMenu.tsx
+++ b/src/client/views/MetadataEntryMenu.tsx
@@ -152,7 +152,6 @@ export class MetadataEntryMenu extends React.Component<MetadataEntryProps>{
     }
 
     render() {
-        trace();
         return (
             <div className="metadataEntry-outerDiv">
                 Key:
diff --git a/src/debug/Repl.tsx b/src/debug/Repl.tsx
index 91b711c79..4f4db13d2 100644
--- a/src/debug/Repl.tsx
+++ b/src/debug/Repl.tsx
@@ -6,6 +6,7 @@ import { CompileScript } from '../client/util/Scripting';
 import { makeInterface } from '../new_fields/Schema';
 import { ObjectField } from '../new_fields/ObjectField';
 import { RefField } from '../new_fields/RefField';
+import { DocServer } from '../client/DocServer';
 
 @observer
 class Repl extends React.Component {
@@ -63,4 +64,7 @@ class Repl extends React.Component {
     }
 }
 
-ReactDOM.render(<Repl />, document.getElementById("root"));
\ No newline at end of file
+(async function () {
+    DocServer.init(window.location.protocol, window.location.hostname, 4321, "repl");
+    ReactDOM.render(<Repl />, document.getElementById("root"));
+})();
\ No newline at end of file
diff --git a/src/debug/Viewer.tsx b/src/debug/Viewer.tsx
index f48eb696c..2b3eed154 100644
--- a/src/debug/Viewer.tsx
+++ b/src/debug/Viewer.tsx
@@ -178,9 +178,12 @@ class Viewer extends React.Component {
     }
 }
 
-ReactDOM.render((
-    <div style={{ position: "absolute", width: "100%", height: "100%" }}>
-        <Viewer />
-    </div>),
-    document.getElementById('root')
-);
\ No newline at end of file
+(async function () {
+    await DocServer.init(window.location.protocol, window.location.hostname, 4321, "viewer");
+    ReactDOM.render((
+        <div style={{ position: "absolute", width: "100%", height: "100%" }}>
+            <Viewer />
+        </div>),
+        document.getElementById('root')
+    );
+})();
\ No newline at end of file
diff --git a/src/server/authentication/models/current_user_utils.ts b/src/server/authentication/models/current_user_utils.ts
index 384c579de..e796ccb43 100644
--- a/src/server/authentication/models/current_user_utils.ts
+++ b/src/server/authentication/models/current_user_utils.ts
@@ -73,17 +73,21 @@ export class CurrentUserUtils {
 
     }
 
-    public static async loadCurrentUser(): Promise<any> {
-        let userPromise = rp.get(DocServer.prepend(RouteStore.getCurrUser)).then(response => {
+    public static loadCurrentUser() {
+        return rp.get(DocServer.prepend(RouteStore.getCurrUser)).then(response => {
             if (response) {
-                let obj = JSON.parse(response);
-                CurrentUserUtils.curr_id = obj.id as string;
-                CurrentUserUtils.curr_email = obj.email as string;
+                const result: { id: string, email: string } = JSON.parse(response);
+                return result;
             } else {
                 throw new Error("There should be a user! Why does Dash think there isn't one?");
             }
         });
-        let userDocPromise = await rp.get(DocServer.prepend(RouteStore.getUserDocumentId)).then(id => {
+    }
+
+    public static async loadUserDocument({ id, email }: { id: string, email: string }) {
+        this.curr_id = id;
+        this.curr_email = email;
+        await rp.get(DocServer.prepend(RouteStore.getUserDocumentId)).then(id => {
             if (id) {
                 return DocServer.GetRefField(id).then(async field => {
                     if (field instanceof Doc) {
@@ -108,7 +112,6 @@ export class CurrentUserUtils {
         } catch (e) {
 
         }
-        return Promise.all([userPromise, userDocPromise]);
     }
 
     /* Northstar catalog ... really just for testing so this should eventually go away */
diff --git a/src/server/index.ts b/src/server/index.ts
index 21adff9e5..9cb43bf4e 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -149,6 +149,32 @@ app.get("/search", async (req, res) => {
     res.send(results);
 });
 
+function msToTime(duration: number) {
+    let milliseconds = Math.floor((duration % 1000) / 100),
+        seconds = Math.floor((duration / 1000) % 60),
+        minutes = Math.floor((duration / (1000 * 60)) % 60),
+        hours = Math.floor((duration / (1000 * 60 * 60)) % 24);
+
+    let hoursS = (hours < 10) ? "0" + hours : hours;
+    let minutesS = (minutes < 10) ? "0" + minutes : minutes;
+    let secondsS = (seconds < 10) ? "0" + seconds : seconds;
+
+    return hoursS + ":" + minutesS + ":" + secondsS + "." + milliseconds;
+}
+
+app.get("/whosOnline", (req, res) => {
+    let users: any = { active: {}, inactive: {} };
+    const now = Date.now();
+
+    for (const user in timeMap) {
+        const time = timeMap[user];
+        const key = ((now - time) / 1000) < (60 * 5) ? "active" : "inactive";
+        users[key][user] = `Last active ${msToTime(now - time)} ago`;
+    }
+
+    res.send(users);
+});
+
 app.get("/thumbnail/:filename", (req, res) => {
     let filename = req.params.filename;
     let noExt = filename.substring(0, filename.length - ".png".length);
@@ -450,12 +476,21 @@ interface Map {
 }
 let clients: Map = {};
 
+let socketMap = new Map<SocketIO.Socket, string>();
+let timeMap: { [id: string]: number } = {};
+
 server.on("connection", function (socket: Socket) {
-    console.log("a user has connected");
+    socket.use((packet, next) => {
+        let id = socketMap.get(socket);
+        if (id) {
+            timeMap[id] = Date.now();
+        }
+        next();
+    });
 
     Utils.Emit(socket, MessageStore.Foo, "handshooken");
 
-    Utils.AddServerHandler(socket, MessageStore.Bar, barReceived);
+    Utils.AddServerHandler(socket, MessageStore.Bar, guid => barReceived(socket, guid));
     Utils.AddServerHandler(socket, MessageStore.SetField, (args) => setField(socket, args));
     Utils.AddServerHandlerCallback(socket, MessageStore.GetField, getField);
     Utils.AddServerHandlerCallback(socket, MessageStore.GetFields, getFields);
@@ -485,8 +520,10 @@ async function deleteAll() {
     await Search.Instance.clear();
 }
 
-function barReceived(guid: String) {
-    clients[guid.toString()] = new Client(guid.toString());
+function barReceived(socket: SocketIO.Socket, guid: string) {
+    clients[guid] = new Client(guid.toString());
+    console.log(`User ${guid} has connected`);
+    socketMap.set(socket, guid);
 }
 
 function getField([id, callback]: [string, (result?: Transferable) => void]) {
-- 
cgit v1.2.3-70-g09d2

