From b3d6eaa3a0b126712eae25c1b91925d030a2d900 Mon Sep 17 00:00:00 2001
From: geireann <geireann.lindfield@gmail.com>
Date: Thu, 7 Apr 2022 18:06:40 -0400
Subject: added RecordingView

---
 .../views/nodes/RecordingBox/RecordingBox.tsx      | 24 ++++++++++++++++++++++
 1 file changed, 24 insertions(+)
 create mode 100644 src/client/views/nodes/RecordingBox/RecordingBox.tsx

(limited to 'src/client/views/nodes/RecordingBox/RecordingBox.tsx')

diff --git a/src/client/views/nodes/RecordingBox/RecordingBox.tsx b/src/client/views/nodes/RecordingBox/RecordingBox.tsx
new file mode 100644
index 000000000..6d444d324
--- /dev/null
+++ b/src/client/views/nodes/RecordingBox/RecordingBox.tsx
@@ -0,0 +1,24 @@
+import { observer } from "mobx-react";
+import * as React from "react";
+import { ViewBoxBaseComponent } from "../../DocComponent";
+import { FieldView } from "../FieldView";
+import { RecordingView } from './RecordingView';
+
+
+@observer
+export class RecordingBox extends ViewBoxBaseComponent(){
+
+    public static LayoutString(fieldKey: string) { return FieldView.LayoutString(RecordingBox, fieldKey); }
+
+    private _ref: React.RefObject<HTMLDivElement> = React.createRef();
+
+    constructor(props: any) {
+        super(props);
+    }
+
+    render() {
+        return <div className="recordingBox" ref={this._ref}>
+            <RecordingView/>
+        </div>;
+    }
+}
-- 
cgit v1.2.3-70-g09d2


From a5da5e6246a6fd7500fed8b206fc3540be509eab Mon Sep 17 00:00:00 2001
From: Jenny Yu <jennyyu212@outlook.com>
Date: Fri, 22 Apr 2022 19:53:44 -0400
Subject: uploading not working (cannot find ffmpeg)

---
 package-lock.json                                  |  18 +-
 package.json                                       |   1 +
 src/client/Network.ts                              |  77 +--
 src/client/views/OverlayView.scss                  |  70 +--
 src/client/views/OverlayView.tsx                   | 387 ++++++------
 .../views/nodes/RecordingBox/RecordingBox.tsx      |  37 +-
 .../views/nodes/RecordingBox/RecordingView.scss    |   5 +-
 .../views/nodes/RecordingBox/RecordingView.tsx     |  28 +-
 src/client/views/nodes/trails/PresElementBox.tsx   | 699 +++++++++++----------
 src/server/ApiManagers/UploadManager.ts            | 534 ++++++++--------
 10 files changed, 957 insertions(+), 899 deletions(-)

(limited to 'src/client/views/nodes/RecordingBox/RecordingBox.tsx')

diff --git a/package-lock.json b/package-lock.json
index 01763e9b6..0f045c546 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -6535,6 +6535,14 @@
                 "pend": "~1.2.0"
             }
         },
+        "ffmpeg": {
+            "version": "0.0.4",
+            "resolved": "https://registry.npmjs.org/ffmpeg/-/ffmpeg-0.0.4.tgz",
+            "integrity": "sha1-HEYN+OfaUSf2LO70v6BsWciWMMs=",
+            "requires": {
+                "when": ">= 0.0.1"
+            }
+        },
         "file-loader": {
             "version": "3.0.1",
             "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-3.0.1.tgz",
@@ -7961,13 +7969,14 @@
             "resolved": "https://registry.npmjs.org/image-size-stream/-/image-size-stream-1.1.0.tgz",
             "integrity": "sha1-Ivou2mbG31AQh0bacUkmSy0l+Gs=",
             "requires": {
+                "image-size": "github:netroy/image-size#da2c863807a3e9602617bdd357b0de3ab4a064c1",
                 "readable-stream": "^1.0.33",
                 "tryit": "^1.0.1"
             },
             "dependencies": {
                 "image-size": {
-                    "version": "git+https://github.com/netroy/image-size.git#da2c863807a3e9602617bdd357b0de3ab4a064c1",
-                    "from": "git+https://github.com/netroy/image-size.git#da2c863807a3e9602617bdd357b0de3ab4a064c1"
+                    "version": "github:netroy/image-size#da2c863807a3e9602617bdd357b0de3ab4a064c1",
+                    "from": "github:netroy/image-size#da2c863807a3e9602617bdd357b0de3ab4a064c1"
                 },
                 "isarray": {
                     "version": "0.0.1",
@@ -19577,6 +19586,11 @@
                 "webidl-conversions": "^3.0.0"
             }
         },
+        "when": {
+            "version": "3.7.8",
+            "resolved": "https://registry.npmjs.org/when/-/when-3.7.8.tgz",
+            "integrity": "sha1-xxMLan6gRpPoQs3J56Hyqjmjn4I="
+        },
         "which": {
             "version": "1.3.1",
             "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
diff --git a/package.json b/package.json
index 35aa2ef92..6185bc0d7 100644
--- a/package.json
+++ b/package.json
@@ -175,6 +175,7 @@
         "express-session": "^1.17.2",
         "express-validator": "^5.3.1",
         "expressjs": "^1.0.1",
+        "ffmpeg": "0.0.4",
         "file-saver": "^2.0.5",
         "find-in-files": "^0.5.0",
         "fit-curve": "^0.1.7",
diff --git a/src/client/Network.ts b/src/client/Network.ts
index bf2918734..58f356c90 100644
--- a/src/client/Network.ts
+++ b/src/client/Network.ts
@@ -4,46 +4,49 @@ import { Upload } from "../server/SharedMediaTypes";
 
 export namespace Networking {
 
-    export async function FetchFromServer(relativeRoute: string) {
-        return (await fetch(relativeRoute)).text();
-    }
+      export async function FetchFromServer(relativeRoute: string) {
+            return (await fetch(relativeRoute)).text();
+      }
 
-    export async function PostToServer(relativeRoute: string, body?: any) {
-        const options = {
-            uri: Utils.prepend(relativeRoute),
-            method: "POST",
-            body,
-            json: true
-        };
-        return requestPromise.post(options);
-    }
+      export async function PostToServer(relativeRoute: string, body?: any) {
+            const options = {
+                  uri: Utils.prepend(relativeRoute),
+                  method: "POST",
+                  body,
+                  json: true
+            };
+            return requestPromise.post(options);
+      }
 
-    export async function UploadFilesToServer<T extends Upload.FileInformation = Upload.FileInformation>(files: File | File[]): Promise<Upload.FileResponse<T>[]> {
-        const formData = new FormData();
-        if (Array.isArray(files)) {
-            if (!files.length) {
-                return [];
+      export async function UploadFilesToServer<T extends Upload.FileInformation = Upload.FileInformation>(files: File | File[]): Promise<Upload.FileResponse<T>[]> {
+            console.log(files)
+            const formData = new FormData();
+            if (Array.isArray(files)) {
+                  if (!files.length) {
+                        return [];
+                  }
+                  files.forEach(file => formData.append(Utils.GenerateGuid(), file));
+            } else {
+                  formData.append(Utils.GenerateGuid(), files);
             }
-            files.forEach(file => formData.append(Utils.GenerateGuid(), file));
-        } else {
-            formData.append(Utils.GenerateGuid(), files);
-        }
-        const parameters = {
-            method: 'POST',
-            body: formData
-        };
-        const response = await fetch("/uploadFormData", parameters);
-        return response.json();
-    }
+            const parameters = {
+                  method: 'POST',
+                  body: formData
+            };
+            const response = await fetch("/uploadFormData", parameters);
+            console.log(response)
+            // console.log(response.json())
+            return response.json();
+      }
 
-    export async function UploadYoutubeToServer<T extends Upload.FileInformation = Upload.FileInformation>(videoId: string): Promise<Upload.FileResponse<T>[]> {
-        const parameters = {
-            method: 'POST',
-            body: JSON.stringify({ videoId }),
-            json: true
-        };
-        const response = await fetch("/uploadYoutubeVideo", parameters);
-        return response.json();
-    }
+      export async function UploadYoutubeToServer<T extends Upload.FileInformation = Upload.FileInformation>(videoId: string): Promise<Upload.FileResponse<T>[]> {
+            const parameters = {
+                  method: 'POST',
+                  body: JSON.stringify({ videoId }),
+                  json: true
+            };
+            const response = await fetch("/uploadYoutubeVideo", parameters);
+            return response.json();
+      }
 
 }
\ No newline at end of file
diff --git a/src/client/views/OverlayView.scss b/src/client/views/OverlayView.scss
index 02ea5e53b..d6a6e4301 100644
--- a/src/client/views/OverlayView.scss
+++ b/src/client/views/OverlayView.scss
@@ -1,59 +1,59 @@
 .overlayView {
-    position: absolute;
-    pointer-events: none;
-    top: 0;
-    width: 100vw;
-    height: 100vh;
-    /* background-color: pink; */
-    z-index: 100000;
+      position: absolute;
+      pointer-events: none;
+      top: 0;
+      width: 100vw;
+      height: 100vh;
+      /* background-color: pink; */
+      z-index: 100000;
 }
 
 .overlayWindow-outerDiv {
-    border-radius: 5px;
-    overflow: hidden;
-    display: flex;
-    flex-direction: column;
-    top: 0;
-    left: 0;
+      border-radius: 5px;
+      overflow: hidden;
+      display: flex;
+      flex-direction: column;
+      top: 0;
+      left: 0;
 }
 
 .overlayWindow-outerDiv,
 .overlayView-wrapperDiv {
-    position: absolute;
-    z-index: 1;
+      position: absolute;
+      z-index: 1;
 }
 
 .overlayWindow-titleBar {
-    flex: 0 1 30px;
-    background: darkslategray;
-    color: whitesmoke;
-    text-align: center;
-    cursor: move;
+      flex: 0 1 30px;
+      background: darkslategray;
+      color: whitesmoke;
+      text-align: center;
+      cursor: move;
 }
 
 .overlayWindow-content {
-    flex: 1 1 auto;
-    display: flex;
-    flex-direction: column;
+      flex: 1 1 auto;
+      display: flex;
+      flex-direction: column;
 }
 
 .overlayWindow-closeButton {
-    float: right;
-    height: 30px;
-    width: 30px;
+      float: right;
+      height: 30px;
+      width: 30px;
 }
 
 .overlayWindow-resizeDragger {
-    background-color: rgb(0, 0, 0);
-    position: absolute;
-    right: 0px;
-    bottom: 0px;
-    width: 10px;
-    height: 10px;
-    cursor: nwse-resize;
+      background-color: rgb(0, 0, 0);
+      position: absolute;
+      right: 0px;
+      bottom: 0px;
+      width: 10px;
+      height: 10px;
+      cursor: nwse-resize;
 }
 
 .overlayView-doc {
-    z-index: 9002; //so that it appears above chroma 
-    position: absolute;
+      z-index: 9002; //so that it appears above chroma 
+      position: absolute;
 }
\ No newline at end of file
diff --git a/src/client/views/OverlayView.tsx b/src/client/views/OverlayView.tsx
index 0f51cf9b2..806abb990 100644
--- a/src/client/views/OverlayView.tsx
+++ b/src/client/views/OverlayView.tsx
@@ -20,212 +20,213 @@ import { DefaultStyleProvider } from "./StyleProvider";
 export type OverlayDisposer = () => void;
 
 export type OverlayElementOptions = {
-    x: number;
-    y: number;
-    width?: number;
-    height?: number;
-    title?: string;
+      x: number;
+      y: number;
+      width?: number;
+      height?: number;
+      title?: string;
 };
 
 export interface OverlayWindowProps {
-    children: JSX.Element;
-    overlayOptions: OverlayElementOptions;
-    onClick: () => void;
+      children: JSX.Element;
+      overlayOptions: OverlayElementOptions;
+      onClick: () => void;
 }
 
 @observer
 export class OverlayWindow extends React.Component<OverlayWindowProps> {
-    @observable x: number;
-    @observable y: number;
-    @observable width: number;
-    @observable height: number;
-    constructor(props: OverlayWindowProps) {
-        super(props);
-
-        const opts = props.overlayOptions;
-        this.x = opts.x;
-        this.y = opts.y;
-        this.width = opts.width || 200;
-        this.height = opts.height || 200;
-    }
-
-    onPointerDown = (_: React.PointerEvent) => {
-        document.removeEventListener("pointermove", this.onPointerMove);
-        document.removeEventListener("pointerup", this.onPointerUp);
-        document.addEventListener("pointermove", this.onPointerMove);
-        document.addEventListener("pointerup", this.onPointerUp);
-    }
-
-    onResizerPointerDown = (_: React.PointerEvent) => {
-        document.removeEventListener("pointermove", this.onResizerPointerMove);
-        document.removeEventListener("pointerup", this.onResizerPointerUp);
-        document.addEventListener("pointermove", this.onResizerPointerMove);
-        document.addEventListener("pointerup", this.onResizerPointerUp);
-    }
-
-    @action
-    onPointerMove = (e: PointerEvent) => {
-        this.x += e.movementX;
-        this.x = Math.max(Math.min(this.x, window.innerWidth - this.width), 0);
-        this.y += e.movementY;
-        this.y = Math.max(Math.min(this.y, window.innerHeight - this.height), 0);
-    }
-
-    @action
-    onResizerPointerMove = (e: PointerEvent) => {
-        this.width += e.movementX;
-        this.width = Math.max(this.width, 30);
-        this.height += e.movementY;
-        this.height = Math.max(this.height, 30);
-    }
-
-    onPointerUp = (e: PointerEvent) => {
-        document.removeEventListener("pointermove", this.onPointerMove);
-        document.removeEventListener("pointerup", this.onPointerUp);
-    }
-
-    onResizerPointerUp = (e: PointerEvent) => {
-        document.removeEventListener("pointermove", this.onResizerPointerMove);
-        document.removeEventListener("pointerup", this.onResizerPointerUp);
-    }
-
-    render() {
-        return (
-            <div className="overlayWindow-outerDiv" style={{ transform: `translate(${this.x}px, ${this.y}px)`, width: this.width, height: this.height }}>
-                <div className="overlayWindow-titleBar" onPointerDown={this.onPointerDown} >
-                    {this.props.overlayOptions.title || "Untitled"}
-                    <button onClick={this.props.onClick} className="overlayWindow-closeButton">X</button>
-                </div>
-                <div className="overlayWindow-content">
-                    {this.props.children}
-                </div>
-                <div className="overlayWindow-resizeDragger" onPointerDown={this.onResizerPointerDown}></div>
-            </div>
-        );
-    }
+      @observable x: number;
+      @observable y: number;
+      @observable width: number;
+      @observable height: number;
+      constructor(props: OverlayWindowProps) {
+            super(props);
+
+            const opts = props.overlayOptions;
+            this.x = opts.x;
+            this.y = opts.y;
+            this.width = opts.width || 200;
+            this.height = opts.height || 200;
+      }
+
+      onPointerDown = (_: React.PointerEvent) => {
+            document.removeEventListener("pointermove", this.onPointerMove);
+            document.removeEventListener("pointerup", this.onPointerUp);
+            document.addEventListener("pointermove", this.onPointerMove);
+            document.addEventListener("pointerup", this.onPointerUp);
+      }
+
+      onResizerPointerDown = (_: React.PointerEvent) => {
+            document.removeEventListener("pointermove", this.onResizerPointerMove);
+            document.removeEventListener("pointerup", this.onResizerPointerUp);
+            document.addEventListener("pointermove", this.onResizerPointerMove);
+            document.addEventListener("pointerup", this.onResizerPointerUp);
+      }
+
+      @action
+      onPointerMove = (e: PointerEvent) => {
+            this.x += e.movementX;
+            this.x = Math.max(Math.min(this.x, window.innerWidth - this.width), 0);
+            this.y += e.movementY;
+            this.y = Math.max(Math.min(this.y, window.innerHeight - this.height), 0);
+      }
+
+      @action
+      onResizerPointerMove = (e: PointerEvent) => {
+            this.width += e.movementX;
+            this.width = Math.max(this.width, 30);
+            this.height += e.movementY;
+            this.height = Math.max(this.height, 30);
+      }
+
+      onPointerUp = (e: PointerEvent) => {
+            document.removeEventListener("pointermove", this.onPointerMove);
+            document.removeEventListener("pointerup", this.onPointerUp);
+      }
+
+      onResizerPointerUp = (e: PointerEvent) => {
+            document.removeEventListener("pointermove", this.onResizerPointerMove);
+            document.removeEventListener("pointerup", this.onResizerPointerUp);
+      }
+
+      render() {
+            return (
+                  <div className="overlayWindow-outerDiv" style={{ transform: `translate(${this.x}px, ${this.y}px)`, width: this.width, height: this.height }}>
+                        <div className="overlayWindow-titleBar" onPointerDown={this.onPointerDown} >
+                              {this.props.overlayOptions.title || "Untitled"}
+                              <button onClick={this.props.onClick} className="overlayWindow-closeButton">X</button>
+                        </div>
+                        <div className="overlayWindow-content">
+                              {this.props.children}
+                        </div>
+                        <div className="overlayWindow-resizeDragger" onPointerDown={this.onResizerPointerDown}></div>
+                  </div>
+            );
+      }
 }
 
 @observer
 export class OverlayView extends React.Component {
-    public static Instance: OverlayView;
-    @observable.shallow
-    private _elements: JSX.Element[] = [];
-
-    constructor(props: any) {
-        super(props);
-        if (!OverlayView.Instance) {
-            OverlayView.Instance = this;
-        }
-    }
-
-    @action
-    addElement(ele: JSX.Element, options: OverlayElementOptions): OverlayDisposer {
-        const remove = action(() => {
-            const index = this._elements.indexOf(ele);
-            if (index !== -1) this._elements.splice(index, 1);
-        });
-        ele = <div key={Utils.GenerateGuid()} className="overlayView-wrapperDiv" style={{
-            transform: `translate(${options.x}px, ${options.y}px)`,
-            width: options.width,
-            height: options.height,
-            top: 0,
-            left: 0
-        }}>{ele}</div>;
-        this._elements.push(ele);
-        return remove;
-    }
-
-    @action
-    addWindow(contents: JSX.Element, options: OverlayElementOptions): OverlayDisposer {
-        const remove = action(() => {
-            const index = this._elements.indexOf(contents);
-            if (index !== -1) this._elements.splice(index, 1);
-        });
-        contents = <OverlayWindow onClick={remove} key={Utils.GenerateGuid()} overlayOptions={options}>{contents}</OverlayWindow>;
-        this._elements.push(contents);
-        return remove;
-    }
-
-
-    @computed get overlayDocs() {
-        return CurrentUserUtils.OverlayDocs?.map(d => {
-            let offsetx = 0, offsety = 0;
-            const dref = React.createRef<HTMLDivElement>();
-            const onPointerMove = action((e: PointerEvent, down: number[]) => {
-                if (e.buttons === 1) {
-                    d.x = e.clientX + offsetx;
-                    d.y = e.clientY + offsety;
-                }
-                if (e.metaKey) {
-                    const dragData = new DragManager.DocumentDragData([d]);
-                    dragData.offset = [-offsetx, -offsety];
-                    dragData.dropAction = "move";
-                    dragData.removeDocument = (doc: Doc | Doc[]) => {
-                        const docs = (doc instanceof Doc) ? [doc] : doc;
-                        docs.forEach(d => Doc.RemoveDocFromList(Cast(Doc.UserDoc().myOverlayDocs, Doc, null), "data", d));
-                        return true;
-                    };
-                    dragData.moveDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (doc: Doc | Doc[]) => boolean): boolean => {
-                        return dragData.removeDocument!(doc) ? addDocument(doc) : false;
-                    };
-                    DragManager.StartDocumentDrag([dref.current!], dragData, down[0], down[1]);
-                    return true;
-                }
-                return false;
+      public static Instance: OverlayView;
+      @observable.shallow
+      private _elements: JSX.Element[] = [];
+
+      constructor(props: any) {
+            super(props);
+            if (!OverlayView.Instance) {
+                  OverlayView.Instance = this;
+            }
+      }
+
+      @action
+      addElement(ele: JSX.Element, options: OverlayElementOptions): OverlayDisposer {
+            const remove = action(() => {
+                  const index = this._elements.indexOf(ele);
+                  if (index !== -1) this._elements.splice(index, 1);
             });
-
-            const onPointerDown = (e: React.PointerEvent) => {
-                setupMoveUpEvents(this, e, onPointerMove, emptyFunction, emptyFunction);
-                offsetx = NumCast(d.x) - e.clientX;
-                offsety = NumCast(d.y) - e.clientY;
-            };
-            return <div className="overlayView-doc" ref={dref} key={d[Id]} onPointerDown={onPointerDown} style={{ top: d.type === 'presentation' ? 0 : undefined, width: NumCast(d._width), height: NumCast(d._height), transform: `translate(${d.x}px, ${d.y}px)` }}>
-                <DocumentView
-                    Document={d}
-                    rootSelected={returnTrue}
-                    bringToFront={emptyFunction}
-                    addDocument={undefined}
-                    removeDocument={undefined}
-                    PanelWidth={returnOne}
-                    PanelHeight={returnOne}
-                    ScreenToLocalTransform={Transform.Identity}
-                    renderDepth={1}
-                    isDocumentActive={returnTrue}
-                    isContentActive={emptyFunction}
-                    whenChildContentsActiveChanged={emptyFunction}
-                    focus={DocUtils.DefaultFocus}
-                    styleProvider={DefaultStyleProvider}
-                    layerProvider={undefined}
-                    docViewPath={returnEmptyDoclist}
-                    addDocTab={returnFalse}
-                    pinToPres={emptyFunction}
-                    docFilters={returnEmptyFilter}
-                    docRangeFilters={returnEmptyFilter}
-                    searchFilterDocs={returnEmptyDoclist}
-                    ContainingCollectionView={undefined}
-                    ContainingCollectionDoc={undefined} />
-            </div>;
-        });
-    }
-
-    public static ShowSpinner() {
-        return OverlayView.Instance.addElement(<ReactLoading type="spinningBubbles" color="green" height={250} width={250} />, { x: 300, y: 200 });
-    }
-
-
-    render() {
-        return (
-            <div className="overlayView" id="overlayView">
-                <div>
-                    {this._elements}
-                </div>
-                <CollectionFreeFormLinksView key="freeformLinks" />
-                {this.overlayDocs}
-            </div>
-        );
-    }
+            ele = <div key={Utils.GenerateGuid()} className="overlayView-wrapperDiv" style={{
+                  transform: `translate(${options.x}px, ${options.y}px)`,
+                  width: options.width,
+                  height: options.height,
+                  top: 0,
+                  left: 0
+            }}>{ele}</div>;
+            this._elements.push(ele);
+            return remove;
+      }
+
+      @action
+      addWindow(contents: JSX.Element, options: OverlayElementOptions): OverlayDisposer {
+            const remove = action(() => {
+                  const index = this._elements.indexOf(contents);
+                  if (index !== -1) this._elements.splice(index, 1);
+            });
+            contents = <OverlayWindow onClick={remove} key={Utils.GenerateGuid()} overlayOptions={options}>{contents}</OverlayWindow>;
+            this._elements.push(contents);
+            return remove;
+      }
+
+
+      @computed get overlayDocs() {
+            return CurrentUserUtils.OverlayDocs?.map(d => {
+                  let offsetx = 0, offsety = 0;
+                  const dref = React.createRef<HTMLDivElement>();
+                  const onPointerMove = action((e: PointerEvent, down: number[]) => {
+                        if (e.buttons === 1) {
+                              d.x = e.clientX + offsetx;
+                              d.y = e.clientY + offsety;
+                        }
+                        if (e.metaKey) {
+                              const dragData = new DragManager.DocumentDragData([d]);
+                              dragData.offset = [-offsetx, -offsety];
+                              dragData.dropAction = "move";
+                              dragData.removeDocument = (doc: Doc | Doc[]) => {
+                                    const docs = (doc instanceof Doc) ? [doc] : doc;
+                                    docs.forEach(d => Doc.RemoveDocFromList(Cast(Doc.UserDoc().myOverlayDocs, Doc, null), "data", d));
+                                    return true;
+                              };
+                              dragData.moveDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (doc: Doc | Doc[]) => boolean): boolean => {
+                                    return dragData.removeDocument!(doc) ? addDocument(doc) : false;
+                              };
+                              DragManager.StartDocumentDrag([dref.current!], dragData, down[0], down[1]);
+                              return true;
+                        }
+                        return false;
+                  });
+
+                  const onPointerDown = (e: React.PointerEvent) => {
+                        setupMoveUpEvents(this, e, onPointerMove, emptyFunction, emptyFunction);
+                        offsetx = NumCast(d.x) - e.clientX;
+                        offsety = NumCast(d.y) - e.clientY;
+                  };
+
+                  return <div className="overlayView-doc" ref={dref} key={d[Id]} onPointerDown={onPointerDown} style={{ top: d.type === 'presentation' ? 0 : undefined, width: NumCast(d._width), height: NumCast(d._height), transform: `translate(${d.x}px, ${d.y}px)` }}>
+                        <DocumentView
+                              Document={d}
+                              rootSelected={returnTrue}
+                              bringToFront={emptyFunction}
+                              addDocument={undefined}
+                              removeDocument={undefined}
+                              PanelWidth={returnOne}
+                              PanelHeight={returnOne}
+                              ScreenToLocalTransform={Transform.Identity}
+                              renderDepth={1}
+                              isDocumentActive={returnTrue}
+                              isContentActive={emptyFunction}
+                              whenChildContentsActiveChanged={emptyFunction}
+                              focus={DocUtils.DefaultFocus}
+                              styleProvider={DefaultStyleProvider}
+                              layerProvider={undefined}
+                              docViewPath={returnEmptyDoclist}
+                              addDocTab={returnFalse}
+                              pinToPres={emptyFunction}
+                              docFilters={returnEmptyFilter}
+                              docRangeFilters={returnEmptyFilter}
+                              searchFilterDocs={returnEmptyDoclist}
+                              ContainingCollectionView={undefined}
+                              ContainingCollectionDoc={undefined} />
+                  </div>;
+            });
+      }
+
+      public static ShowSpinner() {
+            return OverlayView.Instance.addElement(<ReactLoading type="spinningBubbles" color="green" height={250} width={250} />, { x: 300, y: 200 });
+      }
+
+
+      render() {
+            return (
+                  <div className="overlayView" id="overlayView">
+                        <div>
+                              {this._elements}
+                        </div>
+                        <CollectionFreeFormLinksView key="freeformLinks" />
+                        {this.overlayDocs}
+                  </div>
+            );
+      }
 }
 // bcz: ugh ... want to be able to pass ScriptingRepl as tag argument, but that doesn't seem to work.. runtime error
 ScriptingGlobals.add(function addOverlayWindow(type: string, options: OverlayElementOptions) {
-    OverlayView.Instance.addWindow(<ScriptingRepl />, options);
+      OverlayView.Instance.addWindow(<ScriptingRepl />, options);
 });
\ No newline at end of file
diff --git a/src/client/views/nodes/RecordingBox/RecordingBox.tsx b/src/client/views/nodes/RecordingBox/RecordingBox.tsx
index 6d444d324..1e17e2ddd 100644
--- a/src/client/views/nodes/RecordingBox/RecordingBox.tsx
+++ b/src/client/views/nodes/RecordingBox/RecordingBox.tsx
@@ -1,24 +1,39 @@
+import { action, observable } from "mobx";
 import { observer } from "mobx-react";
 import * as React from "react";
+import { AudioField } from "../../../../fields/URLField";
+import { Upload } from "../../../../server/SharedMediaTypes";
 import { ViewBoxBaseComponent } from "../../DocComponent";
 import { FieldView } from "../FieldView";
+import { VideoBox } from "../VideoBox";
 import { RecordingView } from './RecordingView';
 
 
 @observer
-export class RecordingBox extends ViewBoxBaseComponent(){
+export class RecordingBox extends ViewBoxBaseComponent() {
 
-    public static LayoutString(fieldKey: string) { return FieldView.LayoutString(RecordingBox, fieldKey); }
+      public static LayoutString(fieldKey: string) { return FieldView.LayoutString(RecordingBox, fieldKey); }
 
-    private _ref: React.RefObject<HTMLDivElement> = React.createRef();
+      private _ref: React.RefObject<HTMLDivElement> = React.createRef();
 
-    constructor(props: any) {
-        super(props);
-    }
+      constructor(props: any) {
+            super(props);
+      }
 
-    render() {
-        return <div className="recordingBox" ref={this._ref}>
-            <RecordingView/>
-        </div>;
-    }
+      @observable result: Upload.FileInformation | undefined = undefined
+
+      @action
+      setResult = (info: Upload.FileInformation) => {
+            console.log("Setting result to " + info)
+            this.result = info
+            console.log(this.result.accessPaths.agnostic.client)
+            this.props.Document[this.fieldKey] = new AudioField(this.result.accessPaths.agnostic.client);
+      }
+
+      render() {
+            return <div className="recordingBox" ref={this._ref}>
+                  {!this.result ? <RecordingView setResult={this.setResult} /> :
+                        <p>video box</p>}
+            </div>;
+      }
 }
diff --git a/src/client/views/nodes/RecordingBox/RecordingView.scss b/src/client/views/nodes/RecordingBox/RecordingView.scss
index e4d971d51..1fea231b7 100644
--- a/src/client/views/nodes/RecordingBox/RecordingView.scss
+++ b/src/client/views/nodes/RecordingBox/RecordingView.scss
@@ -15,6 +15,7 @@ button {
       height: 100%;
       width: 100%;
       display: flex;
+      pointer-events: all;
 }
 
 .video-wrapper {
@@ -157,8 +158,8 @@ button {
             background-color: red;
             border: 0px;
             border-radius: 10%;
-            height: 80%;
-            width: 80%;
+            height: 70%;
+            width: 70%;
             align-self: center;
             margin: 0;
 
diff --git a/src/client/views/nodes/RecordingBox/RecordingView.tsx b/src/client/views/nodes/RecordingBox/RecordingView.tsx
index 905f87a1a..0d24f3c74 100644
--- a/src/client/views/nodes/RecordingBox/RecordingView.tsx
+++ b/src/client/views/nodes/RecordingBox/RecordingView.tsx
@@ -5,6 +5,8 @@ import { ProgressBar } from "./ProgressBar"
 import { MdBackspace } from 'react-icons/md';
 import { FaCheckCircle } from 'react-icons/fa';
 import { IconContext } from "react-icons";
+import { Networking } from '../../../Network';
+import { Upload } from '../../../../server/SharedMediaTypes';
 
 
 enum RecordingStatus {
@@ -18,9 +20,14 @@ interface VideoSegment {
       endTime: number
 }
 
+interface IRecordingViewProps {
+      setResult: (info: Upload.FileInformation) => void
+}
+
 const MAXTIME = 1000;
 
-export function RecordingView() {
+export function RecordingView(props: IRecordingViewProps) {
+      const { setResult } = props
 
       const [recording, setRecording] = useState(false);
       const recordingTimerRef = useRef<number>(0);
@@ -68,18 +75,27 @@ export function RecordingView() {
                         type: 'video/webm'
                   })
                   const blobUrl = URL.createObjectURL(blob)
+                  const videoFile = new File([blob], "video", { lastModified: new Date().getDate() });
 
                   videoElementRef.current!.srcObject = null
                   videoElementRef.current!.src = blobUrl
                   videoElementRef.current!.muted = false
 
-                  // upload data
-                  //       const [{ result }] = await Networking.UploadFilesToServer(e.data);
-                  // console.log("Data result", result);
-                  // if (!(result instanceof Error)) {
-                  //     this.props.Document[this.fieldKey] = new AudioField(result.accessPaths.agnostic.client);
+                  // const uploadVideo = async () => {
+                  //       const [{ result }] = await Networking.UploadFilesToServer(videoFile);
+                  //       console.log("upload result", result);
+                  //       if (!(result instanceof Error)) {
+                  //             setResult(result)
+                  //       }
                   // }
 
+                  Networking.UploadFilesToServer(allVideoChunks)
+                        .then((data) => {
+                              console.log(data)
+                        })
+                  // uploadVideo()
+
+
                   // change to one recording box
             }
 
diff --git a/src/client/views/nodes/trails/PresElementBox.tsx b/src/client/views/nodes/trails/PresElementBox.tsx
index 766676e95..aa7622736 100644
--- a/src/client/views/nodes/trails/PresElementBox.tsx
+++ b/src/client/views/nodes/trails/PresElementBox.tsx
@@ -30,376 +30,383 @@ import { List } from "../../../../fields/List";
  */
 @observer
 export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
-    public static LayoutString(fieldKey: string) { return FieldView.LayoutString(PresElementBox, fieldKey); }
-    _heightDisposer: IReactionDisposer | undefined;
+      public static LayoutString(fieldKey: string) { return FieldView.LayoutString(PresElementBox, fieldKey); }
+      _heightDisposer: IReactionDisposer | undefined;
 
-    @observable _dragging = false;
-    // these fields are conditionally computed fields on the layout document that take this document as a parameter
-    @computed get indexInPres() { return Number(this.lookupField("indexInPres")); }  // the index field is where this document is in the presBox display list (since this value is different for each presentation element, the value can't be stored on the layout template which is used by all display elements)
-    @computed get collapsedHeight() { return Number(this.lookupField("presCollapsedHeight")); } // the collapsed height changes depending on the state of the presBox.  We could store this on the presentation element template if it's used by only one presentation - but if it's shared by multiple, then this value must be looked up
-    @computed get presStatus() { return StrCast(this.lookupField("presStatus")); }
-    @computed get itemIndex() { return NumCast(this.lookupField("_itemIndex")); }
-    @computed get presBox() { return Cast(this.lookupField("presBox"), Doc, null); }
-    @computed get targetDoc() { return Cast(this.rootDoc.presentationTargetDoc, Doc, null) || this.rootDoc; }
+      @observable _dragging = false;
+      // these fields are conditionally computed fields on the layout document that take this document as a parameter
+      @computed get indexInPres() { return Number(this.lookupField("indexInPres")); }  // the index field is where this document is in the presBox display list (since this value is different for each presentation element, the value can't be stored on the layout template which is used by all display elements)
+      @computed get collapsedHeight() { return Number(this.lookupField("presCollapsedHeight")); } // the collapsed height changes depending on the state of the presBox.  We could store this on the presentation element template if it's used by only one presentation - but if it's shared by multiple, then this value must be looked up
+      @computed get presStatus() { return StrCast(this.lookupField("presStatus")); }
+      @computed get itemIndex() { return NumCast(this.lookupField("_itemIndex")); }
+      @computed get presBox() { return Cast(this.lookupField("presBox"), Doc, null); }
+      @computed get targetDoc() { return Cast(this.rootDoc.presentationTargetDoc, Doc, null) || this.rootDoc; }
 
-    componentDidMount() {
-        this.layoutDoc.hideLinkButton = true;
-        this._heightDisposer = reaction(() => [this.rootDoc.presExpandInlineButton, this.collapsedHeight],
-            params => this.layoutDoc._height = NumCast(params[1]) + (Number(params[0]) ? 100 : 0), { fireImmediately: true });
-    }
-    componentWillUnmount() {
-        this._heightDisposer?.();
-    }
+      @observable isShowingVideo = false;
+      @action setIsShowingVideo(shown: boolean) {
+            this.isShowingVideo = shown
+      }
 
-    /**
-     * Returns a local transformed coordinate array for given coordinates.
-     */
-    ScreenToLocalListTransform = (xCord: number, yCord: number) => [xCord, yCord];
+      componentDidMount() {
+            this.layoutDoc.hideLinkButton = true;
+            this._heightDisposer = reaction(() => [this.rootDoc.presExpandInlineButton, this.collapsedHeight],
+                  params => this.layoutDoc._height = NumCast(params[1]) + (Number(params[0]) ? 100 : 0), { fireImmediately: true });
+      }
+      componentWillUnmount() {
+            this._heightDisposer?.();
+      }
 
-    @action
-    presExpandDocumentClick = () => {
-        this.rootDoc.presExpandInlineButton = !this.rootDoc.presExpandInlineButton;
-    }
+      /**
+       * Returns a local transformed coordinate array for given coordinates.
+       */
+      ScreenToLocalListTransform = (xCord: number, yCord: number) => [xCord, yCord];
 
-    embedHeight = (): number => 97;
-    // embedWidth = () => this.props.PanelWidth();
-    // embedHeight = () => Math.min(this.props.PanelWidth() - 20, this.props.PanelHeight() - this.collapsedHeight);
-    embedWidth = (): number => this.props.PanelWidth() - 35;
-    styleProvider = (doc: (Doc | undefined), props: Opt<DocumentViewProps>, property: string): any => {
-        if (property === StyleProp.Opacity) return 1;
-        return this.props.styleProvider?.(doc, props, property);
-    }
-    /**
-     * The function that is responsible for rendering a preview or not for this
-     * presentation element.
-     */
-    @computed get renderEmbeddedInline() {
-        return !this.rootDoc.presExpandInlineButton || !this.targetDoc ? (null) :
-            <div className="presItem-embedded" style={{ height: this.embedHeight(), width: this.embedWidth() }}>
-                <DocumentView
-                    Document={this.targetDoc}
-                    DataDoc={this.targetDoc[DataSym] !== this.targetDoc && this.targetDoc[DataSym]}
-                    styleProvider={this.styleProvider}
-                    layerProvider={this.props.layerProvider}
-                    docViewPath={returnEmptyDoclist}
-                    rootSelected={returnTrue}
-                    addDocument={returnFalse}
-                    removeDocument={returnFalse}
-                    isContentActive={this.props.isContentActive}
-                    addDocTab={returnFalse}
-                    pinToPres={returnFalse}
-                    PanelWidth={this.embedWidth}
-                    PanelHeight={this.embedHeight}
-                    ScreenToLocalTransform={Transform.Identity}
-                    moveDocument={this.props.moveDocument!}
-                    renderDepth={this.props.renderDepth + 1}
-                    focus={DocUtils.DefaultFocus}
-                    whenChildContentsActiveChanged={returnFalse}
-                    bringToFront={returnFalse}
-                    docFilters={this.props.docFilters}
-                    docRangeFilters={this.props.docRangeFilters}
-                    searchFilterDocs={this.props.searchFilterDocs}
-                    ContainingCollectionView={undefined}
-                    ContainingCollectionDoc={undefined}
-                    hideLinkButton={true}
-                />
-                <div className="presItem-embeddedMask" />
-            </div>;
-    }
+      @action
+      presExpandDocumentClick = () => {
+            this.rootDoc.presExpandInlineButton = !this.rootDoc.presExpandInlineButton;
+      }
 
-    @computed get duration() {
-        let durationInS: number;
-        if (this.rootDoc.type === DocumentType.AUDIO || this.rootDoc.type === DocumentType.VID) { durationInS = NumCast(this.rootDoc.presEndTime) - NumCast(this.rootDoc.presStartTime); durationInS = Math.round(durationInS * 10) / 10; }
-        else if (this.rootDoc.presDuration) durationInS = NumCast(this.rootDoc.presDuration) / 1000;
-        else durationInS = 2;
-        return "D: " + durationInS + "s";
-    }
+      embedHeight = (): number => 97;
+      // embedWidth = () => this.props.PanelWidth();
+      // embedHeight = () => Math.min(this.props.PanelWidth() - 20, this.props.PanelHeight() - this.collapsedHeight);
+      embedWidth = (): number => this.props.PanelWidth() - 35;
+      styleProvider = (doc: (Doc | undefined), props: Opt<DocumentViewProps>, property: string): any => {
+            if (property === StyleProp.Opacity) return 1;
+            return this.props.styleProvider?.(doc, props, property);
+      }
+      /**
+       * The function that is responsible for rendering a preview or not for this
+       * presentation element.
+       */
+      @computed get renderEmbeddedInline() {
+            return !this.rootDoc.presExpandInlineButton || !this.targetDoc ? (null) :
+                  <div className="presItem-embedded" style={{ height: this.embedHeight(), width: this.embedWidth() }}>
+                        <DocumentView
+                              Document={this.targetDoc}
+                              DataDoc={this.targetDoc[DataSym] !== this.targetDoc && this.targetDoc[DataSym]}
+                              styleProvider={this.styleProvider}
+                              layerProvider={this.props.layerProvider}
+                              docViewPath={returnEmptyDoclist}
+                              rootSelected={returnTrue}
+                              addDocument={returnFalse}
+                              removeDocument={returnFalse}
+                              isContentActive={this.props.isContentActive}
+                              addDocTab={returnFalse}
+                              pinToPres={returnFalse}
+                              PanelWidth={this.embedWidth}
+                              PanelHeight={this.embedHeight}
+                              ScreenToLocalTransform={Transform.Identity}
+                              moveDocument={this.props.moveDocument!}
+                              renderDepth={this.props.renderDepth + 1}
+                              focus={DocUtils.DefaultFocus}
+                              whenChildContentsActiveChanged={returnFalse}
+                              bringToFront={returnFalse}
+                              docFilters={this.props.docFilters}
+                              docRangeFilters={this.props.docRangeFilters}
+                              searchFilterDocs={this.props.searchFilterDocs}
+                              ContainingCollectionView={undefined}
+                              ContainingCollectionDoc={undefined}
+                              hideLinkButton={true}
+                        />
+                        <div className="presItem-embeddedMask" />
+                  </div>;
+      }
 
-    @computed get transition() {
-        let transitionInS: number;
-        if (this.rootDoc.presTransition) transitionInS = NumCast(this.rootDoc.presTransition) / 1000;
-        else transitionInS = 0.5;
-        return this.rootDoc.presMovement === PresMovement.Jump || this.rootDoc.presMovement === PresMovement.None ? (null) : "M: " + transitionInS + "s";
-    }
+      @computed get duration() {
+            let durationInS: number;
+            if (this.rootDoc.type === DocumentType.AUDIO || this.rootDoc.type === DocumentType.VID) { durationInS = NumCast(this.rootDoc.presEndTime) - NumCast(this.rootDoc.presStartTime); durationInS = Math.round(durationInS * 10) / 10; }
+            else if (this.rootDoc.presDuration) durationInS = NumCast(this.rootDoc.presDuration) / 1000;
+            else durationInS = 2;
+            return "D: " + durationInS + "s";
+      }
 
-    private _itemRef: React.RefObject<HTMLDivElement> = React.createRef();
-    private _dragRef: React.RefObject<HTMLDivElement> = React.createRef();
-    private _titleRef: React.RefObject<EditableView> = React.createRef();
+      @computed get transition() {
+            let transitionInS: number;
+            if (this.rootDoc.presTransition) transitionInS = NumCast(this.rootDoc.presTransition) / 1000;
+            else transitionInS = 0.5;
+            return this.rootDoc.presMovement === PresMovement.Jump || this.rootDoc.presMovement === PresMovement.None ? (null) : "M: " + transitionInS + "s";
+      }
 
+      private _itemRef: React.RefObject<HTMLDivElement> = React.createRef();
+      private _dragRef: React.RefObject<HTMLDivElement> = React.createRef();
+      private _titleRef: React.RefObject<EditableView> = React.createRef();
 
-    @action
-    headerDown = (e: React.PointerEvent<HTMLDivElement>) => {
-        const element = e.target as any;
-        e.stopPropagation();
-        e.preventDefault();
-        if (element && !(e.ctrlKey || e.metaKey)) {
-            if (PresBox.Instance._selectedArray.has(this.rootDoc)) {
-                PresBox.Instance._selectedArray.size === 1 && PresBox.Instance.regularSelect(this.rootDoc, this._itemRef.current!, this._dragRef.current!, false);
-                setupMoveUpEvents(this, e, this.startDrag, emptyFunction, emptyFunction);
-            } else {
-                setupMoveUpEvents(this, e, ((e: PointerEvent) => {
-                    PresBox.Instance.regularSelect(this.rootDoc, this._itemRef.current!, this._dragRef.current!, false);
-                    return this.startDrag(e);
-                }), emptyFunction, emptyFunction);
+
+      @action
+      headerDown = (e: React.PointerEvent<HTMLDivElement>) => {
+            const element = e.target as any;
+            e.stopPropagation();
+            e.preventDefault();
+            if (element && !(e.ctrlKey || e.metaKey)) {
+                  if (PresBox.Instance._selectedArray.has(this.rootDoc)) {
+                        PresBox.Instance._selectedArray.size === 1 && PresBox.Instance.regularSelect(this.rootDoc, this._itemRef.current!, this._dragRef.current!, false);
+                        setupMoveUpEvents(this, e, this.startDrag, emptyFunction, emptyFunction);
+                  } else {
+                        setupMoveUpEvents(this, e, ((e: PointerEvent) => {
+                              PresBox.Instance.regularSelect(this.rootDoc, this._itemRef.current!, this._dragRef.current!, false);
+                              return this.startDrag(e);
+                        }), emptyFunction, emptyFunction);
+                  }
+            }
+      }
+
+      headerUp = (e: React.PointerEvent<HTMLDivElement>) => {
+            e.stopPropagation();
+            e.preventDefault();
+      }
+
+      /**
+       * Function to drag and drop the pres element to a diferent location
+       */
+      startDrag = (e: PointerEvent) => {
+            const miniView: boolean = this.toolbarWidth <= 100;
+            const activeItem = this.rootDoc;
+            const dragArray = PresBox.Instance._dragArray;
+            const dragData = new DragManager.DocumentDragData(PresBox.Instance.sortArray());
+            const dragItem: HTMLElement[] = [];
+            if (dragArray.length === 1) {
+                  const doc = dragArray[0];
+                  doc.className = miniView ? "presItem-miniSlide" : "presItem-slide";
+                  dragItem.push(doc);
+            } else if (dragArray.length >= 1) {
+                  const doc = document.createElement('div');
+                  doc.className = "presItem-multiDrag";
+                  doc.innerText = "Move " + PresBox.Instance._selectedArray.size + " slides";
+                  doc.style.position = 'absolute';
+                  doc.style.top = (e.clientY) + 'px';
+                  doc.style.left = (e.clientX - 50) + 'px';
+                  dragItem.push(doc);
             }
-        }
-    }
 
-    headerUp = (e: React.PointerEvent<HTMLDivElement>) => {
-        e.stopPropagation();
-        e.preventDefault();
-    }
+            // const dropEvent = () => runInAction(() => this._dragging = false);
+            if (activeItem) {
+                  DragManager.StartDocumentDrag(dragItem.map(ele => ele), dragData, e.clientX, e.clientY, undefined);
+                  // runInAction(() => this._dragging = true);
+                  return true;
+            }
+            return false;
+      }
 
-    /**
-     * Function to drag and drop the pres element to a diferent location
-     */
-    startDrag = (e: PointerEvent) => {
-        const miniView: boolean = this.toolbarWidth <= 100;
-        const activeItem = this.rootDoc;
-        const dragArray = PresBox.Instance._dragArray;
-        const dragData = new DragManager.DocumentDragData(PresBox.Instance.sortArray());
-        const dragItem: HTMLElement[] = [];
-        if (dragArray.length === 1) {
-            const doc = dragArray[0];
-            doc.className = miniView ? "presItem-miniSlide" : "presItem-slide";
-            dragItem.push(doc);
-        } else if (dragArray.length >= 1) {
-            const doc = document.createElement('div');
-            doc.className = "presItem-multiDrag";
-            doc.innerText = "Move " + PresBox.Instance._selectedArray.size + " slides";
-            doc.style.position = 'absolute';
-            doc.style.top = (e.clientY) + 'px';
-            doc.style.left = (e.clientX - 50) + 'px';
-            dragItem.push(doc);
-        }
+      onPointerOver = (e: any) => {
+            document.removeEventListener("pointermove", this.onPointerMove);
+            document.addEventListener("pointermove", this.onPointerMove);
+      }
 
-        // const dropEvent = () => runInAction(() => this._dragging = false);
-        if (activeItem) {
-            DragManager.StartDocumentDrag(dragItem.map(ele => ele), dragData, e.clientX, e.clientY, undefined);
-            // runInAction(() => this._dragging = true);
-            return true;
-        }
-        return false;
-    }
+      onPointerMove = (e: PointerEvent) => {
+            const slide = this._itemRef.current!;
+            let dragIsPresItem: boolean = DragManager.docsBeingDragged.length > 0 ? true : false;
+            for (const doc of DragManager.docsBeingDragged) {
+                  if (!doc.presentationTargetDoc) dragIsPresItem = false;
+            }
+            if (slide && dragIsPresItem) {
+                  const rect = slide.getBoundingClientRect();
+                  const y = e.clientY - rect.top;  //y position within the element.
+                  const height = slide.clientHeight;
+                  const halfLine = height / 2;
+                  if (y <= halfLine) {
+                        slide.style.borderTop = `solid 2px ${Colors.MEDIUM_BLUE}`;
+                        slide.style.borderBottom = "0px";
+                  } else if (y > halfLine) {
+                        slide.style.borderTop = "0px";
+                        slide.style.borderBottom = `solid 2px ${Colors.MEDIUM_BLUE}`;
+                  }
+            }
+            document.removeEventListener("pointermove", this.onPointerMove);
+      }
 
-    onPointerOver = (e: any) => {
-        document.removeEventListener("pointermove", this.onPointerMove);
-        document.addEventListener("pointermove", this.onPointerMove);
-    }
+      onPointerLeave = (e: any) => {
+            this._itemRef.current!.style.borderTop = "0px";
+            this._itemRef.current!.style.borderBottom = "0px";
+            document.removeEventListener("pointermove", this.onPointerMove);
+      }
 
-    onPointerMove = (e: PointerEvent) => {
-        const slide = this._itemRef.current!;
-        let dragIsPresItem: boolean = DragManager.docsBeingDragged.length > 0 ? true : false;
-        for (const doc of DragManager.docsBeingDragged) {
-            if (!doc.presentationTargetDoc) dragIsPresItem = false;
-        }
-        if (slide && dragIsPresItem) {
-            const rect = slide.getBoundingClientRect();
-            const y = e.clientY - rect.top;  //y position within the element.
-            const height = slide.clientHeight;
-            const halfLine = height / 2;
-            if (y <= halfLine) {
-                slide.style.borderTop = `solid 2px ${Colors.MEDIUM_BLUE}`;
-                slide.style.borderBottom = "0px";
-            } else if (y > halfLine) {
-                slide.style.borderTop = "0px";
-                slide.style.borderBottom = `solid 2px ${Colors.MEDIUM_BLUE}`;
+      @action
+      toggleProperties = () => {
+            if (CurrentUserUtils.propertiesWidth < 5) {
+                  action(() => (CurrentUserUtils.propertiesWidth = 250));
             }
-        }
-        document.removeEventListener("pointermove", this.onPointerMove);
-    }
+      }
 
-    onPointerLeave = (e: any) => {
-        this._itemRef.current!.style.borderTop = "0px";
-        this._itemRef.current!.style.borderBottom = "0px";
-        document.removeEventListener("pointermove", this.onPointerMove);
-    }
+      @undoBatch
+      removeItem = action((e: React.MouseEvent) => {
+            this.props.removeDocument?.(this.rootDoc);
+            if (PresBox.Instance._selectedArray.has(this.rootDoc)) {
+                  PresBox.Instance._selectedArray.delete(this.rootDoc);
+            }
+            e.stopPropagation();
+      });
+
+      // set the value/title of the individual pres element
+      @undoBatch
+      @action
+      onSetValue = (value: string) => {
+            this.rootDoc.title = !value.trim().length ? "-untitled-" : value;
+            return true;
+      }
 
-    @action
-    toggleProperties = () => {
-        if (CurrentUserUtils.propertiesWidth < 5) {
-            action(() => (CurrentUserUtils.propertiesWidth = 250));
-        }
-    }
+      /**
+       * Method called for updating the view of the currently selected document
+       * 
+       * @param targetDoc 
+       * @param activeItem 
+       */
+      @undoBatch
+      @action
+      updateView = (targetDoc: Doc, activeItem: Doc) => {
+            if (targetDoc.type === DocumentType.PDF || targetDoc.type === DocumentType.WEB || targetDoc.type === DocumentType.RTF) {
+                  const scroll = targetDoc._scrollTop;
+                  activeItem.presPinViewScroll = scroll;
+            } else if (targetDoc.type === DocumentType.VID) {
+                  activeItem.presPinTimecode = targetDoc._currentTimecode;
+            } else if (targetDoc.type === DocumentType.COMPARISON) {
+                  const clipWidth = targetDoc._clipWidth;
+                  activeItem.presPinClipWidth = clipWidth;
+            } else {
+                  const x = targetDoc._panX;
+                  const y = targetDoc._panY;
+                  const scale = targetDoc._viewScale;
+                  activeItem.presPinViewX = x;
+                  activeItem.presPinViewY = y;
+                  activeItem.presPinViewScale = scale;
+            }
+      }
 
-    @undoBatch
-    removeItem = action((e: React.MouseEvent) => {
-        this.props.removeDocument?.(this.rootDoc);
-        if (PresBox.Instance._selectedArray.has(this.rootDoc)) {
-            PresBox.Instance._selectedArray.delete(this.rootDoc);
-        }
-        e.stopPropagation();
-    });
+      @undoBatch
+      @action
+      startRecording = (targetDoc: Doc, activeItem: Doc) => {
 
-    // set the value/title of the individual pres element
-    @undoBatch
-    @action
-    onSetValue = (value: string) => {
-        this.rootDoc.title = !value.trim().length ? "-untitled-" : value;
-        return true;
-    }
+            // TODO: Remove everything that already exists
+            if (activeItem.recording) {
+                  // if we already have an existing recording
+                  Doc.AddDocToList((Doc.UserDoc().myOverlayDocs as Doc), undefined, Cast(activeItem.recording, Doc, null));
 
-    /**
-     * Method called for updating the view of the currently selected document
-     * 
-     * @param targetDoc 
-     * @param activeItem 
-     */
-    @undoBatch
-    @action
-    updateView = (targetDoc: Doc, activeItem: Doc) => {
-        if (targetDoc.type === DocumentType.PDF || targetDoc.type === DocumentType.WEB || targetDoc.type === DocumentType.RTF) {
-            const scroll = targetDoc._scrollTop;
-            activeItem.presPinViewScroll = scroll;
-        } else if (targetDoc.type === DocumentType.VID) {
-            activeItem.presPinTimecode = targetDoc._currentTimecode;
-        } else if (targetDoc.type === DocumentType.COMPARISON) {
-            const clipWidth = targetDoc._clipWidth;
-            activeItem.presPinClipWidth = clipWidth;
-        } else {
-            const x = targetDoc._panX;
-            const y = targetDoc._panY;
-            const scale = targetDoc._viewScale;
-            activeItem.presPinViewX = x;
-            activeItem.presPinViewY = y;
-            activeItem.presPinViewScale = scale;
-        }
-    }
+            } else {
+                  // if we dont have any recording
+                  console.log(targetDoc.title, activeItem.title);
+                  const recording = Docs.Create.WebCamDocument("", { _width: 400, _height: 200, title: "recording", system: true, cloneFieldFilter: new List<string>(["system"]) });
+                  activeItem.recording = recording
+                  // TODO: Figure out exactly where we want the video to appear
+                  const pt = this.props.ScreenToLocalTransform().inverse().transformPoint(0, 0);
+                  console.log("x: ", pt[0], "y: ", pt[1]);
+                  recording.x = pt[0];
+                  recording.y = pt[1];
+                  Doc.AddDocToList((Doc.UserDoc().myOverlayDocs as Doc), undefined, recording);
+            }
 
-    @undoBatch
-    @action 
-    startRecording = (targetDoc: Doc, activeItem: Doc) => {
-        // If we already have an existing recording
-        // TODO: Remove everything that already exists
-        if (activeItem.recording) {
-            Doc.AddDocToList((Doc.UserDoc().myOverlayDocs as Doc), undefined, Cast(activeItem.recording, Doc, null));
-        // Recording has not yet been created
-        } else {
-            console.log(targetDoc.title, activeItem.title);
-            const recording = Docs.Create.WebCamDocument("", { _width: 400, _height: 200, title: "recording", system: true, cloneFieldFilter: new List<string>(["system"]) });
-            activeItem.recording = recording
-            // TODO: Figure out exactly where we want the video to appear
-            const pt = this.props.ScreenToLocalTransform().inverse().transformPoint(0, 0);
-            console.log("x: ", pt[0], "y: ", pt[1]);
-            recording.x = pt[0];
-            recording.y = pt[1];
-            Doc.AddDocToList((Doc.UserDoc().myOverlayDocs as Doc), undefined, recording);
-        }
-        
-    }
+      }
 
-    @computed
-    get toolbarWidth(): number {
-        const presBoxDocView = DocumentManager.Instance.getDocumentView(this.presBox);
-        let width: number = NumCast(this.presBox._width);
-        if (presBoxDocView) width = presBoxDocView.props.PanelWidth();
-        if (width === 0) width = 300;
-        return width;
-    }
+      @computed
+      get toolbarWidth(): number {
+            const presBoxDocView = DocumentManager.Instance.getDocumentView(this.presBox);
+            let width: number = NumCast(this.presBox._width);
+            if (presBoxDocView) width = presBoxDocView.props.PanelWidth();
+            if (width === 0) width = 300;
+            return width;
+      }
 
-    @computed get mainItem() {
-        const isSelected: boolean = PresBox.Instance._selectedArray.has(this.rootDoc);
-        const toolbarWidth: number = this.toolbarWidth;
-        const showMore: boolean = this.toolbarWidth >= 300;
-        const miniView: boolean = this.toolbarWidth <= 110;
-        const presBox: Doc = this.presBox; //presBox
-        const presBoxColor: string = StrCast(presBox._backgroundColor);
-        const presColorBool: boolean = presBoxColor ? (presBoxColor !== Colors.WHITE && presBoxColor !== "transparent") : false;
-        const targetDoc: Doc = this.targetDoc;
-        const activeItem: Doc = this.rootDoc;
-        return (
-            <div className={`presItem-container`}
-                key={this.props.Document[Id] + this.indexInPres}
-                ref={this._itemRef}
-                style={{
-                    backgroundColor: presColorBool ? isSelected ? "rgba(250,250,250,0.3)" : "transparent" : isSelected ? Colors.LIGHT_BLUE : "transparent",
-                    opacity: this._dragging ? 0.3 : 1
-                }}
-                onClick={e => {
-                    e.stopPropagation();
-                    e.preventDefault();
-                    PresBox.Instance.modifierSelect(this.rootDoc, this._itemRef.current!, this._dragRef.current!, !e.shiftKey && !e.ctrlKey && !e.metaKey, e.ctrlKey || e.metaKey, e.shiftKey);
-                }}
-                onDoubleClick={action(e => {
-                    this.toggleProperties();
-                    PresBox.Instance.regularSelect(this.rootDoc, this._itemRef.current!, this._dragRef.current!, true);
-                })}
-                onPointerOver={this.onPointerOver}
-                onPointerLeave={this.onPointerLeave}
-                onPointerDown={this.headerDown}
-                onPointerUp={this.headerUp}
-            >
-                {miniView ?
-                    // when width is LESS than 110 px 
-                    <div className={`presItem-miniSlide ${isSelected ? "active" : ""}`} ref={miniView ? this._dragRef : null}>
-                        {`${this.indexInPres + 1}.`}
-                    </div>
-                    :
-                    // when width is MORE than 110 px 
-                    <div className="presItem-number">
-                        {`${this.indexInPres + 1}.`}
-                    </div>}
-                {miniView ? (null) : <div ref={miniView ? null : this._dragRef} className={`presItem-slide ${isSelected ? "active" : ""}`}
-                    style={{
-                        backgroundColor: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BackgroundColor),
-                        boxShadow: presBoxColor && presBoxColor !== "white" && presBoxColor !== "transparent" ? isSelected ? "0 0 0px 1.5px" + presBoxColor : undefined : undefined
-                    }}>
-                    <div className="presItem-name" style={{ maxWidth: showMore ? (toolbarWidth - 195) : toolbarWidth - 105, cursor: isSelected ? 'text' : 'grab' }}>
-                        <EditableView
-                            ref={this._titleRef}
-                            editing={!isSelected ? false : undefined}
-                            contents={activeItem.title}
-                            overflow={'ellipsis'}
-                            GetValue={() => StrCast(activeItem.title)}
-                            SetValue={this.onSetValue}
-                        />
-                    </div>
-                    <Tooltip title={<><div className="dash-tooltip">{"Movement speed"}</div></>}><div className="presItem-time" style={{ display: showMore ? "block" : "none" }}>{this.transition}</div></Tooltip>
-                    <Tooltip title={<><div className="dash-tooltip">{"Duration"}</div></>}><div className="presItem-time" style={{ display: showMore ? "block" : "none" }}>{this.duration}</div></Tooltip>
-                    <div className={"presItem-slideButtons"}>
-                        <Tooltip title={<><div className="dash-tooltip">{"Update view"}</div></>}>
-                            <div className="slideButton"
-                                onClick={() => this.updateView(targetDoc, activeItem)}
-                                style={{ fontWeight: 700, display: activeItem.presPinView ? "flex" : "none" }}>V</div>
-                        </Tooltip>
-                        <Tooltip title={<><div className="dash-tooltip">{"Start recording"}</div></>}>
-                            <div className="slideButton"
-                                onClick={() => this.startRecording(targetDoc, activeItem)}
-                                style={{ fontWeight: 700 }}>
-                                    <FontAwesomeIcon icon={"video"} onPointerDown={e => e.stopPropagation()} />
-                                </div>
-                        </Tooltip>
-                        {this.indexInPres === 0 ? (null) : <Tooltip title={<><div className="dash-tooltip">{activeItem.groupWithUp ? "Ungroup" : "Group with up"}</div></>}>
-                            <div className="slideButton"
-                                onClick={() => activeItem.groupWithUp = !activeItem.groupWithUp}
-                                style={{
-                                    zIndex: 1000 - this.indexInPres,
-                                    fontWeight: 700,
-                                    backgroundColor: activeItem.groupWithUp ? presColorBool ? presBoxColor : Colors.MEDIUM_BLUE : undefined,
-                                    height: activeItem.groupWithUp ? 53 : 18,
-                                    transform: activeItem.groupWithUp ? "translate(0, -17px)" : undefined
-                                }}>
-                                <div style={{ transform: activeItem.groupWithUp ? "rotate(180deg) translate(0, -17.5px)" : "rotate(0deg)" }}>
-                                    <FontAwesomeIcon icon={"arrow-up"} onPointerDown={e => e.stopPropagation()} />
-                                </div>
-                            </div>
-                        </Tooltip>}
-                        <Tooltip title={<><div className="dash-tooltip">{this.rootDoc.presExpandInlineButton ? "Minimize" : "Expand"}</div></>}><div className={"slideButton"} onClick={e => { e.stopPropagation(); this.presExpandDocumentClick(); }}>
-                            <FontAwesomeIcon icon={this.rootDoc.presExpandInlineButton ? "eye-slash" : "eye"} onPointerDown={e => e.stopPropagation()} />
-                        </div></Tooltip>
-                        <Tooltip title={<><div className="dash-tooltip">{"Remove from presentation"}</div></>}><div
-                            className={"slideButton"}
-                            onClick={this.removeItem}>
-                            <FontAwesomeIcon icon={"trash"} onPointerDown={e => e.stopPropagation()} />
-                        </div></Tooltip>
-                    </div>
-                    <div className="presItem-docName" style={{ maxWidth: showMore ? (toolbarWidth - 195) : toolbarWidth - 105 }}>{activeItem.presPinView ? (<><i>View of </i> {targetDoc.title}</>) : targetDoc.title}</div>
-                    {this.renderEmbeddedInline}
-                </div>}
-            </div >);
-    }
+      @computed get mainItem() {
+            const isSelected: boolean = PresBox.Instance._selectedArray.has(this.rootDoc);
+            const toolbarWidth: number = this.toolbarWidth;
+            const showMore: boolean = this.toolbarWidth >= 300;
+            const miniView: boolean = this.toolbarWidth <= 110;
+            const presBox: Doc = this.presBox; //presBox
+            const presBoxColor: string = StrCast(presBox._backgroundColor);
+            const presColorBool: boolean = presBoxColor ? (presBoxColor !== Colors.WHITE && presBoxColor !== "transparent") : false;
+            const targetDoc: Doc = this.targetDoc;
+            const activeItem: Doc = this.rootDoc;
+            return (
+                  <div className={`presItem-container`}
+                        key={this.props.Document[Id] + this.indexInPres}
+                        ref={this._itemRef}
+                        style={{
+                              backgroundColor: presColorBool ? isSelected ? "rgba(250,250,250,0.3)" : "transparent" : isSelected ? Colors.LIGHT_BLUE : "transparent",
+                              opacity: this._dragging ? 0.3 : 1
+                        }}
+                        onClick={e => {
+                              e.stopPropagation();
+                              e.preventDefault();
+                              PresBox.Instance.modifierSelect(this.rootDoc, this._itemRef.current!, this._dragRef.current!, !e.shiftKey && !e.ctrlKey && !e.metaKey, e.ctrlKey || e.metaKey, e.shiftKey);
+                        }}
+                        onDoubleClick={action(e => {
+                              this.toggleProperties();
+                              PresBox.Instance.regularSelect(this.rootDoc, this._itemRef.current!, this._dragRef.current!, true);
+                        })}
+                        onPointerOver={this.onPointerOver}
+                        onPointerLeave={this.onPointerLeave}
+                        onPointerDown={this.headerDown}
+                        onPointerUp={this.headerUp}
+                  >
+                        {miniView ?
+                              // when width is LESS than 110 px 
+                              <div className={`presItem-miniSlide ${isSelected ? "active" : ""}`} ref={miniView ? this._dragRef : null}>
+                                    {`${this.indexInPres + 1}.`}
+                              </div>
+                              :
+                              // when width is MORE than 110 px 
+                              <div className="presItem-number">
+                                    {`${this.indexInPres + 1}.`}
+                              </div>}
+                        {miniView ? (null) : <div ref={miniView ? null : this._dragRef} className={`presItem-slide ${isSelected ? "active" : ""}`}
+                              style={{
+                                    backgroundColor: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BackgroundColor),
+                                    boxShadow: presBoxColor && presBoxColor !== "white" && presBoxColor !== "transparent" ? isSelected ? "0 0 0px 1.5px" + presBoxColor : undefined : undefined
+                              }}>
+                              <div className="presItem-name" style={{ maxWidth: showMore ? (toolbarWidth - 195) : toolbarWidth - 105, cursor: isSelected ? 'text' : 'grab' }}>
+                                    <EditableView
+                                          ref={this._titleRef}
+                                          editing={!isSelected ? false : undefined}
+                                          contents={activeItem.title}
+                                          overflow={'ellipsis'}
+                                          GetValue={() => StrCast(activeItem.title)}
+                                          SetValue={this.onSetValue}
+                                    />
+                              </div>
+                              <Tooltip title={<><div className="dash-tooltip">{"Movement speed"}</div></>}><div className="presItem-time" style={{ display: showMore ? "block" : "none" }}>{this.transition}</div></Tooltip>
+                              <Tooltip title={<><div className="dash-tooltip">{"Duration"}</div></>}><div className="presItem-time" style={{ display: showMore ? "block" : "none" }}>{this.duration}</div></Tooltip>
+                              <div className={"presItem-slideButtons"}>
+                                    <Tooltip title={<><div className="dash-tooltip">{"Update view"}</div></>}>
+                                          <div className="slideButton"
+                                                onClick={() => this.updateView(targetDoc, activeItem)}
+                                                style={{ fontWeight: 700, display: activeItem.presPinView ? "flex" : "none" }}>V</div>
+                                    </Tooltip>
+                                    <Tooltip title={<><div className="dash-tooltip">{"Start recording"}</div></>}>
+                                          <div className="slideButton"
+                                                onClick={() => this.startRecording(targetDoc, activeItem)}
+                                                style={{ fontWeight: 700 }}>
+                                                <FontAwesomeIcon icon={"video"} onPointerDown={e => e.stopPropagation()} />
+                                          </div>
+                                    </Tooltip>
+                                    {this.indexInPres === 0 ? (null) : <Tooltip title={<><div className="dash-tooltip">{activeItem.groupWithUp ? "Ungroup" : "Group with up"}</div></>}>
+                                          <div className="slideButton"
+                                                onClick={() => activeItem.groupWithUp = !activeItem.groupWithUp}
+                                                style={{
+                                                      zIndex: 1000 - this.indexInPres,
+                                                      fontWeight: 700,
+                                                      backgroundColor: activeItem.groupWithUp ? presColorBool ? presBoxColor : Colors.MEDIUM_BLUE : undefined,
+                                                      height: activeItem.groupWithUp ? 53 : 18,
+                                                      transform: activeItem.groupWithUp ? "translate(0, -17px)" : undefined
+                                                }}>
+                                                <div style={{ transform: activeItem.groupWithUp ? "rotate(180deg) translate(0, -17.5px)" : "rotate(0deg)" }}>
+                                                      <FontAwesomeIcon icon={"arrow-up"} onPointerDown={e => e.stopPropagation()} />
+                                                </div>
+                                          </div>
+                                    </Tooltip>}
+                                    <Tooltip title={<><div className="dash-tooltip">{this.rootDoc.presExpandInlineButton ? "Minimize" : "Expand"}</div></>}><div className={"slideButton"} onClick={e => { e.stopPropagation(); this.presExpandDocumentClick(); }}>
+                                          <FontAwesomeIcon icon={this.rootDoc.presExpandInlineButton ? "eye-slash" : "eye"} onPointerDown={e => e.stopPropagation()} />
+                                    </div></Tooltip>
+                                    <Tooltip title={<><div className="dash-tooltip">{"Remove from presentation"}</div></>}><div
+                                          className={"slideButton"}
+                                          onClick={this.removeItem}>
+                                          <FontAwesomeIcon icon={"trash"} onPointerDown={e => e.stopPropagation()} />
+                                    </div></Tooltip>
+                              </div>
+                              <div className="presItem-docName" style={{ maxWidth: showMore ? (toolbarWidth - 195) : toolbarWidth - 105 }}>{activeItem.presPinView ? (<><i>View of </i> {targetDoc.title}</>) : targetDoc.title}</div>
+                              {this.renderEmbeddedInline}
+                        </div>}
+                  </div >);
+      }
 
-    render() {
-        return !(this.rootDoc instanceof Doc) || this.targetDoc instanceof Promise ? (null) : this.mainItem;
-    }
+      render() {
+            return !(this.rootDoc instanceof Doc) || this.targetDoc instanceof Promise ? (null) : this.mainItem;
+      }
 }
\ No newline at end of file
diff --git a/src/server/ApiManagers/UploadManager.ts b/src/server/ApiManagers/UploadManager.ts
index bfa07d47a..5a19477dd 100644
--- a/src/server/ApiManagers/UploadManager.ts
+++ b/src/server/ApiManagers/UploadManager.ts
@@ -19,288 +19,288 @@ import { SolrManager } from "./SearchManager";
 import { StringDecoder } from "string_decoder";
 
 export enum Directory {
-    parsed_files = "parsed_files",
-    images = "images",
-    videos = "videos",
-    pdfs = "pdfs",
-    text = "text",
-    pdf_thumbnails = "pdf_thumbnails",
-    audio = "audio",
+      parsed_files = "parsed_files",
+      images = "images",
+      videos = "videos",
+      pdfs = "pdfs",
+      text = "text",
+      pdf_thumbnails = "pdf_thumbnails",
+      audio = "audio",
 }
 
 export function serverPathToFile(directory: Directory, filename: string) {
-    return normalize(`${filesDirectory}/${directory}/${filename}`);
+      return normalize(`${filesDirectory}/${directory}/${filename}`);
 }
 
 export function pathToDirectory(directory: Directory) {
-    return normalize(`${filesDirectory}/${directory}`);
+      return normalize(`${filesDirectory}/${directory}`);
 }
 
 export function clientPathToFile(directory: Directory, filename: string) {
-    return `/files/${directory}/${filename}`;
+      return `/files/${directory}/${filename}`;
 }
 
 export default class UploadManager extends ApiManager {
 
-    protected initialize(register: Registration): void {
+      protected initialize(register: Registration): void {
 
-        register({
-            method: Method.POST,
-            subscription: "/uploadFormData",
-            secureHandler: async ({ req, res }) => {
-                const form = new formidable.IncomingForm();
-                form.keepExtensions = true;
-                form.uploadDir = pathToDirectory(Directory.parsed_files);
-                return new Promise<void>(resolve => {
-                    form.parse(req, async (_err, _fields, files) => {
-                        const results: Upload.FileResponse[] = [];
-                        for (const key in files) {
-                            const f = files[key];
-                            if (!Array.isArray(f)) {
-                                const result = await DashUploadUtils.upload(f);
-                                result && !(result.result instanceof Error) && results.push(result);
-                            }
-                        }
-                        _success(res, results);
-                        resolve();
-                    });
-                });
-            }
-        });
+            register({
+                  method: Method.POST,
+                  subscription: "/uploadFormData",
+                  secureHandler: async ({ req, res }) => {
+                        const form = new formidable.IncomingForm();
+                        form.keepExtensions = true;
+                        form.uploadDir = pathToDirectory(Directory.parsed_files);
+                        return new Promise<void>(resolve => {
+                              form.parse(req, async (_err, _fields, files) => {
+                                    const results: Upload.FileResponse[] = [];
+                                    for (const key in files) {
+                                          const f = files[key];
+                                          if (!Array.isArray(f)) {
+                                                const result = await DashUploadUtils.upload(f);
+                                                result && !(result.result instanceof Error) && results.push(result);
+                                          }
+                                    }
+                                    _success(res, results);
+                                    resolve();
+                              });
+                        });
+                  }
+            });
 
-        register({
-            method: Method.POST,
-            subscription: "/uploadYoutubeVideo",
-            secureHandler: async ({ req, res }) => {
-                //req.readableBuffer.head.data
-                return new Promise<void>(async resolve => {
-                    req.addListener("data", async (args) => {
-                        console.log(args);
-                        const payload = String.fromCharCode.apply(String, args);
-                        const videoId = JSON.parse(payload).videoId;
-                        const results: Upload.FileResponse[] = [];
-                        const result = await DashUploadUtils.uploadYoutube(videoId);
-                        result && !(result.result instanceof Error) && results.push(result);
-                        _success(res, results);
-                        resolve();
-                    });
-                });
-            }
-        });
+            register({
+                  method: Method.POST,
+                  subscription: "/uploadYoutubeVideo",
+                  secureHandler: async ({ req, res }) => {
+                        //req.readableBuffer.head.data
+                        return new Promise<void>(async resolve => {
+                              req.addListener("data", async (args) => {
+                                    console.log(args);
+                                    const payload = String.fromCharCode.apply(String, args);
+                                    const videoId = JSON.parse(payload).videoId;
+                                    const results: Upload.FileResponse[] = [];
+                                    const result = await DashUploadUtils.uploadYoutube(videoId);
+                                    result && !(result.result instanceof Error) && results.push(result);
+                                    _success(res, results);
+                                    resolve();
+                              });
+                        });
+                  }
+            });
 
-        register({
-            method: Method.POST,
-            subscription: new RouteSubscriber("youtubeScreenshot"),
-            secureHandler: async ({ req, res }) => {
-                const { id, timecode } = req.body;
-                const convert = (raw: string) => {
-                    const number = Math.floor(Number(raw));
-                    const seconds = number % 60;
-                    const minutes = (number - seconds) / 60;
-                    return `${minutes}m${seconds}s`;
-                };
-                const suffix = timecode ? `&t=${convert(timecode)}` : ``;
-                const targetUrl = `https://www.youtube.com/watch?v=${id}${suffix}`;
-                const buffer = await captureYoutubeScreenshot(targetUrl);
-                if (!buffer) {
-                    return res.send();
-                }
-                const resolvedName = `youtube_capture_${id}_${suffix}.png`;
-                const resolvedPath = serverPathToFile(Directory.images, resolvedName);
-                return new Promise<void>(resolve => {
-                    writeFile(resolvedPath, buffer, async error => {
-                        if (error) {
-                            return res.send();
+            register({
+                  method: Method.POST,
+                  subscription: new RouteSubscriber("youtubeScreenshot"),
+                  secureHandler: async ({ req, res }) => {
+                        const { id, timecode } = req.body;
+                        const convert = (raw: string) => {
+                              const number = Math.floor(Number(raw));
+                              const seconds = number % 60;
+                              const minutes = (number - seconds) / 60;
+                              return `${minutes}m${seconds}s`;
+                        };
+                        const suffix = timecode ? `&t=${convert(timecode)}` : ``;
+                        const targetUrl = `https://www.youtube.com/watch?v=${id}${suffix}`;
+                        const buffer = await captureYoutubeScreenshot(targetUrl);
+                        if (!buffer) {
+                              return res.send();
                         }
-                        await DashUploadUtils.outputResizedImages(() => createReadStream(resolvedPath), resolvedName, pathToDirectory(Directory.images));
-                        res.send({
-                            accessPaths: {
-                                agnostic: DashUploadUtils.getAccessPaths(Directory.images, resolvedName)
-                            }
-                        } as Upload.FileInformation);
-                        resolve();
-                    });
-                });
-            }
-        });
+                        const resolvedName = `youtube_capture_${id}_${suffix}.png`;
+                        const resolvedPath = serverPathToFile(Directory.images, resolvedName);
+                        return new Promise<void>(resolve => {
+                              writeFile(resolvedPath, buffer, async error => {
+                                    if (error) {
+                                          return res.send();
+                                    }
+                                    await DashUploadUtils.outputResizedImages(() => createReadStream(resolvedPath), resolvedName, pathToDirectory(Directory.images));
+                                    res.send({
+                                          accessPaths: {
+                                                agnostic: DashUploadUtils.getAccessPaths(Directory.images, resolvedName)
+                                          }
+                                    } as Upload.FileInformation);
+                                    resolve();
+                              });
+                        });
+                  }
+            });
 
-        register({
-            method: Method.POST,
-            subscription: "/uploadRemoteImage",
-            secureHandler: async ({ req, res }) => {
+            register({
+                  method: Method.POST,
+                  subscription: "/uploadRemoteImage",
+                  secureHandler: async ({ req, res }) => {
 
-                const { sources } = req.body;
-                if (Array.isArray(sources)) {
-                    const results = await Promise.all(sources.map(source => DashUploadUtils.UploadImage(source)));
-                    return res.send(results);
-                }
-                res.send();
-            }
-        });
+                        const { sources } = req.body;
+                        if (Array.isArray(sources)) {
+                              const results = await Promise.all(sources.map(source => DashUploadUtils.UploadImage(source)));
+                              return res.send(results);
+                        }
+                        res.send();
+                  }
+            });
 
-        register({
-            method: Method.POST,
-            subscription: "/uploadDoc",
-            secureHandler: ({ req, res }) => {
+            register({
+                  method: Method.POST,
+                  subscription: "/uploadDoc",
+                  secureHandler: ({ req, res }) => {
 
-                const form = new formidable.IncomingForm();
-                form.keepExtensions = true;
-                // let path = req.body.path;
-                const ids: { [id: string]: string } = {};
-                let remap = true;
-                const getId = (id: string): string => {
-                    if (!remap) return id;
-                    if (id.endsWith("Proto")) return id;
-                    if (id in ids) {
-                        return ids[id];
-                    } else {
-                        return ids[id] = v4();
-                    }
-                };
-                const mapFn = (doc: any) => {
-                    if (doc.id) {
-                        doc.id = getId(doc.id);
-                    }
-                    for (const key in doc.fields) {
-                        if (!doc.fields.hasOwnProperty(key)) { continue; }
-                        const field = doc.fields[key];
-                        if (field === undefined || field === null) { continue; }
+                        const form = new formidable.IncomingForm();
+                        form.keepExtensions = true;
+                        // let path = req.body.path;
+                        const ids: { [id: string]: string } = {};
+                        let remap = true;
+                        const getId = (id: string): string => {
+                              if (!remap) return id;
+                              if (id.endsWith("Proto")) return id;
+                              if (id in ids) {
+                                    return ids[id];
+                              } else {
+                                    return ids[id] = v4();
+                              }
+                        };
+                        const mapFn = (doc: any) => {
+                              if (doc.id) {
+                                    doc.id = getId(doc.id);
+                              }
+                              for (const key in doc.fields) {
+                                    if (!doc.fields.hasOwnProperty(key)) { continue; }
+                                    const field = doc.fields[key];
+                                    if (field === undefined || field === null) { continue; }
 
-                        if (field.__type === "Doc") {
-                            mapFn(field);
-                        } else if (field.__type === "proxy" || field.__type === "prefetch_proxy") {
-                            field.fieldId = getId(field.fieldId);
-                        } else if (field.__type === "script" || field.__type === "computed") {
-                            if (field.captures) {
-                                field.captures.fieldId = getId(field.captures.fieldId);
-                            }
-                        } else if (field.__type === "list") {
-                            mapFn(field);
-                        } else if (typeof field === "string") {
-                            const re = /("(?:dataD|d)ocumentId"\s*:\s*")([\w\-]*)"/g;
-                            doc.fields[key] = (field as any).replace(re, (match: any, p1: string, p2: string) => {
-                                return `${p1}${getId(p2)}"`;
-                            });
-                        } else if (field.__type === "RichTextField") {
-                            const re = /("href"\s*:\s*")(.*?)"/g;
-                            field.Data = field.Data.replace(re, (match: any, p1: string, p2: string) => {
-                                return `${p1}${getId(p2)}"`;
-                            });
-                        }
-                    }
-                };
-                return new Promise<void>(resolve => {
-                    form.parse(req, async (_err, fields, files) => {
-                        remap = fields.remap !== "false";
-                        let id: string = "";
-                        try {
-                            for (const name in files) {
-                                const f = files[name];
-                                const path_2 = Array.isArray(f) ? "" : f.path;
-                                const zip = new AdmZip(path_2);
-                                zip.getEntries().forEach((entry: any) => {
-                                    if (!entry.entryName.startsWith("files/")) return;
-                                    let directory = dirname(entry.entryName) + "/";
-                                    const extension = extname(entry.entryName);
-                                    const base = basename(entry.entryName).split(".")[0];
+                                    if (field.__type === "Doc") {
+                                          mapFn(field);
+                                    } else if (field.__type === "proxy" || field.__type === "prefetch_proxy") {
+                                          field.fieldId = getId(field.fieldId);
+                                    } else if (field.__type === "script" || field.__type === "computed") {
+                                          if (field.captures) {
+                                                field.captures.fieldId = getId(field.captures.fieldId);
+                                          }
+                                    } else if (field.__type === "list") {
+                                          mapFn(field);
+                                    } else if (typeof field === "string") {
+                                          const re = /("(?:dataD|d)ocumentId"\s*:\s*")([\w\-]*)"/g;
+                                          doc.fields[key] = (field as any).replace(re, (match: any, p1: string, p2: string) => {
+                                                return `${p1}${getId(p2)}"`;
+                                          });
+                                    } else if (field.__type === "RichTextField") {
+                                          const re = /("href"\s*:\s*")(.*?)"/g;
+                                          field.Data = field.Data.replace(re, (match: any, p1: string, p2: string) => {
+                                                return `${p1}${getId(p2)}"`;
+                                          });
+                                    }
+                              }
+                        };
+                        return new Promise<void>(resolve => {
+                              form.parse(req, async (_err, fields, files) => {
+                                    remap = fields.remap !== "false";
+                                    let id: string = "";
                                     try {
-                                        zip.extractEntryTo(entry.entryName, publicDirectory, true, false);
-                                        directory = "/" + directory;
+                                          for (const name in files) {
+                                                const f = files[name];
+                                                const path_2 = Array.isArray(f) ? "" : f.path;
+                                                const zip = new AdmZip(path_2);
+                                                zip.getEntries().forEach((entry: any) => {
+                                                      if (!entry.entryName.startsWith("files/")) return;
+                                                      let directory = dirname(entry.entryName) + "/";
+                                                      const extension = extname(entry.entryName);
+                                                      const base = basename(entry.entryName).split(".")[0];
+                                                      try {
+                                                            zip.extractEntryTo(entry.entryName, publicDirectory, true, false);
+                                                            directory = "/" + directory;
 
-                                        createReadStream(publicDirectory + directory + base + extension).pipe(createWriteStream(publicDirectory + directory + base + "_o" + extension));
-                                        createReadStream(publicDirectory + directory + base + extension).pipe(createWriteStream(publicDirectory + directory + base + "_s" + extension));
-                                        createReadStream(publicDirectory + directory + base + extension).pipe(createWriteStream(publicDirectory + directory + base + "_m" + extension));
-                                        createReadStream(publicDirectory + directory + base + extension).pipe(createWriteStream(publicDirectory + directory + base + "_l" + extension));
-                                    } catch (e) {
-                                        console.log(e);
-                                    }
-                                });
-                                const json = zip.getEntry("doc.json");
-                                try {
-                                    const data = JSON.parse(json.getData().toString("utf8"));
-                                    const datadocs = data.docs;
-                                    id = getId(data.id);
-                                    const docs = Object.keys(datadocs).map(key => datadocs[key]);
-                                    docs.forEach(mapFn);
-                                    await Promise.all(docs.map((doc: any) => new Promise<void>(res => {
-                                        Database.Instance.replace(doc.id, doc, (err, r) => {
-                                            err && console.log(err);
-                                            res();
-                                        }, true);
-                                    })));
-                                } catch (e) { console.log(e); }
-                                unlink(path_2, () => { });
-                            }
-                            SolrManager.update();
-                            res.send(JSON.stringify(id || "error"));
-                        } catch (e) { console.log(e); }
-                        resolve();
-                    });
-                });
-            }
-        });
+                                                            createReadStream(publicDirectory + directory + base + extension).pipe(createWriteStream(publicDirectory + directory + base + "_o" + extension));
+                                                            createReadStream(publicDirectory + directory + base + extension).pipe(createWriteStream(publicDirectory + directory + base + "_s" + extension));
+                                                            createReadStream(publicDirectory + directory + base + extension).pipe(createWriteStream(publicDirectory + directory + base + "_m" + extension));
+                                                            createReadStream(publicDirectory + directory + base + extension).pipe(createWriteStream(publicDirectory + directory + base + "_l" + extension));
+                                                      } catch (e) {
+                                                            console.log(e);
+                                                      }
+                                                });
+                                                const json = zip.getEntry("doc.json");
+                                                try {
+                                                      const data = JSON.parse(json.getData().toString("utf8"));
+                                                      const datadocs = data.docs;
+                                                      id = getId(data.id);
+                                                      const docs = Object.keys(datadocs).map(key => datadocs[key]);
+                                                      docs.forEach(mapFn);
+                                                      await Promise.all(docs.map((doc: any) => new Promise<void>(res => {
+                                                            Database.Instance.replace(doc.id, doc, (err, r) => {
+                                                                  err && console.log(err);
+                                                                  res();
+                                                            }, true);
+                                                      })));
+                                                } catch (e) { console.log(e); }
+                                                unlink(path_2, () => { });
+                                          }
+                                          SolrManager.update();
+                                          res.send(JSON.stringify(id || "error"));
+                                    } catch (e) { console.log(e); }
+                                    resolve();
+                              });
+                        });
+                  }
+            });
 
-        register({
-            method: Method.POST,
-            subscription: "/inspectImage",
-            secureHandler: async ({ req, res }) => {
+            register({
+                  method: Method.POST,
+                  subscription: "/inspectImage",
+                  secureHandler: async ({ req, res }) => {
 
-                const { source } = req.body;
-                if (typeof source === "string") {
-                    return res.send(await DashUploadUtils.InspectImage(source));
-                }
-                res.send({});
-            }
-        });
+                        const { source } = req.body;
+                        if (typeof source === "string") {
+                              return res.send(await DashUploadUtils.InspectImage(source));
+                        }
+                        res.send({});
+                  }
+            });
 
-        register({
-            method: Method.POST,
-            subscription: "/uploadURI",
-            secureHandler: ({ req, res }) => {
-                const uri = req.body.uri;
-                const filename = req.body.name;
-                const origSuffix = req.body.nosuffix ? SizeSuffix.None : SizeSuffix.Original;
-                if (!uri || !filename) {
-                    res.status(401).send("incorrect parameters specified");
-                    return;
-                }
-                return imageDataUri.outputFile(uri, serverPathToFile(Directory.images, InjectSize(filename, origSuffix))).then((savedName: string) => {
-                    const ext = extname(savedName).toLowerCase();
-                    const { pngs, jpgs } = AcceptableMedia;
-                    const resizers = !origSuffix ? [{ resizer: sharp().resize(400, undefined, { withoutEnlargement: true }), suffix: "_m" }] : [
-                        { resizer: sharp().resize(100, undefined, { withoutEnlargement: true }), suffix: "_s" },
-                        { resizer: sharp().resize(400, undefined, { withoutEnlargement: true }), suffix: "_m" },
-                        { resizer: sharp().resize(900, undefined, { withoutEnlargement: true }), suffix: "_l" },
-                    ];
-                    let isImage = false;
-                    if (pngs.includes(ext)) {
-                        resizers.forEach(element => {
-                            element.resizer = element.resizer.png();
-                        });
-                        isImage = true;
-                    } else if (jpgs.includes(ext)) {
-                        resizers.forEach(element => {
-                            element.resizer = element.resizer.jpeg();
-                        });
-                        isImage = true;
-                    }
-                    if (isImage) {
-                        resizers.forEach(resizer => {
-                            const path = serverPathToFile(Directory.images, filename + resizer.suffix + ext);
-                            createReadStream(savedName).pipe(resizer.resizer).pipe(createWriteStream(path));
-                        });
+            register({
+                  method: Method.POST,
+                  subscription: "/uploadURI",
+                  secureHandler: ({ req, res }) => {
+                        const uri = req.body.uri;
+                        const filename = req.body.name;
+                        const origSuffix = req.body.nosuffix ? SizeSuffix.None : SizeSuffix.Original;
+                        if (!uri || !filename) {
+                              res.status(401).send("incorrect parameters specified");
+                              return;
+                        }
+                        return imageDataUri.outputFile(uri, serverPathToFile(Directory.images, InjectSize(filename, origSuffix))).then((savedName: string) => {
+                              const ext = extname(savedName).toLowerCase();
+                              const { pngs, jpgs } = AcceptableMedia;
+                              const resizers = !origSuffix ? [{ resizer: sharp().resize(400, undefined, { withoutEnlargement: true }), suffix: "_m" }] : [
+                                    { resizer: sharp().resize(100, undefined, { withoutEnlargement: true }), suffix: "_s" },
+                                    { resizer: sharp().resize(400, undefined, { withoutEnlargement: true }), suffix: "_m" },
+                                    { resizer: sharp().resize(900, undefined, { withoutEnlargement: true }), suffix: "_l" },
+                              ];
+                              let isImage = false;
+                              if (pngs.includes(ext)) {
+                                    resizers.forEach(element => {
+                                          element.resizer = element.resizer.png();
+                                    });
+                                    isImage = true;
+                              } else if (jpgs.includes(ext)) {
+                                    resizers.forEach(element => {
+                                          element.resizer = element.resizer.jpeg();
+                                    });
+                                    isImage = true;
+                              }
+                              if (isImage) {
+                                    resizers.forEach(resizer => {
+                                          const path = serverPathToFile(Directory.images, filename + resizer.suffix + ext);
+                                          createReadStream(savedName).pipe(resizer.resizer).pipe(createWriteStream(path));
+                                    });
 
-                    }
-                    res.send(clientPathToFile(Directory.images, filename + ext));
-                });
-            }
-        });
+                              }
+                              res.send(clientPathToFile(Directory.images, filename + ext));
+                        });
+                  }
+            });
 
-    }
+      }
 
 }
 function delay(ms: number) {
-    return new Promise(resolve => setTimeout(resolve, ms));
+      return new Promise(resolve => setTimeout(resolve, ms));
 }
 /**
  * On success, returns a buffer containing the bytes of a screenshot
@@ -309,25 +309,25 @@ function delay(ms: number) {
  * On failure, returns undefined.
  */
 async function captureYoutubeScreenshot(targetUrl: string) {
-    // const browser = await launch({ args: ['--no-sandbox', '--disable-setuid-sandbox'] });
-    // const page = await browser.newPage();
-    // // await page.setViewport({ width: 1920, height: 1080 });
+      // const browser = await launch({ args: ['--no-sandbox', '--disable-setuid-sandbox'] });
+      // const page = await browser.newPage();
+      // // await page.setViewport({ width: 1920, height: 1080 });
 
-    // // await page.goto(targetUrl, { waitUntil: 'domcontentloaded' as any });
+      // // await page.goto(targetUrl, { waitUntil: 'domcontentloaded' as any });
 
-    // const videoPlayer = await page.$('.html5-video-player');
-    // videoPlayer && await page.focus("video");
-    // await delay(7000);
-    // const ad = await page.$('.ytp-ad-skip-button-text');
-    // await ad?.click();
-    // await videoPlayer?.click();
-    // await delay(1000);
-    // // hide youtube player controls.
-    // await page.evaluate(() => (document.querySelector('.ytp-chrome-bottom') as HTMLElement).style.display = 'none');
+      // const videoPlayer = await page.$('.html5-video-player');
+      // videoPlayer && await page.focus("video");
+      // await delay(7000);
+      // const ad = await page.$('.ytp-ad-skip-button-text');
+      // await ad?.click();
+      // await videoPlayer?.click();
+      // await delay(1000);
+      // // hide youtube player controls.
+      // await page.evaluate(() => (document.querySelector('.ytp-chrome-bottom') as HTMLElement).style.display = 'none');
 
-    // const buffer = await videoPlayer?.screenshot({ encoding: "binary" });
-    // await browser.close();
+      // const buffer = await videoPlayer?.screenshot({ encoding: "binary" });
+      // await browser.close();
 
-    // return buffer;
-    return null;
+      // return buffer;
+      return null;
 }
\ No newline at end of file
-- 
cgit v1.2.3-70-g09d2


From ddcab916cf125817bb532b7c34617e50bcc3840d Mon Sep 17 00:00:00 2001
From: Jenny Yu <jennyyu212@outlook.com>
Date: Tue, 26 Apr 2022 01:39:51 -0400
Subject: feat: uploading works

---
 src/client/Network.ts                              |   3 -
 .../views/nodes/RecordingBox/RecordingBox.tsx      |   9 +-
 .../views/nodes/RecordingBox/RecordingView.tsx     |  27 +-
 src/client/views/nodes/ScreenshotBox.tsx           | 429 +++++++++++----------
 4 files changed, 240 insertions(+), 228 deletions(-)

(limited to 'src/client/views/nodes/RecordingBox/RecordingBox.tsx')

diff --git a/src/client/Network.ts b/src/client/Network.ts
index 58f356c90..3597e7b2b 100644
--- a/src/client/Network.ts
+++ b/src/client/Network.ts
@@ -19,7 +19,6 @@ export namespace Networking {
       }
 
       export async function UploadFilesToServer<T extends Upload.FileInformation = Upload.FileInformation>(files: File | File[]): Promise<Upload.FileResponse<T>[]> {
-            console.log(files)
             const formData = new FormData();
             if (Array.isArray(files)) {
                   if (!files.length) {
@@ -34,8 +33,6 @@ export namespace Networking {
                   body: formData
             };
             const response = await fetch("/uploadFormData", parameters);
-            console.log(response)
-            // console.log(response.json())
             return response.json();
       }
 
diff --git a/src/client/views/nodes/RecordingBox/RecordingBox.tsx b/src/client/views/nodes/RecordingBox/RecordingBox.tsx
index 1e17e2ddd..4c7fdd278 100644
--- a/src/client/views/nodes/RecordingBox/RecordingBox.tsx
+++ b/src/client/views/nodes/RecordingBox/RecordingBox.tsx
@@ -1,12 +1,13 @@
 import { action, observable } from "mobx";
 import { observer } from "mobx-react";
 import * as React from "react";
-import { AudioField } from "../../../../fields/URLField";
+import { AudioField, VideoField } from "../../../../fields/URLField";
 import { Upload } from "../../../../server/SharedMediaTypes";
 import { ViewBoxBaseComponent } from "../../DocComponent";
 import { FieldView } from "../FieldView";
 import { VideoBox } from "../VideoBox";
 import { RecordingView } from './RecordingView';
+import { DocumentType } from "../../../documents/DocumentTypes";
 
 
 @observer
@@ -27,7 +28,11 @@ export class RecordingBox extends ViewBoxBaseComponent() {
             console.log("Setting result to " + info)
             this.result = info
             console.log(this.result.accessPaths.agnostic.client)
-            this.props.Document[this.fieldKey] = new AudioField(this.result.accessPaths.agnostic.client);
+            this.dataDoc.type = DocumentType.VID;
+            this.layoutDoc.layout = VideoBox.LayoutString(this.fieldKey);
+            this.dataDoc.nativeWidth = this.dataDoc.nativeHeight = undefined;
+            this.layoutDoc._fitWidth = undefined;
+            this.dataDoc[this.props.fieldKey] = new VideoField(this.result.accessPaths.agnostic.client);
       }
 
       render() {
diff --git a/src/client/views/nodes/RecordingBox/RecordingView.tsx b/src/client/views/nodes/RecordingBox/RecordingView.tsx
index 0d24f3c74..3a79f790f 100644
--- a/src/client/views/nodes/RecordingBox/RecordingView.tsx
+++ b/src/client/views/nodes/RecordingBox/RecordingView.tsx
@@ -27,7 +27,6 @@ interface IRecordingViewProps {
 const MAXTIME = 1000;
 
 export function RecordingView(props: IRecordingViewProps) {
-      const { setResult } = props
 
       const [recording, setRecording] = useState(false);
       const recordingTimerRef = useRef<number>(0);
@@ -52,11 +51,11 @@ export function RecordingView(props: IRecordingViewProps) {
                   width: 1280,
                   height: 720,
             },
-            audio: {
-                  echoCancellation: true,
-                  noiseSuppression: true,
-                  sampleRate: 44100
-            }
+            // audio: {
+            //       echoCancellation: true,
+            //       noiseSuppression: true,
+            //       sampleRate: 44100
+            // }
       }
 
       useEffect(() => {
@@ -75,7 +74,7 @@ export function RecordingView(props: IRecordingViewProps) {
                         type: 'video/webm'
                   })
                   const blobUrl = URL.createObjectURL(blob)
-                  const videoFile = new File([blob], "video", { lastModified: new Date().getDate() });
+                  const videoFile = new File(allVideoChunks, "video", { type: allVideoChunks[0].type, lastModified: Date.now() });
 
                   videoElementRef.current!.srcObject = null
                   videoElementRef.current!.src = blobUrl
@@ -89,12 +88,20 @@ export function RecordingView(props: IRecordingViewProps) {
                   //       }
                   // }
 
-                  Networking.UploadFilesToServer(allVideoChunks)
+                  Networking.UploadFilesToServer(videoFile)
                         .then((data) => {
-                              console.log(data)
+                              const result = data[0].result
+                              if (!(result instanceof Error)) { // convert this screenshotBox into normal videoBox
+                                    props.setResult(result)
+                              } else {
+                                    alert("video conversion failed");
+                              }
                         })
                   // uploadVideo()
 
+                  //     this.dataDoc[this.fieldKey + "-duration"] = (new Date().getTime() - this.recordingStart!) / 1000;
+
+
 
                   // change to one recording box
             }
@@ -150,6 +157,8 @@ export function RecordingView(props: IRecordingViewProps) {
       }
       const startShowingStream = async (mediaConstraints = DEFAULT_MEDIA_CONSTRAINTS) => {
             const stream = await navigator.mediaDevices.getUserMedia(mediaConstraints)
+            // const stream = await navigator.mediaDevices.getUserMedia({ video: true });
+
 
             videoElementRef.current!.src = ""
             videoElementRef.current!.srcObject = stream
diff --git a/src/client/views/nodes/ScreenshotBox.tsx b/src/client/views/nodes/ScreenshotBox.tsx
index dbb567d3a..0c7d7dc31 100644
--- a/src/client/views/nodes/ScreenshotBox.tsx
+++ b/src/client/views/nodes/ScreenshotBox.tsx
@@ -26,7 +26,7 @@ import { FormattedTextBox } from "./formattedText/FormattedTextBox";
 import "./ScreenshotBox.scss";
 import { VideoBox } from "./VideoBox";
 declare class MediaRecorder {
-    constructor(e: any, options?: any);  // whatever MediaRecorder has
+      constructor(e: any, options?: any);  // whatever MediaRecorder has
 }
 
 // interface VideoTileProps {
@@ -107,225 +107,226 @@ declare class MediaRecorder {
 
 @observer
 export class ScreenshotBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps & FieldViewProps>() {
-    public static LayoutString(fieldKey: string) { return FieldView.LayoutString(ScreenshotBox, fieldKey); }
-    private _audioRec: any;
-    private _videoRec: any;
-    @observable private _videoRef: HTMLVideoElement | null = null;
-    @observable _screenCapture = false;
-    @computed get recordingStart() { return Cast(this.dataDoc[this.props.fieldKey + "-recordingStart"], DateField)?.date.getTime(); }
+      public static LayoutString(fieldKey: string) { return FieldView.LayoutString(ScreenshotBox, fieldKey); }
+      private _audioRec: any;
+      private _videoRec: any;
+      @observable private _videoRef: HTMLVideoElement | null = null;
+      @observable _screenCapture = false;
+      @computed get recordingStart() { return Cast(this.dataDoc[this.props.fieldKey + "-recordingStart"], DateField)?.date.getTime(); }
 
-    constructor(props: any) {
-        super(props);
-        if (this.rootDoc.videoWall) {
-            this.rootDoc.nativeWidth = undefined;
-            this.rootDoc.nativeHeight = undefined;
-            this.layoutDoc.popOff = 0;
-            this.layoutDoc.popOut = 1;
-        } else {
-            this.setupDictation();
-        }
-    }
-    getAnchor = () => {
-        const startTime = Cast(this.layoutDoc._currentTimecode, "number", null) || (this._videoRec ? (Date.now() - (this.recordingStart || 0)) / 1000 : undefined);
-        return CollectionStackedTimeline.createAnchor(this.rootDoc, this.dataDoc, this.annotationKey, "_timecodeToShow", "_timecodeToHide",
-            startTime, startTime === undefined ? undefined : startTime + 3)
-            || this.rootDoc;
-    }
+      constructor(props: any) {
+            super(props);
+            if (this.rootDoc.videoWall) {
+                  this.rootDoc.nativeWidth = undefined;
+                  this.rootDoc.nativeHeight = undefined;
+                  this.layoutDoc.popOff = 0;
+                  this.layoutDoc.popOut = 1;
+            } else {
+                  this.setupDictation();
+            }
+      }
+      getAnchor = () => {
+            const startTime = Cast(this.layoutDoc._currentTimecode, "number", null) || (this._videoRec ? (Date.now() - (this.recordingStart || 0)) / 1000 : undefined);
+            return CollectionStackedTimeline.createAnchor(this.rootDoc, this.dataDoc, this.annotationKey, "_timecodeToShow", "_timecodeToHide",
+                  startTime, startTime === undefined ? undefined : startTime + 3)
+                  || this.rootDoc;
+      }
 
-    videoLoad = () => {
-        const aspect = this._videoRef!.videoWidth / this._videoRef!.videoHeight;
-        const nativeWidth = Doc.NativeWidth(this.layoutDoc);
-        const nativeHeight = Doc.NativeHeight(this.layoutDoc);
-        if (!nativeWidth || !nativeHeight) {
-            if (!nativeWidth) Doc.SetNativeWidth(this.dataDoc, 1200);
-            Doc.SetNativeHeight(this.dataDoc, (nativeWidth || 1200) / aspect);
-            this.layoutDoc._height = (this.layoutDoc[WidthSym]() || 0) / aspect;
-        }
-    }
+      videoLoad = () => {
+            const aspect = this._videoRef!.videoWidth / this._videoRef!.videoHeight;
+            const nativeWidth = Doc.NativeWidth(this.layoutDoc);
+            const nativeHeight = Doc.NativeHeight(this.layoutDoc);
+            if (!nativeWidth || !nativeHeight) {
+                  if (!nativeWidth) Doc.SetNativeWidth(this.dataDoc, 1200);
+                  Doc.SetNativeHeight(this.dataDoc, (nativeWidth || 1200) / aspect);
+                  this.layoutDoc._height = (this.layoutDoc[WidthSym]() || 0) / aspect;
+            }
+      }
 
-    componentDidMount() {
-        this.dataDoc.nativeWidth = this.dataDoc.nativeHeight = 0;
-        this.props.setContentView?.(this); // this tells the DocumentView that this ScreenshotBox is the "content" of the document.  this allows the DocumentView to indirectly call getAnchor() on the AudioBox when making a link.
-        // this.rootDoc.videoWall && reaction(() => ({ width: this.props.PanelWidth(), height: this.props.PanelHeight() }),
-        //     ({ width, height }) => {
-        //         if (this._camera) {
-        //             const angle = -Math.abs(1 - width / height);
-        //             const xz = [0, (this._numScreens - 2) / Math.abs(1 + angle)];
-        //             this._camera.position.set(this._numScreens / 2 + xz[1] * Math.sin(angle), this._numScreens / 2, xz[1] * Math.cos(angle));
-        //             this._camera.lookAt(this._numScreens / 2, this._numScreens / 2, 0);
-        //             (this._camera as any).updateProjectionMatrix();
-        //         }
-        //     });
-    }
-    componentWillUnmount() {
-        const ind = DocUtils.ActiveRecordings.indexOf(this);
-        ind !== -1 && (DocUtils.ActiveRecordings.splice(ind, 1));
-    }
+      componentDidMount() {
+            this.dataDoc.nativeWidth = this.dataDoc.nativeHeight = 0;
+            this.props.setContentView?.(this); // this tells the DocumentView that this ScreenshotBox is the "content" of the document.  this allows the DocumentView to indirectly call getAnchor() on the AudioBox when making a link.
+            // this.rootDoc.videoWall && reaction(() => ({ width: this.props.PanelWidth(), height: this.props.PanelHeight() }),
+            //     ({ width, height }) => {
+            //         if (this._camera) {
+            //             const angle = -Math.abs(1 - width / height);
+            //             const xz = [0, (this._numScreens - 2) / Math.abs(1 + angle)];
+            //             this._camera.position.set(this._numScreens / 2 + xz[1] * Math.sin(angle), this._numScreens / 2, xz[1] * Math.cos(angle));
+            //             this._camera.lookAt(this._numScreens / 2, this._numScreens / 2, 0);
+            //             (this._camera as any).updateProjectionMatrix();
+            //         }
+            //     });
+      }
+      componentWillUnmount() {
+            const ind = DocUtils.ActiveRecordings.indexOf(this);
+            ind !== -1 && (DocUtils.ActiveRecordings.splice(ind, 1));
+      }
 
-    specificContextMenu = (e: React.MouseEvent): void => {
-        const subitems = [{ description: "Screen Capture", event: this.toggleRecording, icon: "expand-arrows-alt" as any }];
-        ContextMenu.Instance.addItem({ description: "Options...", subitems, icon: "video" });
-    }
+      specificContextMenu = (e: React.MouseEvent): void => {
+            const subitems = [{ description: "Screen Capture", event: this.toggleRecording, icon: "expand-arrows-alt" as any }];
+            ContextMenu.Instance.addItem({ description: "Options...", subitems, icon: "video" });
+      }
 
-    @computed get content() {
-        if (this.rootDoc.videoWall) return (null);
-        return <video className={"videoBox-content"} key="video"
-            ref={r => {
-                this._videoRef = r;
-                setTimeout(() => {
-                    if (this.rootDoc.mediaState === "pendingRecording" && this._videoRef) {
-                        this.toggleRecording();
-                    }
-                }, 100);
-            }}
-            autoPlay={this._screenCapture}
-            style={{ width: this._screenCapture ? "100%" : undefined, height: this._screenCapture ? "100%" : undefined }}
-            onCanPlay={this.videoLoad}
-            controls={true}
-            onClick={e => e.preventDefault()}>
-            <source type="video/mp4" />
-            Not supported.
-        </video>;
-    }
+      @computed get content() {
+            if (this.rootDoc.videoWall) return (null);
+            return <video className={"videoBox-content"} key="video"
+                  ref={r => {
+                        this._videoRef = r;
+                        setTimeout(() => {
+                              if (this.rootDoc.mediaState === "pendingRecording" && this._videoRef) {
+                                    this.toggleRecording();
+                              }
+                        }, 100);
+                  }}
+                  autoPlay={this._screenCapture}
+                  style={{ width: this._screenCapture ? "100%" : undefined, height: this._screenCapture ? "100%" : undefined }}
+                  onCanPlay={this.videoLoad}
+                  controls={true}
+                  onClick={e => e.preventDefault()}>
+                  <source type="video/mp4" />
+                  Not supported.
+            </video>;
+      }
 
-    // _numScreens = 5;
-    // _camera: Camera | undefined;
-    // @observable _raised = [] as { coord: Vector2, off: Vector3 }[];
-    // @action setRaised = (r: { coord: Vector2, off: Vector3 }[]) => this._raised = r;
-    @computed get threed() {
-        // if (this.rootDoc.videoWall) {
-        //     const screens: any[] = [];
-        //     const colors = ["yellow", "red", "orange", "brown", "maroon", "gray"];
-        //     let count = 0;
-        //     numberRange(this._numScreens).forEach(x => numberRange(this._numScreens).forEach(y => screens.push(
-        //         <VideoTile rootDoc={this.rootDoc} color={colors[count++ % colors.length]} x={x} y={y} raised={this._raised} setRaised={this.setRaised} />)));
-        //     return <Canvas key="canvas" id="CANCAN" style={{ width: this.props.PanelWidth(), height: this.props.PanelHeight() }} gl={{ antialias: false }} colorManagement={false} onCreated={props => {
-        //         this._camera = props.camera;
-        //         props.camera.position.set(this._numScreens / 2, this._numScreens / 2, this._numScreens - 2);
-        //         props.camera.lookAt(this._numScreens / 2, this._numScreens / 2, 0);
-        //     }}>
-        //         {/* <ambientLight />*/}
-        //         <pointLight position={[10, 10, 10]} intensity={1} />
-        //         {screens}
-        //     </ Canvas>;
-        // }
-        return (null);
-    }
-    toggleRecording = async () => {
-        if (!this._screenCapture) {
-            this._audioRec = new MediaRecorder(await navigator.mediaDevices.getUserMedia({ audio: true }));
-            const aud_chunks: any = [];
-            this._audioRec.ondataavailable = (e: any) => aud_chunks.push(e.data);
-            this._audioRec.onstop = async (e: any) => {
-                const [{ result }] = await Networking.UploadFilesToServer(aud_chunks);
-                if (!(result instanceof Error)) {
-                    this.dataDoc[this.props.fieldKey + "-audio"] = new AudioField(result.accessPaths.agnostic.client);
-                }
-            };
-            this._videoRef!.srcObject = await (navigator.mediaDevices as any).getDisplayMedia({ video: true });
-            this._videoRec = new MediaRecorder(this._videoRef!.srcObject);
-            const vid_chunks: any = [];
-            this._videoRec.onstart = () => this.dataDoc[this.props.fieldKey + "-recordingStart"] = new DateField(new Date());
-            this._videoRec.ondataavailable = (e: any) => vid_chunks.push(e.data);
-            this._videoRec.onstop = async (e: any) => {
-                const file = new File(vid_chunks, `${this.rootDoc[Id]}.mkv`, { type: vid_chunks[0].type, lastModified: Date.now() });
-                const [{ result }] = await Networking.UploadFilesToServer(file);
-                this.dataDoc[this.fieldKey + "-duration"] = (new Date().getTime() - this.recordingStart!) / 1000;
-                if (!(result instanceof Error)) { // convert this screenshotBox into normal videoBox
-                    this.dataDoc.type = DocumentType.VID;
-                    this.layoutDoc.layout = VideoBox.LayoutString(this.fieldKey);
-                    this.dataDoc.nativeWidth = this.dataDoc.nativeHeight = undefined;
-                    this.layoutDoc._fitWidth = undefined;
-                    this.dataDoc[this.props.fieldKey] = new VideoField(result.accessPaths.agnostic.client);
-                } else alert("video conversion failed");
-            };
-            this._audioRec.start();
-            this._videoRec.start();
-            runInAction(() => {
-                this._screenCapture = true;
-                this.dataDoc.mediaState = "recording";
-            });
-            DocUtils.ActiveRecordings.push(this);
-        } else {
-            this._audioRec?.stop();
-            this._videoRec?.stop();
-            runInAction(() => {
-                this._screenCapture = false;
-                this.dataDoc.mediaState = "paused";
-            });
-            const ind = DocUtils.ActiveRecordings.indexOf(this);
-            ind !== -1 && (DocUtils.ActiveRecordings.splice(ind, 1));
+      // _numScreens = 5;
+      // _camera: Camera | undefined;
+      // @observable _raised = [] as { coord: Vector2, off: Vector3 }[];
+      // @action setRaised = (r: { coord: Vector2, off: Vector3 }[]) => this._raised = r;
+      @computed get threed() {
+            // if (this.rootDoc.videoWall) {
+            //     const screens: any[] = [];
+            //     const colors = ["yellow", "red", "orange", "brown", "maroon", "gray"];
+            //     let count = 0;
+            //     numberRange(this._numScreens).forEach(x => numberRange(this._numScreens).forEach(y => screens.push(
+            //         <VideoTile rootDoc={this.rootDoc} color={colors[count++ % colors.length]} x={x} y={y} raised={this._raised} setRaised={this.setRaised} />)));
+            //     return <Canvas key="canvas" id="CANCAN" style={{ width: this.props.PanelWidth(), height: this.props.PanelHeight() }} gl={{ antialias: false }} colorManagement={false} onCreated={props => {
+            //         this._camera = props.camera;
+            //         props.camera.position.set(this._numScreens / 2, this._numScreens / 2, this._numScreens - 2);
+            //         props.camera.lookAt(this._numScreens / 2, this._numScreens / 2, 0);
+            //     }}>
+            //         {/* <ambientLight />*/}
+            //         <pointLight position={[10, 10, 10]} intensity={1} />
+            //         {screens}
+            //     </ Canvas>;
+            // }
+            return (null);
+      }
+      toggleRecording = async () => {
+            if (!this._screenCapture) {
+                  this._audioRec = new MediaRecorder(await navigator.mediaDevices.getUserMedia({ audio: true }));
+                  const aud_chunks: any = [];
+                  this._audioRec.ondataavailable = (e: any) => aud_chunks.push(e.data);
+                  this._audioRec.onstop = async (e: any) => {
+                        const [{ result }] = await Networking.UploadFilesToServer(aud_chunks);
+                        if (!(result instanceof Error)) {
+                              this.dataDoc[this.props.fieldKey + "-audio"] = new AudioField(result.accessPaths.agnostic.client);
+                        }
+                  };
+                  this._videoRef!.srcObject = await (navigator.mediaDevices as any).getDisplayMedia({ video: true });
+                  this._videoRec = new MediaRecorder(this._videoRef!.srcObject);
+                  const vid_chunks: any = [];
+                  this._videoRec.onstart = () => this.dataDoc[this.props.fieldKey + "-recordingStart"] = new DateField(new Date());
+                  this._videoRec.ondataavailable = (e: any) => vid_chunks.push(e.data);
+                  this._videoRec.onstop = async (e: any) => {
+                        console.log("screenshotbox: upload")
+                        const file = new File(vid_chunks, `${this.rootDoc[Id]}.mkv`, { type: vid_chunks[0].type, lastModified: Date.now() });
+                        const [{ result }] = await Networking.UploadFilesToServer(file);
+                        this.dataDoc[this.fieldKey + "-duration"] = (new Date().getTime() - this.recordingStart!) / 1000;
+                        if (!(result instanceof Error)) { // convert this screenshotBox into normal videoBox
+                              this.dataDoc.type = DocumentType.VID;
+                              this.layoutDoc.layout = VideoBox.LayoutString(this.fieldKey);
+                              this.dataDoc.nativeWidth = this.dataDoc.nativeHeight = undefined;
+                              this.layoutDoc._fitWidth = undefined;
+                              this.dataDoc[this.props.fieldKey] = new VideoField(result.accessPaths.agnostic.client);
+                        } else alert("video conversion failed");
+                  };
+                  this._audioRec.start();
+                  this._videoRec.start();
+                  runInAction(() => {
+                        this._screenCapture = true;
+                        this.dataDoc.mediaState = "recording";
+                  });
+                  DocUtils.ActiveRecordings.push(this);
+            } else {
+                  this._audioRec?.stop();
+                  this._videoRec?.stop();
+                  runInAction(() => {
+                        this._screenCapture = false;
+                        this.dataDoc.mediaState = "paused";
+                  });
+                  const ind = DocUtils.ActiveRecordings.indexOf(this);
+                  ind !== -1 && (DocUtils.ActiveRecordings.splice(ind, 1));
 
-            CaptureManager.Instance.open(this.rootDoc);
-        }
-    }
+                  CaptureManager.Instance.open(this.rootDoc);
+            }
+      }
 
-    setupDictation = () => {
-        if (this.dataDoc[this.fieldKey + "-dictation"]) return;
-        const dictationText = CurrentUserUtils.GetNewTextDoc("dictation",
-            NumCast(this.rootDoc.x), NumCast(this.rootDoc.y) + NumCast(this.layoutDoc._height) + 10,
-            NumCast(this.layoutDoc._width), 2 * NumCast(this.layoutDoc._height));
-        dictationText._autoHeight = false;
-        const dictationTextProto = Doc.GetProto(dictationText);
-        dictationTextProto.recordingSource = this.dataDoc;
-        dictationTextProto.recordingStart = ComputedField.MakeFunction(`self.recordingSource["${this.props.fieldKey}-recordingStart"]`);
-        dictationTextProto.mediaState = ComputedField.MakeFunction("self.recordingSource.mediaState");
-        this.dataDoc[this.fieldKey + "-dictation"] = dictationText;
-    }
-    contentFunc = () => [this.threed, this.content];
-    videoPanelHeight = () => NumCast(this.dataDoc[this.fieldKey + "-nativeHeight"], this.layoutDoc[HeightSym]()) / NumCast(this.dataDoc[this.fieldKey + "-nativeWidth"], this.layoutDoc[WidthSym]()) * this.props.PanelWidth();
-    formattedPanelHeight = () => Math.max(0, this.props.PanelHeight() - this.videoPanelHeight());
-    render() {
-        TraceMobx();
-        return <div className="videoBox" onContextMenu={this.specificContextMenu} style={{ width: "100%", height: "100%" }} >
-            <div className="videoBox-viewer" >
-                <div style={{ position: "relative", height: this.videoPanelHeight() }}>
-                    <CollectionFreeFormView {...OmitKeys(this.props, ["NativeWidth", "NativeHeight"]).omit}
-                        PanelHeight={this.videoPanelHeight}
-                        PanelWidth={this.props.PanelWidth}
-                        focus={this.props.focus}
-                        isSelected={this.props.isSelected}
-                        isAnnotationOverlay={true}
-                        select={emptyFunction}
-                        isContentActive={returnFalse}
-                        scaling={returnOne}
-                        whenChildContentsActiveChanged={emptyFunction}
-                        removeDocument={returnFalse}
-                        moveDocument={returnFalse}
-                        addDocument={returnFalse}
-                        CollectionView={undefined}
-                        ScreenToLocalTransform={this.props.ScreenToLocalTransform}
-                        renderDepth={this.props.renderDepth + 1}
-                        ContainingCollectionDoc={this.props.ContainingCollectionDoc}>
-                        {this.contentFunc}
-                    </CollectionFreeFormView></div>
-                <div style={{ position: "relative", height: this.formattedPanelHeight() }}>
-                    {!(this.dataDoc[this.fieldKey + "-dictation"] instanceof Doc) ? (null) :
-                        <FormattedTextBox {...OmitKeys(this.props, ["NativeWidth", "NativeHeight"]).omit}
-                            Document={this.dataDoc[this.fieldKey + "-dictation"]}
-                            fieldKey={"text"}
-                            PanelHeight={this.formattedPanelHeight}
-                            isAnnotationOverlay={true}
-                            select={emptyFunction}
-                            isContentActive={emptyFunction}
-                            scaling={returnOne}
-                            xPadding={25}
-                            yPadding={10}
-                            whenChildContentsActiveChanged={emptyFunction}
-                            removeDocument={returnFalse}
-                            moveDocument={returnFalse}
-                            addDocument={returnFalse}
-                            CollectionView={undefined}
-                            renderDepth={this.props.renderDepth + 1}
-                            ContainingCollectionDoc={this.props.ContainingCollectionDoc}>
-                        </FormattedTextBox>}
-                </div>
-            </div>
-            {!this.props.isSelected() ? (null) : <div className="screenshotBox-uiButtons">
-                <div className="screenshotBox-recorder" key="snap" onPointerDown={this.toggleRecording} >
-                    <FontAwesomeIcon icon="file" size="lg" />
-                </div>
-            </div>}
-        </div >;
-    }
+      setupDictation = () => {
+            if (this.dataDoc[this.fieldKey + "-dictation"]) return;
+            const dictationText = CurrentUserUtils.GetNewTextDoc("dictation",
+                  NumCast(this.rootDoc.x), NumCast(this.rootDoc.y) + NumCast(this.layoutDoc._height) + 10,
+                  NumCast(this.layoutDoc._width), 2 * NumCast(this.layoutDoc._height));
+            dictationText._autoHeight = false;
+            const dictationTextProto = Doc.GetProto(dictationText);
+            dictationTextProto.recordingSource = this.dataDoc;
+            dictationTextProto.recordingStart = ComputedField.MakeFunction(`self.recordingSource["${this.props.fieldKey}-recordingStart"]`);
+            dictationTextProto.mediaState = ComputedField.MakeFunction("self.recordingSource.mediaState");
+            this.dataDoc[this.fieldKey + "-dictation"] = dictationText;
+      }
+      contentFunc = () => [this.threed, this.content];
+      videoPanelHeight = () => NumCast(this.dataDoc[this.fieldKey + "-nativeHeight"], this.layoutDoc[HeightSym]()) / NumCast(this.dataDoc[this.fieldKey + "-nativeWidth"], this.layoutDoc[WidthSym]()) * this.props.PanelWidth();
+      formattedPanelHeight = () => Math.max(0, this.props.PanelHeight() - this.videoPanelHeight());
+      render() {
+            TraceMobx();
+            return <div className="videoBox" onContextMenu={this.specificContextMenu} style={{ width: "100%", height: "100%" }} >
+                  <div className="videoBox-viewer" >
+                        <div style={{ position: "relative", height: this.videoPanelHeight() }}>
+                              <CollectionFreeFormView {...OmitKeys(this.props, ["NativeWidth", "NativeHeight"]).omit}
+                                    PanelHeight={this.videoPanelHeight}
+                                    PanelWidth={this.props.PanelWidth}
+                                    focus={this.props.focus}
+                                    isSelected={this.props.isSelected}
+                                    isAnnotationOverlay={true}
+                                    select={emptyFunction}
+                                    isContentActive={returnFalse}
+                                    scaling={returnOne}
+                                    whenChildContentsActiveChanged={emptyFunction}
+                                    removeDocument={returnFalse}
+                                    moveDocument={returnFalse}
+                                    addDocument={returnFalse}
+                                    CollectionView={undefined}
+                                    ScreenToLocalTransform={this.props.ScreenToLocalTransform}
+                                    renderDepth={this.props.renderDepth + 1}
+                                    ContainingCollectionDoc={this.props.ContainingCollectionDoc}>
+                                    {this.contentFunc}
+                              </CollectionFreeFormView></div>
+                        <div style={{ position: "relative", height: this.formattedPanelHeight() }}>
+                              {!(this.dataDoc[this.fieldKey + "-dictation"] instanceof Doc) ? (null) :
+                                    <FormattedTextBox {...OmitKeys(this.props, ["NativeWidth", "NativeHeight"]).omit}
+                                          Document={this.dataDoc[this.fieldKey + "-dictation"]}
+                                          fieldKey={"text"}
+                                          PanelHeight={this.formattedPanelHeight}
+                                          isAnnotationOverlay={true}
+                                          select={emptyFunction}
+                                          isContentActive={emptyFunction}
+                                          scaling={returnOne}
+                                          xPadding={25}
+                                          yPadding={10}
+                                          whenChildContentsActiveChanged={emptyFunction}
+                                          removeDocument={returnFalse}
+                                          moveDocument={returnFalse}
+                                          addDocument={returnFalse}
+                                          CollectionView={undefined}
+                                          renderDepth={this.props.renderDepth + 1}
+                                          ContainingCollectionDoc={this.props.ContainingCollectionDoc}>
+                                    </FormattedTextBox>}
+                        </div>
+                  </div>
+                  {!this.props.isSelected() ? (null) : <div className="screenshotBox-uiButtons">
+                        <div className="screenshotBox-recorder" key="snap" onPointerDown={this.toggleRecording} >
+                              <FontAwesomeIcon icon="file" size="lg" />
+                        </div>
+                  </div>}
+            </div >;
+      }
 }
\ No newline at end of file
-- 
cgit v1.2.3-70-g09d2


From 0a84982e2ea59d737f394bc79fa7f26a4bf29a80 Mon Sep 17 00:00:00 2001
From: Jenny Yu <jennyyu212@outlook.com>
Date: Thu, 28 Apr 2022 16:01:49 -0400
Subject: feat: uploading works

---
 package-lock.json                                  | 45 ++++++++++++++++++++--
 src/client/Network.ts                              |  1 +
 .../views/nodes/RecordingBox/RecordingBox.tsx      | 14 ++++++-
 .../views/nodes/RecordingBox/RecordingView.tsx     | 23 ++++-------
 src/client/views/nodes/trails/PresElementBox.tsx   |  2 +
 5 files changed, 65 insertions(+), 20 deletions(-)

(limited to 'src/client/views/nodes/RecordingBox/RecordingBox.tsx')

diff --git a/package-lock.json b/package-lock.json
index 0f045c546..684caa92e 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -768,9 +768,9 @@
             "integrity": "sha512-BJJH2OXdUreVNdfTCuuO2oHoqBmXSnXtIQvbRYj3LwZ27nnzfY87D/KEUPXnCXj/56kEeVlBbvAbcKKeMRmkSw==",
             "dependencies": {
                 "@typescript/lib-dom": {
-                    "version": "npm:@types/web@0.0.61",
-                    "resolved": "https://registry.npmjs.org/@types/web/-/web-0.0.61.tgz",
-                    "integrity": "sha512-lQ4M3OSh8eFj55fz1RJsLhCubET/7duOXoKcxVaUXYXyxikD9BA+WWrqAn5CjJxrc82b7zFY4OCVIeVdkPLaGg=="
+                    "version": "npm:@types/web@0.0.62",
+                    "resolved": "https://registry.npmjs.org/@types/web/-/web-0.0.62.tgz",
+                    "integrity": "sha512-NI9oDqfsnKHY/VI+4Uomw976LzopMphE2cCAv8cNNdd3LyA2aUolkipIDR5idnyX6Amq5oo2Lbdtj826h7gFjA=="
                 }
             }
         },
@@ -4775,6 +4775,16 @@
             "integrity": "sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk=",
             "dev": true
         },
+        "d": {
+            "version": "1.0.1",
+            "resolved": "https://registry.npmjs.org/d/-/d-1.0.1.tgz",
+            "integrity": "sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==",
+            "dev": true,
+            "requires": {
+                "es5-ext": "^0.10.50",
+                "type": "^1.0.1"
+            }
+        },
         "d3-array": {
             "version": "2.12.1",
             "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz",
@@ -5882,6 +5892,28 @@
                 "is-symbol": "^1.0.2"
             }
         },
+        "es5-ext": {
+            "version": "0.10.61",
+            "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.61.tgz",
+            "integrity": "sha512-yFhIqQAzu2Ca2I4SE2Au3rxVfmohU9Y7wqGR+s7+H7krk26NXhIRAZDgqd6xqjCEFUomDEA3/Bo/7fKmIkW1kA==",
+            "dev": true,
+            "requires": {
+                "es6-iterator": "^2.0.3",
+                "es6-symbol": "^3.1.3",
+                "next-tick": "^1.1.0"
+            }
+        },
+        "es6-iterator": {
+            "version": "2.0.3",
+            "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz",
+            "integrity": "sha1-p96IkUGgWpSwhUQDstCg+/qY87c=",
+            "dev": true,
+            "requires": {
+                "d": "1",
+                "es5-ext": "^0.10.35",
+                "es6-symbol": "^3.1.1"
+            }
+        },
         "es6-promise": {
             "version": "3.2.1",
             "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.2.1.tgz",
@@ -5893,6 +5925,7 @@
             "integrity": "sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA==",
             "dev": true,
             "requires": {
+                "d": "^1.0.1",
                 "ext": "^1.1.2"
             }
         },
@@ -18441,6 +18474,12 @@
             "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
             "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q="
         },
+        "type": {
+            "version": "1.2.0",
+            "resolved": "https://registry.npmjs.org/type/-/type-1.2.0.tgz",
+            "integrity": "sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==",
+            "dev": true
+        },
         "type-check": {
             "version": "0.3.2",
             "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz",
diff --git a/src/client/Network.ts b/src/client/Network.ts
index 3597e7b2b..1255e5ce0 100644
--- a/src/client/Network.ts
+++ b/src/client/Network.ts
@@ -19,6 +19,7 @@ export namespace Networking {
       }
 
       export async function UploadFilesToServer<T extends Upload.FileInformation = Upload.FileInformation>(files: File | File[]): Promise<Upload.FileResponse<T>[]> {
+            console.log(files)
             const formData = new FormData();
             if (Array.isArray(files)) {
                   if (!files.length) {
diff --git a/src/client/views/nodes/RecordingBox/RecordingBox.tsx b/src/client/views/nodes/RecordingBox/RecordingBox.tsx
index 4c7fdd278..6f9451925 100644
--- a/src/client/views/nodes/RecordingBox/RecordingBox.tsx
+++ b/src/client/views/nodes/RecordingBox/RecordingBox.tsx
@@ -22,6 +22,12 @@ export class RecordingBox extends ViewBoxBaseComponent() {
       }
 
       @observable result: Upload.FileInformation | undefined = undefined
+      @observable videoDuration: number | undefined = undefined
+
+      @action
+      setVideoDuration = (duration: number) => {
+            this.videoDuration = duration
+      }
 
       @action
       setResult = (info: Upload.FileInformation) => {
@@ -29,6 +35,9 @@ export class RecordingBox extends ViewBoxBaseComponent() {
             this.result = info
             console.log(this.result.accessPaths.agnostic.client)
             this.dataDoc.type = DocumentType.VID;
+            console.log(this.videoDuration)
+            this.dataDoc[this.fieldKey + "-duration"] = this.videoDuration;
+
             this.layoutDoc.layout = VideoBox.LayoutString(this.fieldKey);
             this.dataDoc.nativeWidth = this.dataDoc.nativeHeight = undefined;
             this.layoutDoc._fitWidth = undefined;
@@ -37,8 +46,9 @@ export class RecordingBox extends ViewBoxBaseComponent() {
 
       render() {
             return <div className="recordingBox" ref={this._ref}>
-                  {!this.result ? <RecordingView setResult={this.setResult} /> :
-                        <p>video box</p>}
+                  {!this.result && <RecordingView setResult={this.setResult} setDuration={this.setVideoDuration} />}
+                  {/* {!this.result ? <RecordingView setResult={this.setResult} /> :
+                        <p>video box</p>} */}
             </div>;
       }
 }
diff --git a/src/client/views/nodes/RecordingBox/RecordingView.tsx b/src/client/views/nodes/RecordingBox/RecordingView.tsx
index 16f750e14..6aed07c60 100644
--- a/src/client/views/nodes/RecordingBox/RecordingView.tsx
+++ b/src/client/views/nodes/RecordingBox/RecordingView.tsx
@@ -22,6 +22,7 @@ interface MediaSegment {
 
 interface IRecordingViewProps {
       setResult: (info: Upload.FileInformation) => void
+      setDuration: (seconds: number) => void
 }
 
 const MAXTIME = 1000;
@@ -47,12 +48,12 @@ export function RecordingView(props: IRecordingViewProps) {
 
 
       const DEFAULT_MEDIA_CONSTRAINTS = {
-            video: true,
+            // video: true,
             // audio: true
-            // video: {
-            //       width: 1280,
-            //       height: 720,
-            // },
+            video: {
+                  width: 1280,
+                  height: 720,
+            },
             // audio: true,
             // audio: {
             //       echoCancellation: true,
@@ -64,6 +65,7 @@ export function RecordingView(props: IRecordingViewProps) {
       useEffect(() => {
 
             if (finished) {
+                  props.setDuration(recordingTimer * 100)
                   let allVideoChunks: any = []
                   console.log(videos)
                   videos.forEach((vid) => {
@@ -72,16 +74,7 @@ export function RecordingView(props: IRecordingViewProps) {
                   })
 
                   console.log(allVideoChunks)
-
-                  const blob = new Blob(allVideoChunks, {
-                        type: 'video/webm'
-                  })
-                  const blobUrl = URL.createObjectURL(blob)
-                  const videoFile = new File(allVideoChunks, "video", { type: allVideoChunks[0].type, lastModified: Date.now() });
-
-                  videoElementRef.current!.srcObject = null
-                  videoElementRef.current!.src = blobUrl
-                  videoElementRef.current!.muted = false
+                  const videoFile = new File(allVideoChunks, "video.mkv", { type: allVideoChunks[0].type, lastModified: Date.now() });
 
                   // const uploadVideo = async () => {
                   //       const [{ result }] = await Networking.UploadFilesToServer(videoFile);
diff --git a/src/client/views/nodes/trails/PresElementBox.tsx b/src/client/views/nodes/trails/PresElementBox.tsx
index aa7622736..92834fb07 100644
--- a/src/client/views/nodes/trails/PresElementBox.tsx
+++ b/src/client/views/nodes/trails/PresElementBox.tsx
@@ -290,6 +290,8 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
                   console.log("x: ", pt[0], "y: ", pt[1]);
                   recording.x = pt[0];
                   recording.y = pt[1];
+                  console.log(Doc.UserDoc().myOverlayDocs)
+                  // Doc.RemoveDocFromList((Doc.UserDoc().myOverlayDocs as Doc), undefined, this.rootDoc);
                   Doc.AddDocToList((Doc.UserDoc().myOverlayDocs as Doc), undefined, recording);
             }
 
-- 
cgit v1.2.3-70-g09d2


From 99ae2ccde9dbcf6bae75edea231d4b10c736a692 Mon Sep 17 00:00:00 2001
From: Jenny Yu <jennyyu212@outlook.com>
Date: Thu, 28 Apr 2022 17:09:13 -0400
Subject: feat: recording box turning into video + only one recording box

---
 package-lock.json                                  | 44 ++++++++++++++--------
 src/client/views/OverlayView.tsx                   |  4 +-
 src/client/views/nodes/DocumentView.tsx            |  2 +-
 .../views/nodes/RecordingBox/RecordingBox.tsx      |  4 +-
 src/client/views/nodes/trails/PresElementBox.tsx   | 16 +++++---
 5 files changed, 44 insertions(+), 26 deletions(-)

(limited to 'src/client/views/nodes/RecordingBox/RecordingBox.tsx')

diff --git a/package-lock.json b/package-lock.json
index 684caa92e..691f56160 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -4775,16 +4775,6 @@
             "integrity": "sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk=",
             "dev": true
         },
-        "d": {
-            "version": "1.0.1",
-            "resolved": "https://registry.npmjs.org/d/-/d-1.0.1.tgz",
-            "integrity": "sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==",
-            "dev": true,
-            "requires": {
-                "es5-ext": "^0.10.50",
-                "type": "^1.0.1"
-            }
-        },
         "d3-array": {
             "version": "2.12.1",
             "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz",
@@ -5812,8 +5802,8 @@
             "integrity": "sha512-/o+BXHmB7ocbHEAs6F2EnG0ogybVVUdkRunTT2glZU9XAaGmhqskrvKwqXuDfNjEO0LZKWdejEEpnq8aM0tOaw=="
         },
         "equation-editor-react": {
-            "version": "github:bobzel/equation-editor-react#75915e852b4b36a6a4cd3e1cbc80598da6b65227",
-            "from": "github:bobzel/equation-editor-react#useLocally",
+            "version": "git+ssh://git@github.com/bobzel/equation-editor-react.git#75915e852b4b36a6a4cd3e1cbc80598da6b65227",
+            "from": "equation-editor-react@github:bobzel/equation-editor-react#useLocally",
             "requires": {
                 "jquery": "^3.4.1",
                 "mathquill": "^0.10.1-a"
@@ -5912,6 +5902,18 @@
                 "d": "1",
                 "es5-ext": "^0.10.35",
                 "es6-symbol": "^3.1.1"
+            },
+            "dependencies": {
+                "d": {
+                    "version": "1.0.1",
+                    "resolved": "https://registry.npmjs.org/d/-/d-1.0.1.tgz",
+                    "integrity": "sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==",
+                    "dev": true,
+                    "requires": {
+                        "es5-ext": "^0.10.50",
+                        "type": "^1.0.1"
+                    }
+                }
             }
         },
         "es6-promise": {
@@ -5927,6 +5929,18 @@
             "requires": {
                 "d": "^1.0.1",
                 "ext": "^1.1.2"
+            },
+            "dependencies": {
+                "d": {
+                    "version": "1.0.1",
+                    "resolved": "https://registry.npmjs.org/d/-/d-1.0.1.tgz",
+                    "integrity": "sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==",
+                    "dev": true,
+                    "requires": {
+                        "es5-ext": "^0.10.50",
+                        "type": "^1.0.1"
+                    }
+                }
             }
         },
         "escalade": {
@@ -8002,14 +8016,14 @@
             "resolved": "https://registry.npmjs.org/image-size-stream/-/image-size-stream-1.1.0.tgz",
             "integrity": "sha1-Ivou2mbG31AQh0bacUkmSy0l+Gs=",
             "requires": {
-                "image-size": "github:netroy/image-size#da2c863807a3e9602617bdd357b0de3ab4a064c1",
+                "image-size": "image-size@git+https://github.com/netroy/image-size#da2c863807a3e9602617bdd357b0de3ab4a064c1",
                 "readable-stream": "^1.0.33",
                 "tryit": "^1.0.1"
             },
             "dependencies": {
                 "image-size": {
-                    "version": "github:netroy/image-size#da2c863807a3e9602617bdd357b0de3ab4a064c1",
-                    "from": "github:netroy/image-size#da2c863807a3e9602617bdd357b0de3ab4a064c1"
+                    "version": "git+ssh://git@github.com/netroy/image-size.git#da2c863807a3e9602617bdd357b0de3ab4a064c1",
+                    "from": "image-size@git+https://github.com/netroy/image-size#da2c863807a3e9602617bdd357b0de3ab4a064c1"
                 },
                 "isarray": {
                     "version": "0.0.1",
diff --git a/src/client/views/OverlayView.tsx b/src/client/views/OverlayView.tsx
index 806abb990..f86406997 100644
--- a/src/client/views/OverlayView.tsx
+++ b/src/client/views/OverlayView.tsx
@@ -187,8 +187,8 @@ export class OverlayView extends React.Component {
                               bringToFront={emptyFunction}
                               addDocument={undefined}
                               removeDocument={undefined}
-                              PanelWidth={returnOne}
-                              PanelHeight={returnOne}
+                              PanelWidth={() => NumCast(d._width)}
+                              PanelHeight={() => NumCast(d._height)}
                               ScreenToLocalTransform={Transform.Identity}
                               renderDepth={1}
                               isDocumentActive={returnTrue}
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 78c6c9776..f48173975 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -1317,7 +1317,7 @@ export class DocumentView extends React.Component<DocumentViewProps> {
             const yshift = () => (this.props.Document.isInkMask ? InkingStroke.MaskDim : Math.abs(this.Yshift) <= 0.001 ? this.props.PanelHeight() : undefined);
             const isPresTreeElement: boolean = this.props.treeViewDoc?.type === DocumentType.PRES;
             const isButton: boolean = this.props.Document.type === DocumentType.FONTICON || this.props.Document._viewType === CollectionViewType.Linear;
-            const isOverlay: boolean = CurrentUserUtils.OverlayDocs.includes(this.props.Document);
+            // const isOverlay: boolean = CurrentUserUtils.OverlayDocs.includes(this.props.Document);
             return (<div className="contentFittingDocumentView">
                   {!this.props.Document || !this.props.PanelWidth() ? (null) : (
                         <div className="contentFittingDocumentView-previewDoc" ref={this.ContentRef}
diff --git a/src/client/views/nodes/RecordingBox/RecordingBox.tsx b/src/client/views/nodes/RecordingBox/RecordingBox.tsx
index 6f9451925..86358e838 100644
--- a/src/client/views/nodes/RecordingBox/RecordingBox.tsx
+++ b/src/client/views/nodes/RecordingBox/RecordingBox.tsx
@@ -39,8 +39,8 @@ export class RecordingBox extends ViewBoxBaseComponent() {
             this.dataDoc[this.fieldKey + "-duration"] = this.videoDuration;
 
             this.layoutDoc.layout = VideoBox.LayoutString(this.fieldKey);
-            this.dataDoc.nativeWidth = this.dataDoc.nativeHeight = undefined;
-            this.layoutDoc._fitWidth = undefined;
+            // this.dataDoc.nativeWidth = this.dataDoc.nativeHeight = undefined;
+            // this.layoutDoc._fitWidth = undefined;
             this.dataDoc[this.props.fieldKey] = new VideoField(this.result.accessPaths.agnostic.client);
       }
 
diff --git a/src/client/views/nodes/trails/PresElementBox.tsx b/src/client/views/nodes/trails/PresElementBox.tsx
index 05f1b0365..f4f33482e 100644
--- a/src/client/views/nodes/trails/PresElementBox.tsx
+++ b/src/client/views/nodes/trails/PresElementBox.tsx
@@ -2,7 +2,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
 import { Tooltip } from "@material-ui/core";
 import { action, computed, IReactionDisposer, observable, reaction } from "mobx";
 import { observer } from "mobx-react";
-import { DataSym, Doc, Opt } from "../../../../fields/Doc";
+import { DataSym, Doc, DocListCast, Opt } from "../../../../fields/Doc";
 import { Id } from "../../../../fields/FieldSymbols";
 import { Cast, NumCast, StrCast } from "../../../../fields/Types";
 import { emptyFunction, returnEmptyDoclist, returnFalse, returnTrue, setupMoveUpEvents } from "../../../../Utils";
@@ -275,23 +275,27 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
       @action
       startRecording = (targetDoc: Doc, activeItem: Doc) => {
 
-            // TODO: Remove everything that already exists
+            // Remove every recording that already exists in overlay view
+            DocListCast((Doc.UserDoc().myOverlayDocs as Doc).data).forEach((doc) => {
+                  if (doc.slides !== null) {
+                        Doc.RemoveDocFromList((Doc.UserDoc().myOverlayDocs as Doc), undefined, doc);
+                  }
+            })
+
             if (activeItem.recording) {
                   // if we already have an existing recording
                   Doc.AddDocToList((Doc.UserDoc().myOverlayDocs as Doc), undefined, Cast(activeItem.recording, Doc, null));
 
             } else {
                   // if we dont have any recording
-                  console.log(targetDoc.title, activeItem.title);
                   const recording = Docs.Create.WebCamDocument("", { _width: 400, _height: 200, title: "recording", system: true, cloneFieldFilter: new List<string>(["system"]) });
+                  // attach the recording to the slide, and attach the slide to the recording
+                  recording.slides = activeItem
                   activeItem.recording = recording
                   // TODO: Figure out exactly where we want the video to appear
                   const pt = this.props.ScreenToLocalTransform().inverse().transformPoint(0, 0);
-                  console.log("x: ", pt[0], "y: ", pt[1]);
                   recording.x = pt[0];
                   recording.y = pt[1];
-                  console.log(Doc.UserDoc().myOverlayDocs)
-                  // Doc.RemoveDocFromList((Doc.UserDoc().myOverlayDocs as Doc), undefined, this.rootDoc);
                   Doc.AddDocToList((Doc.UserDoc().myOverlayDocs as Doc), undefined, recording);
             }
 
-- 
cgit v1.2.3-70-g09d2


From 92142bba66010a8792c00dc372228b92e151c8b3 Mon Sep 17 00:00:00 2001
From: Jenny Yu <jennyyu212@outlook.com>
Date: Wed, 4 May 2022 00:15:53 -0400
Subject: fix: removed clear previous button

---
 src/client/views/DocComponent.tsx                  | 396 ++++++++++-----------
 .../views/nodes/RecordingBox/RecordingBox.tsx      |   3 +-
 .../views/nodes/RecordingBox/RecordingView.scss    |   4 +-
 .../views/nodes/RecordingBox/RecordingView.tsx     |  15 +-
 src/client/views/nodes/trails/PresElementBox.tsx   |   2 +
 5 files changed, 208 insertions(+), 212 deletions(-)

(limited to 'src/client/views/nodes/RecordingBox/RecordingBox.tsx')

diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx
index 79aaf2158..103560a2a 100644
--- a/src/client/views/DocComponent.tsx
+++ b/src/client/views/DocComponent.tsx
@@ -17,223 +17,223 @@ import { Touchable } from './Touchable';
 
 ///  DocComponent returns a generic React base class used by views that don't have 'fieldKey' props (e.g.,CollectionFreeFormDocumentView, DocumentView)
 export interface DocComponentProps {
-    Document: Doc;
-    LayoutTemplate?: () => Opt<Doc>;
-    LayoutTemplateString?: string;
+      Document: Doc;
+      LayoutTemplate?: () => Opt<Doc>;
+      LayoutTemplateString?: string;
 }
 export function DocComponent<P extends DocComponentProps>() {
-    class Component extends Touchable<P> {
-        //TODO This might be pretty inefficient if doc isn't observed, because computed doesn't cache then
-        @computed get Document() { return this.props.Document; }
-        // This is the "The Document" -- it encapsulates, data, layout, and any templates
-        @computed get rootDoc() { return Cast(this.props.Document.rootDocument, Doc, null) || this.props.Document; }
-        // This is the rendering data of a document -- it may be "The Document", or it may be some template document that holds the rendering info
-        @computed get layoutDoc() { return this.props.LayoutTemplateString ? this.props.Document : Doc.Layout(this.props.Document, this.props.LayoutTemplate?.()); }
-        // This is the data part of a document -- ie, the data that is constant across all views of the document
-        @computed get dataDoc() { return this.props.Document[DataSym] as Doc; }
-
-        protected _multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer;
-    }
-    return Component;
+      class Component extends Touchable<P> {
+            //TODO This might be pretty inefficient if doc isn't observed, because computed doesn't cache then
+            @computed get Document() { return this.props.Document; }
+            // This is the "The Document" -- it encapsulates, data, layout, and any templates
+            @computed get rootDoc() { return Cast(this.props.Document.rootDocument, Doc, null) || this.props.Document; }
+            // This is the rendering data of a document -- it may be "The Document", or it may be some template document that holds the rendering info
+            @computed get layoutDoc() { return this.props.LayoutTemplateString ? this.props.Document : Doc.Layout(this.props.Document, this.props.LayoutTemplate?.()); }
+            // This is the data part of a document -- ie, the data that is constant across all views of the document
+            @computed get dataDoc() { return this.props.Document[DataSym] as Doc; }
+
+            protected _multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer;
+      }
+      return Component;
 }
 
 /// FieldViewBoxProps  -  a generic base class for field views that are not annotatable (e.g. InkingStroke, ColorBox)
 interface ViewBoxBaseProps {
-    Document: Doc;
-    DataDoc?: Doc;
-    ContainingCollectionDoc: Opt<Doc>;
-    DocumentView?: () => DocumentView;
-    fieldKey: string;
-    layerProvider?: (doc: Doc, assign?: boolean) => boolean;
-    isSelected: (outsideReaction?: boolean) => boolean;
-    isContentActive: () => boolean | undefined;
-    renderDepth: number;
-    rootSelected: (outsideReaction?: boolean) => boolean;
+      Document: Doc;
+      DataDoc?: Doc;
+      ContainingCollectionDoc: Opt<Doc>;
+      DocumentView?: () => DocumentView;
+      fieldKey: string;
+      layerProvider?: (doc: Doc, assign?: boolean) => boolean;
+      isSelected: (outsideReaction?: boolean) => boolean;
+      isContentActive: () => boolean | undefined;
+      renderDepth: number;
+      rootSelected: (outsideReaction?: boolean) => boolean;
 }
 export function ViewBoxBaseComponent<P extends ViewBoxBaseProps>() {
-    class Component extends Touchable<P> {
-        //TODO This might be pretty inefficient if doc isn't observed, because computed doesn't cache then
-        //@computed get Document(): T { return schemaCtor(this.props.Document); }
-
-        // This is the "The Document" -- it encapsulates, data, layout, and any templates
-        @computed get rootDoc() { return Cast(this.props.Document.rootDocument, Doc, null) || this.props.Document; }
-        // This is the rendering data of a document -- it may be "The Document", or it may be some template document that holds the rendering info
-        @computed get layoutDoc() { return Doc.Layout(this.props.Document); }
-        // This is the data part of a document -- ie, the data that is constant across all views of the document
-        @computed get dataDoc() { return this.props.DataDoc && (this.props.Document.isTemplateForField || this.props.Document.isTemplateDoc) ? this.props.DataDoc : this.props.Document[DataSym]; }
-
-        // key where data is stored
-        @computed get fieldKey() { return this.props.fieldKey; }
-
-        lookupField = (field: string) => ScriptCast(this.layoutDoc.lookupField)?.script.run({ self: this.layoutDoc, data: this.rootDoc, field: field, container: this.props.DocumentView?.().props.treeViewDoc ?? this.props.ContainingCollectionDoc }).result;
-
-        isContentActive = (outsideReaction?: boolean) => (
-            this.props.isContentActive?.() === false ? false :
-                (CurrentUserUtils.SelectedTool !== InkTool.None ||
-                    (this.props.isContentActive?.() || this.props.Document.forceActive ||
-                        this.props.isSelected(outsideReaction) ||
-                        this.props.rootSelected(outsideReaction)) ? true : undefined))
-        protected _multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer;
-    }
-    return Component;
+      class Component extends Touchable<P> {
+            //TODO This might be pretty inefficient if doc isn't observed, because computed doesn't cache then
+            //@computed get Document(): T { return schemaCtor(this.props.Document); }
+
+            // This is the "The Document" -- it encapsulates, data, layout, and any templates
+            @computed get rootDoc() { return Cast(this.props.Document.rootDocument, Doc, null) || this.props.Document; }
+            // This is the rendering data of a document -- it may be "The Document", or it may be some template document that holds the rendering info
+            @computed get layoutDoc() { return Doc.Layout(this.props.Document); }
+            // This is the data part of a document -- ie, the data that is constant across all views of the document
+            @computed get dataDoc() { return this.props.DataDoc && (this.props.Document.isTemplateForField || this.props.Document.isTemplateDoc) ? this.props.DataDoc : this.props.Document[DataSym]; }
+
+            // key where data is stored
+            @computed get fieldKey() { return this.props.fieldKey; }
+
+            lookupField = (field: string) => ScriptCast(this.layoutDoc.lookupField)?.script.run({ self: this.layoutDoc, data: this.rootDoc, field: field, container: this.props.DocumentView?.().props.treeViewDoc ?? this.props.ContainingCollectionDoc }).result;
+
+            isContentActive = (outsideReaction?: boolean) => (
+                  this.props.isContentActive?.() === false ? false :
+                        (CurrentUserUtils.SelectedTool !== InkTool.None ||
+                              (this.props.isContentActive?.() || this.props.Document.forceActive ||
+                                    this.props.isSelected(outsideReaction) ||
+                                    this.props.rootSelected(outsideReaction)) ? true : undefined))
+            protected _multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer;
+      }
+      return Component;
 }
 
 
 ///  DocAnnotatbleComponent -return a base class for React views of document fields that are annotatable *and* interactive when selected (e.g., pdf, image)
 export interface ViewBoxAnnotatableProps {
-    Document: Doc;
-    DataDoc?: Doc;
-    fieldKey: string;
-    filterAddDocument?: (doc: Doc[]) => boolean;  // allows a document that renders a Collection view to filter or modify any documents added to the collection (see PresBox for an example)
-    layerProvider?: (doc: Doc, assign?: boolean) => boolean;
-    isContentActive: () => boolean | undefined;
-    select: (isCtrlPressed: boolean) => void;
-    whenChildContentsActiveChanged: (isActive: boolean) => void;
-    isSelected: (outsideReaction?: boolean) => boolean;
-    rootSelected: (outsideReaction?: boolean) => boolean;
-    renderDepth: number;
-    isAnnotationOverlay?: boolean;
+      Document: Doc;
+      DataDoc?: Doc;
+      fieldKey: string;
+      filterAddDocument?: (doc: Doc[]) => boolean;  // allows a document that renders a Collection view to filter or modify any documents added to the collection (see PresBox for an example)
+      layerProvider?: (doc: Doc, assign?: boolean) => boolean;
+      isContentActive: () => boolean | undefined;
+      select: (isCtrlPressed: boolean) => void;
+      whenChildContentsActiveChanged: (isActive: boolean) => void;
+      isSelected: (outsideReaction?: boolean) => boolean;
+      rootSelected: (outsideReaction?: boolean) => boolean;
+      renderDepth: number;
+      isAnnotationOverlay?: boolean;
 }
 export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps>() {
-    class Component extends Touchable<P> {
-        @observable _annotationKeySuffix = () => "annotations";
-
-        @observable _isAnyChildContentActive = false;
-        //TODO This might be pretty inefficient if doc isn't observed, because computed doesn't cache then
-        @computed get Document() { return this.props.Document; }
-        // This is the "The Document" -- it encapsulates, data, layout, and any templates
-        @computed get rootDoc() { return Cast(this.props.Document.rootDocument, Doc, null) || this.props.Document; }
-        // This is the rendering data of a document -- it may be "The Document", or it may be some template document that holds the rendering info
-        @computed get layoutDoc() { return Doc.Layout(this.props.Document); }
-        // This is the data part of a document -- ie, the data that is constant across all views of the document
-        @computed get dataDoc() { return this.props.DataDoc && (this.props.Document.isTemplateForField || this.props.Document.isTemplateDoc) ? this.props.DataDoc : this.props.Document[DataSym]; }
-
-        // key where data is stored
-        @computed get fieldKey() { return this.props.fieldKey; }
-
-        isAnyChildContentActive = () => this._isAnyChildContentActive;
-
-        lookupField = (field: string) => ScriptCast((this.layoutDoc as any).lookupField)?.script.run({ self: this.layoutDoc, data: this.rootDoc, field: field }).result;
-
-        styleFromLayoutString = (scale: number) => {
-            const style: { [key: string]: any } = {};
-            const divKeys = ["width", "height", "fontSize", "transform", "left", "background", "left", "right", "top", "bottom", "pointerEvents", "position"];
-            const replacer = (match: any, expr: string, offset: any, string: any) => { // bcz: this executes a script to convert a property expression string:  { script }  into a value
-                return ScriptField.MakeFunction(expr, { self: Doc.name, this: Doc.name, scale: "number" })?.script.run({ self: this.rootDoc, this: this.layoutDoc, scale }).result?.toString() ?? "";
-            };
-            divKeys.map((prop: string) => {
-                const p = (this.props as any)[prop];
-                typeof p === "string" && (style[prop] = p?.replace(/{([^.'][^}']+)}/g, replacer));
-            });
-            return style;
-        }
-
-        protected _multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer;
-
-        @computed public get annotationKey() { return this.fieldKey + (this._annotationKeySuffix() ? "-" + this._annotationKeySuffix() : ""); }
-
-        @action.bound
-        removeDocument(doc: Doc | Doc[], annotationKey?: string, leavePushpin?: boolean): boolean {
-            const effectiveAcl = GetEffectiveAcl(this.dataDoc);
-            const indocs = doc instanceof Doc ? [doc] : doc;
-            const docs = indocs.filter(doc => [AclEdit, AclAdmin].includes(effectiveAcl) || GetEffectiveAcl(doc) === AclAdmin);
-            if (docs.length) {
-                setTimeout(() => docs.map(doc => { // this allows 'addDocument' to see the annotationOn field in order to create a pushin
-                    Doc.SetInPlace(doc, "isPushpin", undefined, true);
-                    doc.annotationOn === this.props.Document && Doc.SetInPlace(doc, "annotationOn", undefined, true);
-                }));
-                const targetDataDoc = this.dataDoc;
-                const value = DocListCast(targetDataDoc[annotationKey ?? this.annotationKey]);
-                const toRemove = value.filter(v => docs.includes(v));
-
-                if (toRemove.length !== 0) {
-                    const recent = Cast(Doc.UserDoc().myRecentlyClosedDocs, Doc) as Doc;
-                    toRemove.forEach(doc => {
-                        leavePushpin && DocUtils.LeavePushpin(doc, annotationKey ?? this.annotationKey);
-                        Doc.RemoveDocFromList(targetDataDoc, annotationKey ?? this.annotationKey, doc);
-                        doc.context = undefined;
-                        if (recent) {
-                            Doc.RemoveDocFromList(recent, "data", doc);
-                            Doc.AddDocToList(recent, "data", doc, undefined, true, true);
-                        }
-                    });
-                    this.isAnyChildContentActive() && this.props.select(false);
-                    return true;
-                }
+      class Component extends Touchable<P> {
+            @observable _annotationKeySuffix = () => "annotations";
+
+            @observable _isAnyChildContentActive = false;
+            //TODO This might be pretty inefficient if doc isn't observed, because computed doesn't cache then
+            @computed get Document() { return this.props.Document; }
+            // This is the "The Document" -- it encapsulates, data, layout, and any templates
+            @computed get rootDoc() { return Cast(this.props.Document.rootDocument, Doc, null) || this.props.Document; }
+            // This is the rendering data of a document -- it may be "The Document", or it may be some template document that holds the rendering info
+            @computed get layoutDoc() { return Doc.Layout(this.props.Document); }
+            // This is the data part of a document -- ie, the data that is constant across all views of the document
+            @computed get dataDoc() { return this.props.DataDoc && (this.props.Document.isTemplateForField || this.props.Document.isTemplateDoc) ? this.props.DataDoc : this.props.Document[DataSym]; }
+
+            // key where data is stored
+            @computed get fieldKey() { return this.props.fieldKey; }
+
+            isAnyChildContentActive = () => this._isAnyChildContentActive;
+
+            lookupField = (field: string) => ScriptCast((this.layoutDoc as any).lookupField)?.script.run({ self: this.layoutDoc, data: this.rootDoc, field: field }).result;
+
+            styleFromLayoutString = (scale: number) => {
+                  const style: { [key: string]: any } = {};
+                  const divKeys = ["width", "height", "fontSize", "transform", "left", "background", "left", "right", "top", "bottom", "pointerEvents", "position"];
+                  const replacer = (match: any, expr: string, offset: any, string: any) => { // bcz: this executes a script to convert a property expression string:  { script }  into a value
+                        return ScriptField.MakeFunction(expr, { self: Doc.name, this: Doc.name, scale: "number" })?.script.run({ self: this.rootDoc, this: this.layoutDoc, scale }).result?.toString() ?? "";
+                  };
+                  divKeys.map((prop: string) => {
+                        const p = (this.props as any)[prop];
+                        typeof p === "string" && (style[prop] = p?.replace(/{([^.'][^}']+)}/g, replacer));
+                  });
+                  return style;
             }
 
-            return false;
-        }
-        // this is called with the document that was dragged and the collection to move it into.
-        // if the target collection is the same as this collection, then the move will be allowed.
-        // otherwise, the document being moved must be able to be removed from its container before
-        // moving it into the target.
-        @action.bound
-        moveDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (doc: Doc | Doc[], annotationKey?: string) => boolean, annotationKey?: string): boolean => {
-            if (Doc.AreProtosEqual(this.props.Document, targetCollection)) {
-                return true;
-            }
-            const first = doc instanceof Doc ? doc : doc[0];
-            if (!first?._stayInCollection && addDocument !== returnFalse) {
-                return UndoManager.RunInTempBatch(() => this.removeDocument(doc, annotationKey, true) && addDocument(doc, annotationKey));
+            protected _multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer;
+
+            @computed public get annotationKey() { return this.fieldKey + (this._annotationKeySuffix() ? "-" + this._annotationKeySuffix() : ""); }
+
+            @action.bound
+            removeDocument(doc: Doc | Doc[], annotationKey?: string, leavePushpin?: boolean): boolean {
+                  const effectiveAcl = GetEffectiveAcl(this.dataDoc);
+                  const indocs = doc instanceof Doc ? [doc] : doc;
+                  const docs = indocs.filter(doc => [AclEdit, AclAdmin].includes(effectiveAcl) || GetEffectiveAcl(doc) === AclAdmin);
+                  if (docs.length) {
+                        setTimeout(() => docs.map(doc => { // this allows 'addDocument' to see the annotationOn field in order to create a pushin
+                              Doc.SetInPlace(doc, "isPushpin", undefined, true);
+                              doc.annotationOn === this.props.Document && Doc.SetInPlace(doc, "annotationOn", undefined, true);
+                        }));
+                        const targetDataDoc = this.dataDoc;
+                        const value = DocListCast(targetDataDoc[annotationKey ?? this.annotationKey]);
+                        const toRemove = value.filter(v => docs.includes(v));
+
+                        if (toRemove.length !== 0) {
+                              const recent = Cast(Doc.UserDoc().myRecentlyClosedDocs, Doc) as Doc;
+                              toRemove.forEach(doc => {
+                                    leavePushpin && DocUtils.LeavePushpin(doc, annotationKey ?? this.annotationKey);
+                                    Doc.RemoveDocFromList(targetDataDoc, annotationKey ?? this.annotationKey, doc);
+                                    doc.context = undefined;
+                                    if (recent) {
+                                          Doc.RemoveDocFromList(recent, "data", doc);
+                                          Doc.AddDocToList(recent, "data", doc, undefined, true, true);
+                                    }
+                              });
+                              this.isAnyChildContentActive() && this.props.select(false);
+                              return true;
+                        }
+                  }
+
+                  return false;
             }
-            return false;
-        }
-        @action.bound
-        addDocument = (doc: Doc | Doc[], annotationKey?: string): boolean => {
-            const docs = doc instanceof Doc ? [doc] : doc;
-            if (this.props.filterAddDocument?.(docs) === false ||
-                docs.find(doc => Doc.AreProtosEqual(doc, this.props.Document) && Doc.LayoutField(doc) === Doc.LayoutField(this.props.Document))) {
-                return false;
+            // this is called with the document that was dragged and the collection to move it into.
+            // if the target collection is the same as this collection, then the move will be allowed.
+            // otherwise, the document being moved must be able to be removed from its container before
+            // moving it into the target.
+            @action.bound
+            moveDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (doc: Doc | Doc[], annotationKey?: string) => boolean, annotationKey?: string): boolean => {
+                  if (Doc.AreProtosEqual(this.props.Document, targetCollection)) {
+                        return true;
+                  }
+                  const first = doc instanceof Doc ? doc : doc[0];
+                  if (!first?._stayInCollection && addDocument !== returnFalse) {
+                        return UndoManager.RunInTempBatch(() => this.removeDocument(doc, annotationKey, true) && addDocument(doc, annotationKey));
+                  }
+                  return false;
             }
-            const targetDataDoc = this.props.Document[DataSym];
-            const docList = DocListCast(targetDataDoc[annotationKey ?? this.annotationKey]);
-            const added = docs.filter(d => !docList.includes(d));
-            const effectiveAcl = GetEffectiveAcl(this.dataDoc);
-
-            if (added.length) {
-                if (effectiveAcl === AclPrivate || effectiveAcl === AclReadonly) {
-                    return false;
-                }
-                else {
-                    if (this.props.Document[AclSym] && Object.keys(this.props.Document[AclSym]).length) {
-                        added.forEach(d => {
-                            for (const [key, value] of Object.entries(this.props.Document[AclSym])) {
-                                if (d.author === denormalizeEmail(key.substring(4)) && !d.aliasOf) distributeAcls(key, SharingPermissions.Admin, d);
-                            }
-                        });
-                    }
-
-                    if (effectiveAcl === AclAugment) {
-                        added.map(doc => {
-                            if ([AclAdmin, AclEdit].includes(GetEffectiveAcl(doc))) inheritParentAcls(CurrentUserUtils.ActiveDashboard, doc);
-                            doc.context = this.props.Document;
-                            if (annotationKey ?? this._annotationKeySuffix()) Doc.GetProto(doc).annotationOn = this.props.Document;
-                            this.props.layerProvider?.(doc, true);
-                            Doc.AddDocToList(targetDataDoc, annotationKey ?? this.annotationKey, doc);
-                        });
-                    }
-                    else {
-                        added.filter(doc => [AclAdmin, AclEdit].includes(GetEffectiveAcl(doc))).map(doc => {  // only make a pushpin if we have acl's to edit the document
-                            this.props.layerProvider?.(doc, true);
-                            //DocUtils.LeavePushpin(doc);
-                            doc._stayInCollection = undefined;
-                            doc.context = this.props.Document;
-                            if (annotationKey ?? this._annotationKeySuffix()) Doc.GetProto(doc).annotationOn = this.props.Document;
-
-                            inheritParentAcls(CurrentUserUtils.ActiveDashboard, doc);
-                        });
-                        const annoDocs = targetDataDoc[annotationKey ?? this.annotationKey] as List<Doc>;
-                        if (annoDocs instanceof List) annoDocs.push(...added);
-                        else targetDataDoc[annotationKey ?? this.annotationKey] = new List<Doc>(added);
-                        targetDataDoc[(annotationKey ?? this.annotationKey) + "-lastModified"] = new DateField(new Date(Date.now()));
-                    }
-                }
+            @action.bound
+            addDocument = (doc: Doc | Doc[], annotationKey?: string): boolean => {
+                  const docs = doc instanceof Doc ? [doc] : doc;
+                  if (this.props.filterAddDocument?.(docs) === false ||
+                        docs.find(doc => Doc.AreProtosEqual(doc, this.props.Document) && Doc.LayoutField(doc) === Doc.LayoutField(this.props.Document))) {
+                        return false;
+                  }
+                  const targetDataDoc = this.props.Document[DataSym];
+                  const docList = DocListCast(targetDataDoc[annotationKey ?? this.annotationKey]);
+                  const added = docs.filter(d => !docList.includes(d));
+                  const effectiveAcl = GetEffectiveAcl(this.dataDoc);
+
+                  if (added.length) {
+                        if (effectiveAcl === AclPrivate || effectiveAcl === AclReadonly) {
+                              return false;
+                        }
+                        else {
+                              if (this.props.Document[AclSym] && Object.keys(this.props.Document[AclSym]).length) {
+                                    added.forEach(d => {
+                                          for (const [key, value] of Object.entries(this.props.Document[AclSym])) {
+                                                if (d.author === denormalizeEmail(key.substring(4)) && !d.aliasOf) distributeAcls(key, SharingPermissions.Admin, d);
+                                          }
+                                    });
+                              }
+
+                              if (effectiveAcl === AclAugment) {
+                                    added.map(doc => {
+                                          if ([AclAdmin, AclEdit].includes(GetEffectiveAcl(doc))) inheritParentAcls(CurrentUserUtils.ActiveDashboard, doc);
+                                          doc.context = this.props.Document;
+                                          if (annotationKey ?? this._annotationKeySuffix()) Doc.GetProto(doc).annotationOn = this.props.Document;
+                                          this.props.layerProvider?.(doc, true);
+                                          Doc.AddDocToList(targetDataDoc, annotationKey ?? this.annotationKey, doc);
+                                    });
+                              }
+                              else {
+                                    added.filter(doc => [AclAdmin, AclEdit].includes(GetEffectiveAcl(doc))).map(doc => {  // only make a pushpin if we have acl's to edit the document
+                                          this.props.layerProvider?.(doc, true);
+                                          //DocUtils.LeavePushpin(doc);
+                                          doc._stayInCollection = undefined;
+                                          doc.context = this.props.Document;
+                                          if (annotationKey ?? this._annotationKeySuffix()) Doc.GetProto(doc).annotationOn = this.props.Document;
+
+                                          inheritParentAcls(CurrentUserUtils.ActiveDashboard, doc);
+                                    });
+                                    const annoDocs = targetDataDoc[annotationKey ?? this.annotationKey] as List<Doc>;
+                                    if (annoDocs instanceof List) annoDocs.push(...added);
+                                    else targetDataDoc[annotationKey ?? this.annotationKey] = new List<Doc>(added);
+                                    targetDataDoc[(annotationKey ?? this.annotationKey) + "-lastModified"] = new DateField(new Date(Date.now()));
+                              }
+                        }
+                  }
+                  return true;
             }
-            return true;
-        }
 
-        whenChildContentsActiveChanged = action((isActive: boolean) => this.props.whenChildContentsActiveChanged(this._isAnyChildContentActive = isActive));
-    }
-    return Component;
+            whenChildContentsActiveChanged = action((isActive: boolean) => this.props.whenChildContentsActiveChanged(this._isAnyChildContentActive = isActive));
+      }
+      return Component;
 }
\ No newline at end of file
diff --git a/src/client/views/nodes/RecordingBox/RecordingBox.tsx b/src/client/views/nodes/RecordingBox/RecordingBox.tsx
index 86358e838..a49cbbea5 100644
--- a/src/client/views/nodes/RecordingBox/RecordingBox.tsx
+++ b/src/client/views/nodes/RecordingBox/RecordingBox.tsx
@@ -38,7 +38,8 @@ export class RecordingBox extends ViewBoxBaseComponent() {
             console.log(this.videoDuration)
             this.dataDoc[this.fieldKey + "-duration"] = this.videoDuration;
 
-            this.layoutDoc.layout = VideoBox.LayoutString(this.fieldKey);
+            // this.layoutDoc.layout = VideoBox.LayoutString(this.fieldKey);
+            this.dataDoc.layout = VideoBox.LayoutString(this.fieldKey);
             // this.dataDoc.nativeWidth = this.dataDoc.nativeHeight = undefined;
             // this.layoutDoc._fitWidth = undefined;
             this.dataDoc[this.props.fieldKey] = new VideoField(this.result.accessPaths.agnostic.client);
diff --git a/src/client/views/nodes/RecordingBox/RecordingView.scss b/src/client/views/nodes/RecordingBox/RecordingView.scss
index 1fea231b7..0c153c9c8 100644
--- a/src/client/views/nodes/RecordingBox/RecordingView.scss
+++ b/src/client/views/nodes/RecordingBox/RecordingView.scss
@@ -1,7 +1,7 @@
 video {
       flex: 100%;
       width: 100%;
-      min-height: 400px;
+      // min-height: 400px;
       height: auto;
       display: block;
       background-color: black;
@@ -42,7 +42,7 @@ button {
       padding: 14px;
       width: 100%;
       max-width: 500px;
-      max-height: 20%;
+      // max-height: 20%;
       flex-wrap: wrap;
       background: rgba(255, 255, 255, 0.25);
       box-shadow: 0 8px 32px 0 rgba(255, 255, 255, 0.1);
diff --git a/src/client/views/nodes/RecordingBox/RecordingView.tsx b/src/client/views/nodes/RecordingBox/RecordingView.tsx
index a2a08f5b8..e5f6deca2 100644
--- a/src/client/views/nodes/RecordingBox/RecordingView.tsx
+++ b/src/client/views/nodes/RecordingBox/RecordingView.tsx
@@ -48,13 +48,8 @@ export function RecordingView(props: IRecordingViewProps) {
 
 
       const DEFAULT_MEDIA_CONSTRAINTS = {
-            // video: true,
-            // audio: true
-            video: {
-                  width: 1280,
-                  height: 720,
-            },
-            // audio: true,
+            video: true,
+            audio: true,
             // audio: {
             //       echoCancellation: true,
             //       noiseSuppression: true,
@@ -84,8 +79,6 @@ export function RecordingView(props: IRecordingViewProps) {
                               }
                         })
 
-                  //     this.dataDoc[this.fieldKey + "-duration"] = (new Date().getTime() - this.recordingStart!) / 1000;
-
             }
 
 
@@ -275,9 +268,9 @@ export function RecordingView(props: IRecordingViewProps) {
                                     {!recording && videos.length > 0 ?
 
                                           <div className="video-edit-wrapper">
-                                                <IconContext.Provider value={{ color: "grey", className: "video-edit-buttons" }}>
+                                                {/* <IconContext.Provider value={{ color: "grey", className: "video-edit-buttons" }}>
                                                       <MdBackspace onClick={clearPrevious} />
-                                                </IconContext.Provider>
+                                                </IconContext.Provider> */}
                                                 <IconContext.Provider value={{ color: "#cc1c08", className: "video-edit-buttons" }}>
                                                       <FaCheckCircle onClick={stop} />
                                                 </IconContext.Provider>
diff --git a/src/client/views/nodes/trails/PresElementBox.tsx b/src/client/views/nodes/trails/PresElementBox.tsx
index 23c80253c..c13f469cc 100644
--- a/src/client/views/nodes/trails/PresElementBox.tsx
+++ b/src/client/views/nodes/trails/PresElementBox.tsx
@@ -308,6 +308,8 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
             if (activeItem.recording) {
                   // if we already have an existing recording
                   console.log("recording exists")
+                  console.log(activeItem.recording)
+                  console.log(Cast(activeItem.recording, Doc, null))
                   Doc.AddDocToList((Doc.UserDoc().myOverlayDocs as Doc), undefined, Cast(activeItem.recording, Doc, null));
 
             } else {
-- 
cgit v1.2.3-70-g09d2


From 1eb2c362b020b3cbe446bbc1585108129fda6977 Mon Sep 17 00:00:00 2001
From: Michael Foiani <sotech117@michaels-mbp-3.devices.brown.edu>
Date: Wed, 4 May 2022 01:11:54 -0400
Subject: Get storing pres data working, but is choppy due to mobx usage.

---
 src/client/apis/recording/recordingApi.ts          | 300 --------------------
 src/client/util/RecordingApi.ts                    | 303 +++++++++++++++++++++
 src/client/views/Main.tsx                          |   2 +
 .../collectionFreeForm/CollectionFreeFormView.tsx  |   5 +-
 .../views/nodes/RecordingBox/RecordingBox.tsx      |  14 +-
 .../views/nodes/RecordingBox/RecordingView.tsx     |  26 +-
 src/client/views/nodes/VideoBox.tsx                |  14 +
 7 files changed, 348 insertions(+), 316 deletions(-)
 delete mode 100644 src/client/apis/recording/recordingApi.ts
 create mode 100644 src/client/util/RecordingApi.ts

(limited to 'src/client/views/nodes/RecordingBox/RecordingBox.tsx')

diff --git a/src/client/apis/recording/recordingApi.ts b/src/client/apis/recording/recordingApi.ts
deleted file mode 100644
index b57e675b7..000000000
--- a/src/client/apis/recording/recordingApi.ts
+++ /dev/null
@@ -1,300 +0,0 @@
-import { CollectionFreeFormView } from "../../views/collections/collectionFreeForm";
-import React, { useState } from "react";
-import { IReactionDisposer, observable, reaction } from "mobx";
-import { NumCast } from "../../../fields/Types";
-
-type Movement = {
-    time: number,
-    panX: number,
-    panY: number,
-}
-
-type Presentation = {
-    movements: Array<Movement>
-    meta: Object,
-    startDate: Date | null,
-}
-
-export class RecordingApi {
-
-    private static NULL_PRESENTATION: Presentation = {
-        movements: [],
-        meta: {},
-        startDate: null,
-    }
-
-    // instance variables
-    private currentPresentation: Presentation;
-    private isRecording: boolean;
-    private absoluteStart: number;
-
-
-    // create static instance and getter for global use
-    @observable static _instance: RecordingApi;
-    public static get instance(): RecordingApi { return RecordingApi._instance }
-    public constructor() {
-        // init the global instance
-        RecordingApi._instance = this;
-
-        // init the instance variables
-        this.currentPresentation = RecordingApi.NULL_PRESENTATION
-        this.isRecording = false;
-        this.absoluteStart = -1;
-
-        // used for tracking movements in the view frame
-        this.disposeFunc = null;
-
-        // for now, set playFFView
-        this.playFFView = null;
-        this.timers = null;
-    }
-
-    // little helper :)
-    private get isInitPresenation(): boolean {
-        return this.currentPresentation.startDate === null
-    }
-
-    public start = (view: CollectionFreeFormView, meta?: Object): Error | undefined => {
-        // check if already init a presentation
-        if (!this.isInitPresenation) {
-            console.error('[recordingApi.ts] start() failed: current presentation data exists. please call clear() first.')
-            return new Error('[recordingApi.ts] start()')
-        }
-
-        // (1a) get start date for presenation
-        const startDate = new Date()
-        // (1b) set start timestamp to absolute timestamp
-        this.absoluteStart = startDate.getTime()
-
-        // (2) assign meta content if it exists
-        this.currentPresentation.meta = meta || {}
-        // (3) assign start date to currentPresenation
-        this.currentPresentation.startDate = startDate
-        // (4) set isRecording true to allow trackMovements
-        this.isRecording = true
-    }
-
-    public clear = (): Error | undefined => {
-        // TODO: maybe archive the data?
-        if (this.isRecording) {
-            console.error('[recordingApi.ts] clear() failed: currently recording presentation. call pause() or finish() first')
-            return new Error('[recordingApi.ts] clear()')
-        }
-
-        // clear presenation data
-        this.currentPresentation = RecordingApi.NULL_PRESENTATION
-        // clear isRecording
-        this.isRecording = false
-        // clear absoluteStart
-        this.absoluteStart = -1
-
-        // clear the disposeFunc
-        this.disposeFunc = null
-    }
-
-    public pause = (): Error | undefined => {
-        if (this.currentPresentation.startDate === null) {
-            console.error('[recordingApi.ts] pause() failed: no presentation started. try calling init() first')
-            return new Error('[recordingApi.ts] pause(): no presenation')
-        }
-        // don't allow track movments
-        this.isRecording = false
-
-        // set relativeStart to the pausedTimestamp
-        const timestamp = new Date().getTime()
-        this.absoluteStart = timestamp
-    }
-
-    public resume = () => {
-        const timestamp = new Date().getTime()
-        const startTimestamp = this.currentPresentation.startDate?.getTime()
-        if (!startTimestamp) {
-            console.error('[recordingApi.ts] resume() failed: no presentation data. try calling start() first')
-            return new Error('[recordingApi.ts] pause()')
-        }
-
-        // update absoluteStart to bridge the paused time
-        const absoluteTimePaused = timestamp - this.absoluteStart
-        this.absoluteStart = absoluteTimePaused
-    }
-
-    public finish = (): Error | Presentation => {
-        if (this.isInitPresenation) {
-            console.error('[recordingApi.ts] finish() failed: no presentation data. try calling start() first')
-            return new Error('[recordingApi.ts] finish()')
-        }
-
-        // return a copy of the the presentation data
-        return { ...this.currentPresentation }
-    }
-
-    private trackMovements = (panX: number, panY: number): Error | undefined => {
-        // ensure we are recording
-        if (!this.isRecording) {
-            console.error('[recordingApi.ts] pause() failed: recording is paused()')
-            return new Error('[recordingApi.ts] pause()')
-        }
-
-        // get the relative time
-        const timestamp = new Date().getTime()
-        const relativeTime = timestamp - this.absoluteStart
-
-        // make new movement struct
-        const movement: Movement = { time: relativeTime, panX, panY }
-        
-        // add that movement to the current presentation data's movement array
-        this.currentPresentation.movements.push(movement)
-    }
-
-    // instance variable for the FFView
-    private disposeFunc: IReactionDisposer | null;
-    
-    // set the FFView that will be used in a reaction to track the movements
-    public setRecordingFFView = (view: CollectionFreeFormView): void => {
-        // set the view to the current view
-        if (view === null) return;
-        
-        //this.recordingFFView = view;
-        // set the reaction to track the movements
-        this.disposeFunc = reaction(
-            () => ({ x: NumCast(view.Document.panX, -1), y: NumCast(view.Document.panY, -1) }),
-            (res) => (res.x !== -1 && res.y !== -1) && this.trackMovements(res.x, res.y)
-        )
-
-        // for now, set the most recent recordingFFView to the playFFView
-        this.playFFView = view;
-    }
-
-    // call on dispose function to stop tracking movements
-    public removeRecordingFFView = (): void => {
-        this.disposeFunc?.();
-        this.disposeFunc = null;
-    }
-
-    // TODO: extract this into different class with pause and resume recording
-    private playFFView: CollectionFreeFormView | null;
-    private timers: Timer[] | null;
-
-    public followMovements = (presentation: Presentation): undefined | Error => {
-        if (presentation.startDate === null || this.playFFView === null) {
-            return new Error('[recordingApi.ts] followMovements() failed: no presentation data or no view')
-        }
-
-        const document = this.playFFView.Document
-        const { movements } = presentation
-        this.timers = movements.map(movement => {
-            const { panX, panY, time } = movement
-            return new Timer(() => {
-                document._panX = panX;
-                document._panY = panY;
-                // TODO: consider cleaning this array to null or some state
-            }, time)
-        })
-    }
-
-    public pauseMovements = (): undefined | Error => {
-        if (this.playFFView === null) {
-            return new Error('[recordingApi.ts] pauseMovements() failed: no view')
-        }
-        // TODO: set userdoc presentMode to browsing
-        this.timers?.forEach(timer => timer.pause())
-    }
-
-    public resumeMovements = (): undefined | Error => {
-        if (this.playFFView === null) {
-            return new Error('[recordingApi.ts] resumeMovements() failed: no view')
-        }
-        this.timers?.forEach(timer => timer.resume())
-    }
-
-    // public followMovements = (presentation: Presentation): undefined | Error => {
-    //     if (presentation.startDate === null || this.playFFView === null) {
-    //         return new Error('[recordingApi.ts] followMovements() failed: no presentation data or no view')
-    //     }
-
-    //     const document = this.playFFView.Document
-    //     const { movements } = presentation
-    //     movements.forEach(movement => {
-    //         const { panX, panY, time } = movement
-    //         // set the pan to what was stored
-    //         setTimeout(() => {
-    //             document._panX = panX;
-    //             document._panY = panY;
-    //         }, time)
-    //     })
-    // }
-    // Unfinished code for tracing multiple free form views
-    // export let pres: Map<CollectionFreeFormView, IReactionDisposer> = new Map()
-
-    // export function AddRecordingFFView(ffView: CollectionFreeFormView): void {
-    //     pres.set(ffView,
-    //          reaction(() => ({ x: ffView.panX, y: ffView.panY }),
-    //     (pt) => RecordingApi.trackMovements(ffView, pt.x, pt.y)))
-    //     )
-    // }
-
-    // export function RemoveRecordingFFView(ffView: CollectionFreeFormView): void {
-    //     const disposer = pres.get(ffView);
-    //     disposer?.();
-    //     pres.delete(ffView)
-    // }
-}
-
-/** Represents the `setTimeout` with an ability to perform pause/resume actions
- * citation: https://stackoverflow.com/questions/3969475/javascript-pause-settimeout
- */
-export class Timer {
-    private _start: Date;
-    private _remaining: number;
-    private _durationTimeoutId?: NodeJS.Timeout;
-    private _callback: (...args: any[]) => void;
-    private _done = false;
-    get done () {
-        return this._done;
-    }
-
-    public constructor(callback: (...args: any[]) => void, ms = 0) {
-        this._callback = () => {
-            callback();
-            this._done = true;
-        };
-        this._remaining = ms;
-        this.resume();
-    }
-
-    /** pauses the timer */
-    public pause(): Timer {
-        if (this._durationTimeoutId && !this._done) {
-            this._clearTimeoutRef();
-            this._remaining -= new Date().getTime() - this._start.getTime();
-        }
-        return this;
-    }
-
-    /** resumes the timer */
-    public resume(): Timer {
-        if (!this._durationTimeoutId && !this._done) {
-            this._start = new Date;
-            this._durationTimeoutId = setTimeout(this._callback, this._remaining);
-        }
-        return this;
-    }
-
-    /** 
-     * clears the timeout and marks it as done. 
-     * 
-     * After called, the timeout will not resume
-     */
-    public clearTimeout() {
-        this._clearTimeoutRef();
-        this._done = true;
-    }
-
-    private _clearTimeoutRef() {
-        if (this._durationTimeoutId) {
-            clearTimeout(this._durationTimeoutId);
-            this._durationTimeoutId = undefined;
-        }
-    }
-
-}
\ No newline at end of file
diff --git a/src/client/util/RecordingApi.ts b/src/client/util/RecordingApi.ts
new file mode 100644
index 000000000..c4f76282c
--- /dev/null
+++ b/src/client/util/RecordingApi.ts
@@ -0,0 +1,303 @@
+import { CollectionFreeFormView } from "../views/collections/collectionFreeForm";
+import { IReactionDisposer, observable, reaction } from "mobx";
+import { NumCast } from "../../fields/Types";
+
+type Movement = {
+    time: number,
+    panX: number,
+    panY: number,
+}
+
+type Presentation = {
+    movements: Array<Movement>
+    meta: Object,
+    startDate: Date | null,
+}
+
+export class RecordingApi {
+
+    private static NULL_PRESENTATION: Presentation = {
+        movements: [],
+        meta: {},
+        startDate: null,
+    }
+
+    // instance variables
+    private currentPresentation: Presentation;
+    private isRecording: boolean;
+    private absoluteStart: number;
+
+
+    // create static instance and getter for global use
+    @observable static _instance: RecordingApi;
+    public static get Instance(): RecordingApi { return RecordingApi._instance }
+    public constructor() {
+        // init the global instance
+        RecordingApi._instance = this;
+
+        // init the instance variables
+        this.currentPresentation = RecordingApi.NULL_PRESENTATION
+        this.isRecording = false;
+        this.absoluteStart = -1;
+
+        // used for tracking movements in the view frame
+        this.disposeFunc = null;
+
+        // for now, set playFFView
+        this.playFFView = null;
+        this.timers = null;
+    }
+
+    // little helper :)
+    private get isInitPresenation(): boolean {
+        return this.currentPresentation.startDate === null
+    }
+
+    public start = (meta?: Object): Error | undefined => {
+        // check if already init a presentation
+        if (!this.isInitPresenation) {
+            console.error('[recordingApi.ts] start() failed: current presentation data exists. please call clear() first.')
+            return new Error('[recordingApi.ts] start()')
+        }
+
+        // (1a) get start date for presenation
+        const startDate = new Date()
+        // (1b) set start timestamp to absolute timestamp
+        this.absoluteStart = startDate.getTime()
+
+        // (2) assign meta content if it exists
+        this.currentPresentation.meta = meta || {}
+        // (3) assign start date to currentPresenation
+        this.currentPresentation.startDate = startDate
+        // (4) set isRecording true to allow trackMovements
+        this.isRecording = true
+    }
+
+    public clear = (): Error | Presentation => {
+        // TODO: maybe archive the data?
+        if (this.isRecording) {
+            console.error('[recordingApi.ts] clear() failed: currently recording presentation. call pause() first')
+            return new Error('[recordingApi.ts] clear()')
+        }
+
+        const presCopy = { ...this.currentPresentation }
+
+        // clear presenation data
+        this.currentPresentation = RecordingApi.NULL_PRESENTATION
+        // clear isRecording
+        this.isRecording = false
+        // clear absoluteStart
+        this.absoluteStart = -1
+        // clear the disposeFunc
+        this.disposeFunc = null
+
+        return presCopy;
+    }
+
+    public pause = (): Error | undefined => {
+        if (this.currentPresentation.startDate === null) {
+            console.error('[recordingApi.ts] pause() failed: no presentation started. try calling init() first')
+            return new Error('[recordingApi.ts] pause(): no presenation')
+        }
+        // don't allow track movments
+        this.isRecording = false
+
+        // set relativeStart to the pausedTimestamp
+        const timestamp = new Date().getTime()
+        this.absoluteStart = timestamp
+    }
+
+    public resume = () => {
+        const timestamp = new Date().getTime()
+        const startTimestamp = this.currentPresentation.startDate?.getTime()
+        if (!startTimestamp) {
+            console.error('[recordingApi.ts] resume() failed: no presentation data. try calling start() first')
+            return new Error('[recordingApi.ts] pause()')
+        }
+
+        // update absoluteStart to bridge the paused time
+        const absoluteTimePaused = timestamp - this.absoluteStart
+        this.absoluteStart = absoluteTimePaused
+    }
+
+    // public finish = (): Error | Presentation => {
+    //     if (this.isInitPresenation) {
+    //         console.error('[recordingApi.ts] finish() failed: no presentation data. try calling start() first')
+    //         return new Error('[recordingApi.ts] finish()')
+    //     }
+
+    //     // return a copy of the the presentation data
+    //     return { ...this.currentPresentation }
+    // }
+
+    private trackMovements = (panX: number, panY: number): Error | undefined => {
+        // ensure we are recording
+        if (!this.isRecording) {
+            console.error('[recordingApi.ts] pause() failed: recording is paused()')
+            return new Error('[recordingApi.ts] pause()')
+        }
+
+        // get the relative time
+        const timestamp = new Date().getTime()
+        const relativeTime = timestamp - this.absoluteStart
+
+        // make new movement struct
+        const movement: Movement = { time: relativeTime, panX, panY }
+        
+        // add that movement to the current presentation data's movement array
+        this.currentPresentation.movements.push(movement)
+    }
+
+    // instance variable for the FFView
+    private disposeFunc: IReactionDisposer | null;
+    
+    // set the FFView that will be used in a reaction to track the movements
+    public setRecordingFFView = (view: CollectionFreeFormView): void => {
+        // set the view to the current view
+        if (view === null || view === this.playFFView) return;
+        
+        //this.recordingFFView = view;
+        // set the reaction to track the movements
+        this.disposeFunc = reaction(
+            () => ({ x: NumCast(view.Document.panX, -1), y: NumCast(view.Document.panY, -1) }),
+            (res) => (res.x !== -1 && res.y !== -1) && this.trackMovements(res.x, res.y)
+        )
+
+        // for now, set the most recent recordingFFView to the playFFView
+        this.playFFView = view;
+    }
+
+    // call on dispose function to stop tracking movements
+    public removeRecordingFFView = (): void => {
+        this.disposeFunc?.();
+        this.disposeFunc = null;
+    }
+
+    // TODO: extract this into different class with pause and resume recording
+    private playFFView: CollectionFreeFormView | null;
+    private timers: Timer[] | null;
+
+    // public followMovements = (presentation: Presentation): undefined | Error => {
+    //     console.log(presentation)
+    //     if (presentation.startDate === null || this.playFFView === null) {
+    //         return new Error('[recordingApi.ts] followMovements() failed: no presentation data or no view')
+    //     }
+
+    //     const document = this.playFFView.Document
+    //     const { movements } = presentation
+    //     this.timers = movements.map(movement => {
+    //         const { panX, panY, time } = movement
+    //         return new Timer(() => {
+    //             document._panX = panX;
+    //             document._panY = panY;
+    //             // TODO: consider cleaning this array to null or some state
+    //         }, time)
+    //     })
+    // }
+
+    public pauseMovements = (): undefined | Error => {
+        if (this.playFFView === null) {
+            return new Error('[recordingApi.ts] pauseMovements() failed: no view')
+        }
+        // TODO: set userdoc presentMode to browsing
+        this.timers?.forEach(timer => timer.pause())
+    }
+
+    public resumeMovements = (): undefined | Error => {
+        if (this.playFFView === null) {
+            return new Error('[recordingApi.ts] resumeMovements() failed: no view')
+        }
+        this.timers?.forEach(timer => timer.resume())
+    }
+
+    public followMovements = (presentation: Presentation): undefined | Error => {
+        if (presentation.startDate === null || this.playFFView === null) {
+            return new Error('[recordingApi.ts] followMovements() failed: no presentation data or no view')
+        }
+
+        const document = this.playFFView.Document
+        const { movements } = presentation
+        movements.forEach(movement => {
+            const { panX, panY, time } = movement
+            // set the pan to what was stored
+            setTimeout(() => {
+                document._panX = panX;
+                document._panY = panY;
+            }, time)
+        })
+    }
+    // Unfinished code for tracing multiple free form views
+    // export let pres: Map<CollectionFreeFormView, IReactionDisposer> = new Map()
+
+    // export function AddRecordingFFView(ffView: CollectionFreeFormView): void {
+    //     pres.set(ffView,
+    //          reaction(() => ({ x: ffView.panX, y: ffView.panY }),
+    //     (pt) => RecordingApi.trackMovements(ffView, pt.x, pt.y)))
+    //     )
+    // }
+
+    // export function RemoveRecordingFFView(ffView: CollectionFreeFormView): void {
+    //     const disposer = pres.get(ffView);
+    //     disposer?.();
+    //     pres.delete(ffView)
+    // }
+}
+
+/** Represents the `setTimeout` with an ability to perform pause/resume actions
+ * citation: https://stackoverflow.com/questions/3969475/javascript-pause-settimeout
+ */
+export class Timer {
+    private _start: Date;
+    private _remaining: number;
+    private _durationTimeoutId?: NodeJS.Timeout;
+    private _callback: (...args: any[]) => void;
+    private _done = false;
+    get done () {
+        return this._done;
+    }
+
+    public constructor(callback: (...args: any[]) => void, ms = 0) {
+        this._callback = () => {
+            callback();
+            this._done = true;
+        };
+        this._remaining = ms;
+        this.resume();
+    }
+
+    /** pauses the timer */
+    public pause(): Timer {
+        if (this._durationTimeoutId && !this._done) {
+            this._clearTimeoutRef();
+            this._remaining -= new Date().getTime() - this._start.getTime();
+        }
+        return this;
+    }
+
+    /** resumes the timer */
+    public resume(): Timer {
+        if (!this._durationTimeoutId && !this._done) {
+            this._start = new Date;
+            this._durationTimeoutId = setTimeout(this._callback, this._remaining);
+        }
+        return this;
+    }
+
+    /** 
+     * clears the timeout and marks it as done. 
+     * 
+     * After called, the timeout will not resume
+     */
+    public clearTimeout() {
+        this._clearTimeoutRef();
+        this._done = true;
+    }
+
+    private _clearTimeoutRef() {
+        if (this._durationTimeoutId) {
+            clearTimeout(this._durationTimeoutId);
+            this._durationTimeoutId = undefined;
+        }
+    }
+
+}
\ No newline at end of file
diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx
index 8560ccb29..517fe097c 100644
--- a/src/client/views/Main.tsx
+++ b/src/client/views/Main.tsx
@@ -8,6 +8,7 @@ import { AssignAllExtensions } from "../../extensions/General/Extensions";
 import { Docs } from "../documents/Documents";
 import { CurrentUserUtils } from "../util/CurrentUserUtils";
 import { LinkManager } from "../util/LinkManager";
+import { RecordingApi } from "../util/RecordingApi";
 import { CollectionView } from "./collections/CollectionView";
 import { MainView } from "./MainView";
 
@@ -36,5 +37,6 @@ AssignAllExtensions();
     const expires = "expires=" + d.toUTCString();
     document.cookie = `loadtime=${loading};${expires};path=/`;
     new LinkManager();
+    new RecordingApi;
     ReactDOM.render(<MainView />, document.getElementById('root'));
 })();
\ No newline at end of file
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 35bd9cf79..f74e526b6 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -54,7 +54,7 @@ import "./CollectionFreeFormView.scss";
 import { MarqueeView } from "./MarqueeView";
 import React = require("react");
 import e = require("connect-flash");
-import { RecordingApi } from "../../../apis/recording/RecordingApi";
+import { RecordingApi } from "../../../util/RecordingApi";
 
 export const panZoomSchema = createSchema({
     _panX: "number",
@@ -965,6 +965,9 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
 
     @action
     setPan(panX: number, panY: number, panTime: number = 0, clamp: boolean = false) {
+        // set the current FFview to the tab being panned.
+        RecordingApi.Instance.setRecordingFFView(this)
+
         if (!this.isAnnotationOverlay && clamp) {
             // this section wraps the pan position, horizontally and/or vertically whenever the content is panned out of the viewing bounds
             const docs = this.childLayoutPairs.map(pair => pair.layout).filter(doc => doc instanceof Doc);
diff --git a/src/client/views/nodes/RecordingBox/RecordingBox.tsx b/src/client/views/nodes/RecordingBox/RecordingBox.tsx
index 86358e838..1b17476f7 100644
--- a/src/client/views/nodes/RecordingBox/RecordingBox.tsx
+++ b/src/client/views/nodes/RecordingBox/RecordingBox.tsx
@@ -1,7 +1,7 @@
 import { action, observable } from "mobx";
 import { observer } from "mobx-react";
 import * as React from "react";
-import { AudioField, VideoField } from "../../../../fields/URLField";
+import { VideoField } from "../../../../fields/URLField";
 import { Upload } from "../../../../server/SharedMediaTypes";
 import { ViewBoxBaseComponent } from "../../DocComponent";
 import { FieldView } from "../FieldView";
@@ -9,6 +9,8 @@ import { VideoBox } from "../VideoBox";
 import { RecordingView } from './RecordingView';
 import { DocumentType } from "../../../documents/DocumentTypes";
 
+import { RecordingApi } from "../../../util/RecordingApi";
+
 
 @observer
 export class RecordingBox extends ViewBoxBaseComponent() {
@@ -31,17 +33,19 @@ export class RecordingBox extends ViewBoxBaseComponent() {
 
       @action
       setResult = (info: Upload.FileInformation) => {
-            console.log("Setting result to " + info)
+            // console.log("Setting result to " + info)
             this.result = info
-            console.log(this.result.accessPaths.agnostic.client)
+            // console.log(this.result.accessPaths.agnostic.client)
             this.dataDoc.type = DocumentType.VID;
-            console.log(this.videoDuration)
+            // console.log(this.videoDuration)
             this.dataDoc[this.fieldKey + "-duration"] = this.videoDuration;
 
             this.layoutDoc.layout = VideoBox.LayoutString(this.fieldKey);
             // this.dataDoc.nativeWidth = this.dataDoc.nativeHeight = undefined;
             // this.layoutDoc._fitWidth = undefined;
-            this.dataDoc[this.props.fieldKey] = new VideoField(this.result.accessPaths.agnostic.client);
+          this.dataDoc[this.props.fieldKey] = new VideoField(this.result.accessPaths.agnostic.client);
+          // stringify the presenation and store it
+            this.dataDoc[this.fieldKey + "-presentation"] = JSON.stringify(RecordingApi.Instance.clear());
       }
 
       render() {
diff --git a/src/client/views/nodes/RecordingBox/RecordingView.tsx b/src/client/views/nodes/RecordingBox/RecordingView.tsx
index d99492095..870ef87d7 100644
--- a/src/client/views/nodes/RecordingBox/RecordingView.tsx
+++ b/src/client/views/nodes/RecordingBox/RecordingView.tsx
@@ -8,7 +8,7 @@ import { IconContext } from "react-icons";
 import { Networking } from '../../../Network';
 import { Upload } from '../../../../server/SharedMediaTypes';
 
-import { RecordingApi } from '../../../apis/recording/RecordingApi';
+import { RecordingApi } from '../../../util/RecordingApi';
 
 
 enum RecordingStatus {
@@ -57,11 +57,11 @@ export function RecordingView(props: IRecordingViewProps) {
                   height: 720,
             },
             // audio: true,
-            // audio: {
-            //       echoCancellation: true,
-            //       noiseSuppression: true,
-            //       sampleRate: 44100
-            // }
+            audio: {
+                  echoCancellation: true,
+                  noiseSuppression: true,
+                  sampleRate: 44100
+            }
       }
 
       useEffect(() => {
@@ -159,7 +159,10 @@ export function RecordingView(props: IRecordingViewProps) {
             // }
 
             videoRecorder.current.onstart = (event: any) => {
-                  setRecording(true);
+                setRecording(true);
+                // TODO: update names
+                // RecordingApi.Instance.clear();
+                RecordingApi.Instance.start();
             }
 
             videoRecorder.current.onstop = () => {
@@ -172,7 +175,8 @@ export function RecordingView(props: IRecordingViewProps) {
                   // reset the temporary chunks
                   videoChunks = []
                   setRecording(false);
-                  setFinished(true);
+                setFinished(true);
+                RecordingApi.Instance.pause();
             }
 
             // recording paused
@@ -182,12 +186,14 @@ export function RecordingView(props: IRecordingViewProps) {
 
                   // reset the temporary chunks
                   videoChunks = []
-                  setRecording(false);
+                setRecording(false);
+                RecordingApi.Instance.pause();
             }
 
             videoRecorder.current.onresume = async (event: any) => {
                   await startShowingStream();
-                  setRecording(true);
+                setRecording(true);
+                RecordingApi.Instance.resume();
             }
 
             videoRecorder.current.start(200)
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index e57cb1abe..7364a64d9 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -28,6 +28,7 @@ import { AnchorMenu } from "../pdf/AnchorMenu";
 import { StyleProp } from "../StyleProvider";
 import { FieldView, FieldViewProps } from './FieldView';
 import "./VideoBox.scss";
+import { RecordingApi } from "../../util/RecordingApi";
 const path = require('path');
 
 
@@ -117,6 +118,12 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
         return field?.url.href ?? vfield?.url.href ?? "";
     }
 
+    // returns the presentation data if it exists, null otherwise
+    @computed get presentation() {
+        const data = this.dataDoc[this.fieldKey + '-presentation'];
+        return data ? JSON.parse(data) : null;
+    }
+
 
     @computed private get timeline() { return this._stackedTimeline; }
     private get transition() { return this._clicking ? "left 0.5s, width 0.5s, height 0.5s" : ""; } // css transition for hiding/showing timeline
@@ -147,6 +154,13 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
 
     // plays video
     @action public Play = (update: boolean = true) => {
+        // if presentation isn't null, call followmovements on the recording api
+        if (this.presentation) {
+            const err = RecordingApi.Instance.followMovements(this.presentation);
+            if (err) console.log(err);
+        }
+
+
         this._playing = true;
         const eleTime = this.player?.currentTime || 0;
         if (this.timeline) {
-- 
cgit v1.2.3-70-g09d2


From c1ad0be926f6add954b58be581eee778eb4b7fca Mon Sep 17 00:00:00 2001
From: Jenny Yu <jennyyu212@outlook.com>
Date: Wed, 11 May 2022 13:30:18 -0400
Subject: fix: recording box fix dimensions

---
 src/client/views/nodes/RecordingBox/RecordingBox.tsx | 11 ++++++++---
 1 file changed, 8 insertions(+), 3 deletions(-)

(limited to 'src/client/views/nodes/RecordingBox/RecordingBox.tsx')

diff --git a/src/client/views/nodes/RecordingBox/RecordingBox.tsx b/src/client/views/nodes/RecordingBox/RecordingBox.tsx
index d00f05759..a7524df37 100644
--- a/src/client/views/nodes/RecordingBox/RecordingBox.tsx
+++ b/src/client/views/nodes/RecordingBox/RecordingBox.tsx
@@ -8,8 +8,8 @@ import { FieldView } from "../FieldView";
 import { VideoBox } from "../VideoBox";
 import { RecordingView } from './RecordingView';
 import { DocumentType } from "../../../documents/DocumentTypes";
-
 import { RecordingApi } from "../../../util/RecordingApi";
+import { Doc } from "../../../../fields/Doc";
 
 
 @observer
@@ -23,6 +23,11 @@ export class RecordingBox extends ViewBoxBaseComponent() {
             super(props);
       }
 
+      componentDidMount() {
+            Doc.SetNativeWidth(this.dataDoc, 1280);
+            Doc.SetNativeHeight(this.dataDoc, 720);
+      }
+
       @observable result: Upload.FileInformation | undefined = undefined
       @observable videoDuration: number | undefined = undefined
 
@@ -44,8 +49,8 @@ export class RecordingBox extends ViewBoxBaseComponent() {
             this.dataDoc.layout = VideoBox.LayoutString(this.fieldKey);
             // this.dataDoc.nativeWidth = this.dataDoc.nativeHeight = undefined;
             // this.layoutDoc._fitWidth = undefined;
-          this.dataDoc[this.props.fieldKey] = new VideoField(this.result.accessPaths.agnostic.client);
-          // stringify the presenation and store it
+            this.dataDoc[this.props.fieldKey] = new VideoField(this.result.accessPaths.agnostic.client);
+            // stringify the presenation and store it
             this.dataDoc[this.fieldKey + "-presentation"] = JSON.stringify(RecordingApi.Instance.clear());
       }
 
-- 
cgit v1.2.3-70-g09d2


From b47ff4a335d15a259a6db436e8f2a1beb3180f6d Mon Sep 17 00:00:00 2001
From: Jenny Yu <jennyyu212@outlook.com>
Date: Thu, 12 May 2022 01:13:34 -0400
Subject: feat: option to recreate recording

---
 .../views/nodes/RecordingBox/RecordingBox.tsx      |    5 +-
 src/client/views/nodes/VideoBox.tsx                | 1686 ++++++++++----------
 2 files changed, 853 insertions(+), 838 deletions(-)

(limited to 'src/client/views/nodes/RecordingBox/RecordingBox.tsx')

diff --git a/src/client/views/nodes/RecordingBox/RecordingBox.tsx b/src/client/views/nodes/RecordingBox/RecordingBox.tsx
index a7524df37..807624394 100644
--- a/src/client/views/nodes/RecordingBox/RecordingBox.tsx
+++ b/src/client/views/nodes/RecordingBox/RecordingBox.tsx
@@ -24,6 +24,7 @@ export class RecordingBox extends ViewBoxBaseComponent() {
       }
 
       componentDidMount() {
+            console.log("set native width and height")
             Doc.SetNativeWidth(this.dataDoc, 1280);
             Doc.SetNativeHeight(this.dataDoc, 720);
       }
@@ -45,11 +46,9 @@ export class RecordingBox extends ViewBoxBaseComponent() {
             // console.log(this.videoDuration)
             this.dataDoc[this.fieldKey + "-duration"] = this.videoDuration;
 
-            // this.layoutDoc.layout = VideoBox.LayoutString(this.fieldKey);
             this.dataDoc.layout = VideoBox.LayoutString(this.fieldKey);
-            // this.dataDoc.nativeWidth = this.dataDoc.nativeHeight = undefined;
-            // this.layoutDoc._fitWidth = undefined;
             this.dataDoc[this.props.fieldKey] = new VideoField(this.result.accessPaths.agnostic.client);
+            this.dataDoc[this.fieldKey + "-recorded"] = true;
             // stringify the presenation and store it
             this.dataDoc[this.fieldKey + "-presentation"] = JSON.stringify(RecordingApi.Instance.clear());
       }
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index f449ae429..58e9f390c 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -7,7 +7,7 @@ import * as rp from 'request-promise';
 import { Doc, DocListCast, HeightSym, WidthSym } from "../../../fields/Doc";
 import { InkTool } from "../../../fields/InkField";
 import { Cast, NumCast, StrCast } from "../../../fields/Types";
-import { AudioField, ImageField, VideoField } from "../../../fields/URLField";
+import { AudioField, ImageField, RecordingField, VideoField } from "../../../fields/URLField";
 import { emptyFunction, formatTime, OmitKeys, returnFalse, returnOne, setupMoveUpEvents, Utils } from "../../../Utils";
 import { Docs, DocUtils } from "../../documents/Documents";
 import { DocumentType } from "../../documents/DocumentTypes";
@@ -29,6 +29,8 @@ import { StyleProp } from "../StyleProvider";
 import { FieldView, FieldViewProps } from './FieldView';
 import "./VideoBox.scss";
 import { RecordingApi } from "../../util/RecordingApi";
+import { List } from "../../../fields/List";
+import { RecordingBox } from "./RecordingBox";
 const path = require('path');
 
 
@@ -46,857 +48,871 @@ const path = require('path');
 
 @observer
 export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps & FieldViewProps>() {
-    public static LayoutString(fieldKey: string) { return FieldView.LayoutString(VideoBox, fieldKey); }
-    /**
-     * Uploads an image buffer to the server and stores with specified filename.  by default the image
-     * is stored at multiple resolutions each retrieved by using the filename appended with _o, _s, _m, _l (indicating original, small, medium, or large)
-     * @param imageUri the bytes of the image
-     * @param returnedFilename the base filename to store the image on the server
-     * @param nosuffix optionally suppress creating multiple resolution images
-     */
-    public static async convertDataUri(imageUri: string, returnedFilename: string, nosuffix = false, replaceRootFilename?: string) {
-        try {
-            const posting = Utils.prepend("/uploadURI");
-            const returnedUri = await rp.post(posting, {
-                body: {
-                    uri: imageUri,
-                    name: returnedFilename,
-                    nosuffix,
-                    replaceRootFilename
-                },
-                json: true,
-            });
-            return returnedUri;
-
-        } catch (e) {
-            console.log("VideoBox :" + e);
-        }
-    }
-
-    static _youtubeIframeCounter: number = 0;
-    static heightPercent = 80; // height of video relative to videoBox when timeline is open
-    private _disposers: { [name: string]: IReactionDisposer } = {};
-    private _youtubePlayer: YT.Player | undefined = undefined;
-    private _videoRef: HTMLVideoElement | null = null; // <video> ref
-    private _contentRef: HTMLDivElement | null = null; // ref to div that wraps video and controls for full screen
-    private _youtubeIframeId: number = -1;
-    private _youtubeContentCreated = false;
-    private _audioPlayer: HTMLAudioElement | null = null;
-    private _mainCont: React.RefObject<HTMLDivElement> = React.createRef(); // outermost div
-    private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef();
-    private _playRegionTimer: any = null; // timeout for playback
-    @observable _stackedTimeline: any; // CollectionStackedTimeline ref
-    @observable static _nativeControls: boolean; // default html controls
-    @observable _marqueeing: number[] | undefined; // coords for marquee selection
-    @observable _savedAnnotations = new ObservableMap<number, HTMLDivElement[]>();
-    @observable _screenCapture = false;
-    @observable _clicking = false; // used for transition between showing/hiding timeline
-    @observable _forceCreateYouTubeIFrame = false;
-    @observable _playTimer?: NodeJS.Timeout = undefined;
-    @observable _fullScreen = false;
-    @observable _playing = false;
-    @observable _finished: boolean = false; // has playback reached end of clip
-    @observable _volume: number = 1;
-    @observable _muted: boolean = false;
-
-    @computed get links() { return DocListCast(this.dataDoc.links); }
-    @computed get heightPercent() { return NumCast(this.layoutDoc._timelineHeightPercent, 100); } // current percent of video relative to VideoBox height
-    // @computed get rawDuration() { return NumCast(this.dataDoc[this.fieldKey + "-duration"]); }
-    @observable rawDuration: number = 0;
-
-
-    @computed get youtubeVideoId() {
-        const field = Cast(this.dataDoc[this.props.fieldKey], VideoField);
-        return field && field.url.href.indexOf("youtube") !== -1 ? ((arr: string[]) => arr[arr.length - 1])(field.url.href.split("/")) : "";
-    }
-
-
-    // returns the path of the audio file
-    @computed get audiopath() {
-        const field = Cast(this.props.Document[this.props.fieldKey + '-audio'], AudioField, null);
-        const vfield = Cast(this.dataDoc[this.fieldKey], VideoField, null);
-        return field?.url.href ?? vfield?.url.href ?? "";
-    }
-
-    // returns the presentation data if it exists, null otherwise
-    @computed get presentation() {
-        const data = this.dataDoc[this.fieldKey + '-presentation'];
-        return data ? JSON.parse(data) : null;
-    }
-
-
-    @computed private get timeline() { return this._stackedTimeline; }
-    private get transition() { return this._clicking ? "left 0.5s, width 0.5s, height 0.5s" : ""; } // css transition for hiding/showing timeline
-    public get player(): HTMLVideoElement | null { return this._videoRef; }
-
-
-    componentDidMount() {
-        this.props.setContentView?.(this); // this tells the DocumentView that this VideoBox is the "content" of the document.  this allows the DocumentView to indirectly call getAnchor() on the VideoBox when making a link.
-        if (this.youtubeVideoId) {
-            const youtubeaspect = 400 / 315;
-            const nativeWidth = Doc.NativeWidth(this.layoutDoc);
-            const nativeHeight = Doc.NativeHeight(this.layoutDoc);
-            if (!nativeWidth || !nativeHeight) {
-                if (!nativeWidth) Doc.SetNativeWidth(this.dataDoc, 600);
-                Doc.SetNativeHeight(this.dataDoc, (nativeWidth || 600) / youtubeaspect);
-                this.layoutDoc._height = NumCast(this.layoutDoc._width) / youtubeaspect;
+      public static LayoutString(fieldKey: string) { return FieldView.LayoutString(VideoBox, fieldKey); }
+      /**
+       * Uploads an image buffer to the server and stores with specified filename.  by default the image
+       * is stored at multiple resolutions each retrieved by using the filename appended with _o, _s, _m, _l (indicating original, small, medium, or large)
+       * @param imageUri the bytes of the image
+       * @param returnedFilename the base filename to store the image on the server
+       * @param nosuffix optionally suppress creating multiple resolution images
+       */
+      public static async convertDataUri(imageUri: string, returnedFilename: string, nosuffix = false, replaceRootFilename?: string) {
+            try {
+                  const posting = Utils.prepend("/uploadURI");
+                  const returnedUri = await rp.post(posting, {
+                        body: {
+                              uri: imageUri,
+                              name: returnedFilename,
+                              nosuffix,
+                              replaceRootFilename
+                        },
+                        json: true,
+                  });
+                  return returnedUri;
+
+            } catch (e) {
+                  console.log("VideoBox :" + e);
             }
-        }
-        this.player && this.setPlayheadTime(0);
-    }
-
-    componentWillUnmount() {
-        this.removeCurrentlyPlaying();
-        this.Pause();
-        Object.keys(this._disposers).forEach(d => this._disposers[d]?.());
-    }
-
-
-    // plays video
-    @action public Play = (update: boolean = true) => {
-        // if (Doc.UserDoc().presentationMode === 'watching' && !this._playing) {
-        //     console.log('VideoBox : Play : presentation mode', this._playing);
-        //     return;
-        // }
-
-        // if presentation isn't null, call followmovements on the recording api
-        if (this.presentation) {
-            const err = RecordingApi.Instance.playMovements(this.presentation, this.player?.currentTime || 0, this);
-            err && console.log(err)
-        }
-
-        this._playing = true;
-        const eleTime = this.player?.currentTime || 0;
-        if (this.timeline) {
-            let start = eleTime >= this.timeline.trimEnd || eleTime <= this.timeline.trimStart ? this.timeline.trimStart : eleTime;
-
-            if (this._finished) {
-                // restarts video if reached end on previous play
-                this._finished = false;
-                start = this.timeline.trimStart;
+      }
+
+      static _youtubeIframeCounter: number = 0;
+      static heightPercent = 80; // height of video relative to videoBox when timeline is open
+      private _disposers: { [name: string]: IReactionDisposer } = {};
+      private _youtubePlayer: YT.Player | undefined = undefined;
+      private _videoRef: HTMLVideoElement | null = null; // <video> ref
+      private _contentRef: HTMLDivElement | null = null; // ref to div that wraps video and controls for full screen
+      private _youtubeIframeId: number = -1;
+      private _youtubeContentCreated = false;
+      private _audioPlayer: HTMLAudioElement | null = null;
+      private _mainCont: React.RefObject<HTMLDivElement> = React.createRef(); // outermost div
+      private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef();
+      private _playRegionTimer: any = null; // timeout for playback
+      @observable _stackedTimeline: any; // CollectionStackedTimeline ref
+      @observable static _nativeControls: boolean; // default html controls
+      @observable _marqueeing: number[] | undefined; // coords for marquee selection
+      @observable _savedAnnotations = new ObservableMap<number, HTMLDivElement[]>();
+      @observable _screenCapture = false;
+      @observable _clicking = false; // used for transition between showing/hiding timeline
+      @observable _forceCreateYouTubeIFrame = false;
+      @observable _playTimer?: NodeJS.Timeout = undefined;
+      @observable _fullScreen = false;
+      @observable _playing = false;
+      @observable _finished: boolean = false; // has playback reached end of clip
+      @observable _volume: number = 1;
+      @observable _muted: boolean = false;
+
+      @computed get links() { return DocListCast(this.dataDoc.links); }
+      @computed get heightPercent() { return NumCast(this.layoutDoc._timelineHeightPercent, 100); } // current percent of video relative to VideoBox height
+      // @computed get rawDuration() { return NumCast(this.dataDoc[this.fieldKey + "-duration"]); }
+      @observable rawDuration: number = 0;
+
+
+      @computed get youtubeVideoId() {
+            const field = Cast(this.dataDoc[this.props.fieldKey], VideoField);
+            return field && field.url.href.indexOf("youtube") !== -1 ? ((arr: string[]) => arr[arr.length - 1])(field.url.href.split("/")) : "";
+      }
+
+
+      // returns the path of the audio file
+      @computed get audiopath() {
+            const field = Cast(this.props.Document[this.props.fieldKey + '-audio'], AudioField, null);
+            const vfield = Cast(this.dataDoc[this.fieldKey], VideoField, null);
+            return field?.url.href ?? vfield?.url.href ?? "";
+      }
+
+      // returns the presentation data if it exists, null otherwise
+      @computed get presentation() {
+            const data = this.dataDoc[this.fieldKey + '-presentation'];
+            return data ? JSON.parse(data) : null;
+      }
+
+
+      @computed private get timeline() { return this._stackedTimeline; }
+      private get transition() { return this._clicking ? "left 0.5s, width 0.5s, height 0.5s" : ""; } // css transition for hiding/showing timeline
+      public get player(): HTMLVideoElement | null { return this._videoRef; }
+
+
+      componentDidMount() {
+            this.props.setContentView?.(this); // this tells the DocumentView that this VideoBox is the "content" of the document.  this allows the DocumentView to indirectly call getAnchor() on the VideoBox when making a link.
+            if (this.youtubeVideoId) {
+                  const youtubeaspect = 400 / 315;
+                  const nativeWidth = Doc.NativeWidth(this.layoutDoc);
+                  const nativeHeight = Doc.NativeHeight(this.layoutDoc);
+                  if (!nativeWidth || !nativeHeight) {
+                        if (!nativeWidth) Doc.SetNativeWidth(this.dataDoc, 600);
+                        Doc.SetNativeHeight(this.dataDoc, (nativeWidth || 600) / youtubeaspect);
+                        this.layoutDoc._height = NumCast(this.layoutDoc._width) / youtubeaspect;
+                  }
             }
+            this.player && this.setPlayheadTime(0);
+      }
+
+      componentWillUnmount() {
+            this.removeCurrentlyPlaying();
+            this.Pause();
+            Object.keys(this._disposers).forEach(d => this._disposers[d]?.());
+      }
+
+
+      // plays video
+      @action public Play = (update: boolean = true) => {
+            // if (Doc.UserDoc().presentationMode === 'watching' && !this._playing) {
+            //     console.log('VideoBox : Play : presentation mode', this._playing);
+            //     return;
+            // }
+
+            // if presentation isn't null, call followmovements on the recording api
+            if (this.presentation) {
+                  const err = RecordingApi.Instance.playMovements(this.presentation, this.player?.currentTime || 0, this);
+                  err && console.log(err)
+            }
+
+            this._playing = true;
+            const eleTime = this.player?.currentTime || 0;
+            if (this.timeline) {
+                  let start = eleTime >= this.timeline.trimEnd || eleTime <= this.timeline.trimStart ? this.timeline.trimStart : eleTime;
+
+                  if (this._finished) {
+                        // restarts video if reached end on previous play
+                        this._finished = false;
+                        start = this.timeline.trimStart;
+                  }
+
+                  try {
+                        this._audioPlayer && this.player && (this._audioPlayer.currentTime = this.player?.currentTime);
+                        update && this.player && this.playFrom(start, undefined, true);
+                        update && this._audioPlayer?.play();
+                        update && this._youtubePlayer?.playVideo();
+                        this._youtubePlayer && !this._playTimer && (this._playTimer = setInterval(this.updateTimecode, 5));
+                  } catch (e) {
+                        console.log("Video Play Exception:", e);
+                  }
+            }
+            this.updateTimecode();
+      }
 
+      // goes to time
+      @action public Seek(time: number) {
             try {
-                this._audioPlayer && this.player && (this._audioPlayer.currentTime = this.player?.currentTime);
-                update && this.player && this.playFrom(start, undefined, true);
-                update && this._audioPlayer?.play();
-                update && this._youtubePlayer?.playVideo();
-                this._youtubePlayer && !this._playTimer && (this._playTimer = setInterval(this.updateTimecode, 5));
+                  this._youtubePlayer?.seekTo(Math.round(time), true);
             } catch (e) {
-                console.log("Video Play Exception:", e);
+                  console.log("Video Seek Exception:", e);
             }
-        }
-        this.updateTimecode();
-    }
-
-    // goes to time
-    @action public Seek(time: number) {
-        try {
-            this._youtubePlayer?.seekTo(Math.round(time), true);
-        } catch (e) {
-            console.log("Video Seek Exception:", e);
-        }
-        this.player && (this.player.currentTime = time);
-        this._audioPlayer && (this._audioPlayer.currentTime = time);
-        // TODO: revisit this and clean it
-        if ((this.player?.currentTime || -1) < this.rawDuration) {
-            this._finished = false;
-        }
-    }
-
-    // pauses video
-    @action public Pause = (update: boolean = true) => {
-        if (this.presentation) {
-            console.log('VideoBox : Pause');
-            const err = RecordingApi.Instance.pauseMovements();
-            err && console.log(err);
-        }
-
-        this._playing = false;
-        this.removeCurrentlyPlaying();
-        try {
-            update && this.player?.pause();
-            update && this._audioPlayer?.pause();
-            update && this._youtubePlayer?.pauseVideo();
-            this._youtubePlayer && this._playTimer && clearInterval(this._playTimer);
-            this._youtubePlayer?.seekTo(this._youtubePlayer?.getCurrentTime(), true);
-        } catch (e) {
-            console.log("Video Pause Exception:", e);
-        }
-        this._youtubePlayer && SelectionManager.DeselectAll(); // if we don't deselect the player, then we get an annoying YouTube spinner I guess telling us we're paused.
-        this._playTimer = undefined;
-        this.updateTimecode();
-        if (!this._finished) clearTimeout(this._playRegionTimer); // if paused in the middle of playback, prevents restart on next play
-    }
-
-    // toggles video full screen
-    @action public FullScreen = () => {
-        if (document.fullscreenElement === this._contentRef) {
-            this._fullScreen = false;
-            this.player && this._contentRef && document.exitFullscreen();
-        }
-        else {
-            this._fullScreen = true;
-            this.player && this._contentRef && this._contentRef.requestFullscreen();
-
-        }
-        try {
-            this._youtubePlayer && this.props.addDocTab(this.rootDoc, "add");
-        } catch (e) {
-            console.log("Video FullScreen Exception:", e);
-        }
-    }
-
-
-    // creates and links snapshot photo of current video frame
-    @action public Snapshot = (downX?: number, downY?: number, cb?: (filename: string, x: number | undefined, y: number | undefined) => void) => {
-        const width = NumCast(this.layoutDoc._width);
-        const canvas = document.createElement('canvas');
-        canvas.width = 640;
-        canvas.height = 640 * Doc.NativeHeight(this.layoutDoc) / (Doc.NativeWidth(this.layoutDoc) || 1);
-        const ctx = canvas.getContext('2d');//draw image to canvas. scale to target dimensions
-        if (ctx) {
-            this._videoRef && ctx.drawImage(this._videoRef, 0, 0, canvas.width, canvas.height);
-        }
-
-        if (!this._videoRef) {
-            const b = Docs.Create.LabelDocument({
-                x: NumCast(this.layoutDoc.x) + width, y: NumCast(this.layoutDoc.y, 1),
-                _width: 150, _height: 50, title: (this.layoutDoc._currentTimecode || 0).toString(),
-                _isLinkButton: true
-            });
-            this.props.addDocument?.(b);
-            DocUtils.MakeLink({ doc: b }, { doc: this.rootDoc }, "video snapshot");
-            Networking.PostToServer("/youtubeScreenshot", {
-                id: this.youtubeVideoId,
-                timecode: this.layoutDoc._currentTimecode
-            }).then(response => {
-                const resolved = response?.accessPaths?.agnostic?.client;
-                if (resolved) {
-                    this.props.removeDocument?.(b);
-                    this.createRealSummaryLink(resolved);
-                }
-            });
-        } else {
-            //convert to desired file format
-            const dataUrl = canvas.toDataURL('image/png'); // can also use 'image/png'
-            // if you want to preview the captured image,
-            const retitled = StrCast(this.rootDoc.title).replace(/[ -\.]/g, "");
-            const encodedFilename = encodeURIComponent("snapshot" + retitled + "_" + (this.layoutDoc._currentTimecode || 0).toString().replace(/\./, "_"));
-            const filename = basename(encodedFilename);
-            VideoBox.convertDataUri(dataUrl, filename).then((returnedFilename: string) =>
-                returnedFilename && (cb ?? this.createRealSummaryLink)(returnedFilename, downX, downY));
-        }
-    }
-
-    updateIcon = () => {
-        const makeIcon = (returnedfilename: string) => {
-            this.dataDoc.icon = new ImageField(returnedfilename);
-            this.dataDoc["icon-nativeWidth"] = this.layoutDoc[WidthSym]();
-            this.dataDoc["icon-nativeHeight"] = this.layoutDoc[HeightSym]();
-        };
-        this.Snapshot(undefined, undefined, makeIcon);
-    }
-
-    // creates link for snapshot
-    createRealSummaryLink = (imagePath: string, downX?: number, downY?: number) => {
-        const url = !imagePath.startsWith("/") ? Utils.CorsProxy(imagePath) : imagePath;
-        const width = NumCast(this.layoutDoc._width) || 1;
-        const height = NumCast(this.layoutDoc._height);
-        const imageSummary = Docs.Create.ImageDocument(url, {
-            _nativeWidth: Doc.NativeWidth(this.layoutDoc), _nativeHeight: Doc.NativeHeight(this.layoutDoc),
-            x: NumCast(this.layoutDoc.x) + width, y: NumCast(this.layoutDoc.y), _isLinkButton: true,
-            _width: 150, _height: height / width * 150, title: "--snapshot" + NumCast(this.layoutDoc._currentTimecode) + " image-"
-        });
-        Doc.SetNativeWidth(Doc.GetProto(imageSummary), Doc.NativeWidth(this.layoutDoc));
-        Doc.SetNativeHeight(Doc.GetProto(imageSummary), Doc.NativeHeight(this.layoutDoc));
-        this.props.addDocument?.(imageSummary);
-        const link = DocUtils.MakeLink({ doc: imageSummary }, { doc: this.getAnchor() }, "video snapshot");
-        link && (Doc.GetProto(link.anchor2 as Doc).timecodeToHide = NumCast((link.anchor2 as Doc).timecodeToShow) + 3);
-        setTimeout(() =>
-            (downX !== undefined && downY !== undefined) && DocumentManager.Instance.getFirstDocumentView(imageSummary)?.startDragging(downX, downY, "move", true));
-    }
-
-
-    getAnchor = () => {
-        const timecode = Cast(this.layoutDoc._currentTimecode, "number", null);
-        const marquee = AnchorMenu.Instance.GetAnchor?.();
-        return CollectionStackedTimeline.createAnchor(this.rootDoc, this.dataDoc, this.annotationKey, "_timecodeToShow"/* videoStart */, "_timecodeToHide" /* videoEnd */, timecode ? timecode : undefined, undefined, marquee) || this.rootDoc;
-    }
-
-
-    // sets video info on load
-    videoLoad = action(() => {
-        const aspect = this.player!.videoWidth / this.player!.videoHeight;
-        Doc.SetNativeWidth(this.dataDoc, this.player!.videoWidth);
-        Doc.SetNativeHeight(this.dataDoc, this.player!.videoHeight);
-        this.layoutDoc._height = NumCast(this.layoutDoc._width) / aspect;
-        if (Number.isFinite(this.player!.duration)) {
-            this.rawDuration = this.player!.duration;
-        } else this.rawDuration = NumCast(this.dataDoc[this.fieldKey + "-duration"]);
-    });
-
-
-    // updates video time
-    @action
-    updateTimecode = () => {
-        this.player && (this.layoutDoc._currentTimecode = this.player.currentTime);
-        try {
-            this._youtubePlayer && (this.layoutDoc._currentTimecode = this._youtubePlayer.getCurrentTime?.());
-        } catch (e) {
-            console.log("Video Timecode Exception:", e);
-        }
-    }
-
-
-    // sets video element ref
-    @action
-    setVideoRef = (vref: HTMLVideoElement | null) => {
-        console.log("SEt video ref " +  vref)
-        this._videoRef = vref;
-        if (vref) {
-            this._videoRef!.ontimeupdate = this.updateTimecode;
-            // @ts-ignore
-            // vref.onfullscreenchange = action((e) => this._fullScreen = vref.webkitDisplayingFullscreen);
-            this._disposers.reactionDisposer?.();
-            this._disposers.reactionDisposer = reaction(() => NumCast(this.layoutDoc._currentTimecode),
-                time => {
-                    !this._playing && (vref.currentTime = time);
-                    console.log("vref time = " + vref.currentTime)
-                }, { fireImmediately: true });
-        }
-    }
-
-    // set ref for div that wraps video and controls for fullscreen
-    @action
-    setContentRef = (cref: HTMLDivElement | null) => {
-        this._contentRef = cref;
-        if (cref) {
-            cref.onfullscreenchange = action((e) => this._fullScreen = (document.fullscreenElement === cref));
-        }
-    }
-
-
-    // context menu
-    specificContextMenu = (e: React.MouseEvent): void => {
-        const field = Cast(this.dataDoc[this.props.fieldKey], VideoField);
-        if (field) {
-            const url = field.url.href;
-            const subitems: ContextMenuProps[] = [];
-            subitems.push({ description: "Full Screen", event: this.FullScreen, icon: "expand" });
-            subitems.push({ description: "Take Snapshot", event: this.Snapshot, icon: "expand-arrows-alt" });
-            this.rootDoc.type === DocumentType.SCREENSHOT && subitems.push({
-                description: "Screen Capture", event: (async () => {
-                    runInAction(() => this._screenCapture = !this._screenCapture);
-                    this._videoRef!.srcObject = !this._screenCapture ? undefined : await (navigator.mediaDevices as any).getDisplayMedia({ video: true });
-                }), icon: "expand-arrows-alt"
+            this.player && (this.player.currentTime = time);
+            this._audioPlayer && (this._audioPlayer.currentTime = time);
+            // TODO: revisit this and clean it
+            if ((this.player?.currentTime || -1) < this.rawDuration) {
+                  this._finished = false;
+            }
+      }
+
+      // pauses video
+      @action public Pause = (update: boolean = true) => {
+            if (this.presentation) {
+                  console.log('VideoBox : Pause');
+                  const err = RecordingApi.Instance.pauseMovements();
+                  err && console.log(err);
+            }
+
+            this._playing = false;
+            this.removeCurrentlyPlaying();
+            try {
+                  update && this.player?.pause();
+                  update && this._audioPlayer?.pause();
+                  update && this._youtubePlayer?.pauseVideo();
+                  this._youtubePlayer && this._playTimer && clearInterval(this._playTimer);
+                  this._youtubePlayer?.seekTo(this._youtubePlayer?.getCurrentTime(), true);
+            } catch (e) {
+                  console.log("Video Pause Exception:", e);
+            }
+            this._youtubePlayer && SelectionManager.DeselectAll(); // if we don't deselect the player, then we get an annoying YouTube spinner I guess telling us we're paused.
+            this._playTimer = undefined;
+            this.updateTimecode();
+            if (!this._finished) clearTimeout(this._playRegionTimer); // if paused in the middle of playback, prevents restart on next play
+      }
+
+      // toggles video full screen
+      @action public FullScreen = () => {
+            if (document.fullscreenElement === this._contentRef) {
+                  this._fullScreen = false;
+                  this.player && this._contentRef && document.exitFullscreen();
+            }
+            else {
+                  this._fullScreen = true;
+                  this.player && this._contentRef && this._contentRef.requestFullscreen();
+
+            }
+            try {
+                  this._youtubePlayer && this.props.addDocTab(this.rootDoc, "add");
+            } catch (e) {
+                  console.log("Video FullScreen Exception:", e);
+            }
+      }
+
+
+      // creates and links snapshot photo of current video frame
+      @action public Snapshot = (downX?: number, downY?: number, cb?: (filename: string, x: number | undefined, y: number | undefined) => void) => {
+            const width = NumCast(this.layoutDoc._width);
+            const canvas = document.createElement('canvas');
+            canvas.width = 640;
+            canvas.height = 640 * Doc.NativeHeight(this.layoutDoc) / (Doc.NativeWidth(this.layoutDoc) || 1);
+            const ctx = canvas.getContext('2d');//draw image to canvas. scale to target dimensions
+            if (ctx) {
+                  this._videoRef && ctx.drawImage(this._videoRef, 0, 0, canvas.width, canvas.height);
+            }
+
+            if (!this._videoRef) {
+                  const b = Docs.Create.LabelDocument({
+                        x: NumCast(this.layoutDoc.x) + width, y: NumCast(this.layoutDoc.y, 1),
+                        _width: 150, _height: 50, title: (this.layoutDoc._currentTimecode || 0).toString(),
+                        _isLinkButton: true
+                  });
+                  this.props.addDocument?.(b);
+                  DocUtils.MakeLink({ doc: b }, { doc: this.rootDoc }, "video snapshot");
+                  Networking.PostToServer("/youtubeScreenshot", {
+                        id: this.youtubeVideoId,
+                        timecode: this.layoutDoc._currentTimecode
+                  }).then(response => {
+                        const resolved = response?.accessPaths?.agnostic?.client;
+                        if (resolved) {
+                              this.props.removeDocument?.(b);
+                              this.createRealSummaryLink(resolved);
+                        }
+                  });
+            } else {
+                  //convert to desired file format
+                  const dataUrl = canvas.toDataURL('image/png'); // can also use 'image/png'
+                  // if you want to preview the captured image,
+                  const retitled = StrCast(this.rootDoc.title).replace(/[ -\.]/g, "");
+                  const encodedFilename = encodeURIComponent("snapshot" + retitled + "_" + (this.layoutDoc._currentTimecode || 0).toString().replace(/\./, "_"));
+                  const filename = basename(encodedFilename);
+                  VideoBox.convertDataUri(dataUrl, filename).then((returnedFilename: string) =>
+                        returnedFilename && (cb ?? this.createRealSummaryLink)(returnedFilename, downX, downY));
+            }
+      }
+
+      updateIcon = () => {
+            const makeIcon = (returnedfilename: string) => {
+                  this.dataDoc.icon = new ImageField(returnedfilename);
+                  this.dataDoc["icon-nativeWidth"] = this.layoutDoc[WidthSym]();
+                  this.dataDoc["icon-nativeHeight"] = this.layoutDoc[HeightSym]();
+            };
+            this.Snapshot(undefined, undefined, makeIcon);
+      }
+
+      // creates link for snapshot
+      createRealSummaryLink = (imagePath: string, downX?: number, downY?: number) => {
+            const url = !imagePath.startsWith("/") ? Utils.CorsProxy(imagePath) : imagePath;
+            const width = NumCast(this.layoutDoc._width) || 1;
+            const height = NumCast(this.layoutDoc._height);
+            const imageSummary = Docs.Create.ImageDocument(url, {
+                  _nativeWidth: Doc.NativeWidth(this.layoutDoc), _nativeHeight: Doc.NativeHeight(this.layoutDoc),
+                  x: NumCast(this.layoutDoc.x) + width, y: NumCast(this.layoutDoc.y), _isLinkButton: true,
+                  _width: 150, _height: height / width * 150, title: "--snapshot" + NumCast(this.layoutDoc._currentTimecode) + " image-"
             });
-            subitems.push({ description: (this.layoutDoc.dontAutoFollowLinks ? "" : "Don't") + " follow links when encountered", event: () => this.layoutDoc.dontAutoFollowLinks = !this.layoutDoc.dontAutoFollowLinks, icon: "expand-arrows-alt" });
-            subitems.push({ description: (this.layoutDoc.dontAutoPlayFollowedLinks ? "" : "Don't") + " play when link is selected", event: () => this.layoutDoc.dontAutoPlayFollowedLinks = !this.layoutDoc.dontAutoPlayFollowedLinks, icon: "expand-arrows-alt" });
-            subitems.push({ description: (this.layoutDoc.autoPlayAnchors ? "Don't auto play" : "Auto play") + " anchors onClick", event: () => this.layoutDoc.autoPlayAnchors = !this.layoutDoc.autoPlayAnchors, icon: "expand-arrows-alt" });
-            // subitems.push({ description: "Toggle Native Controls", event: action(() => VideoBox._nativeControls = !VideoBox._nativeControls), icon: "expand-arrows-alt" });
-            // subitems.push({ description: "Start Trim All", event: () => this.startTrim(TrimScope.All), icon: "expand-arrows-alt" });
-            // subitems.push({ description: "Start Trim Clip", event: () => this.startTrim(TrimScope.Clip), icon: "expand-arrows-alt" });
-            // subitems.push({ description: "Stop Trim", event: () => this.finishTrim(), icon: "expand-arrows-alt" });
-            subitems.push({ description: "Copy path", event: () => { Utils.CopyText(url); }, icon: "expand-arrows-alt" });
-            ContextMenu.Instance.addItem({ description: "Options...", subitems: subitems, icon: "video" });
-        }
-    }
-
-
-    // ref for updating time
-    setAudioRef = (e: HTMLAudioElement | null) => this._audioPlayer = e;
-
-    // renders the video and audio
-    @computed get content() {
-        const field = Cast(this.dataDoc[this.fieldKey], VideoField);
-        const interactive = CurrentUserUtils.SelectedTool !== InkTool.None || !this.props.isSelected() ? "" : "-interactive";
-        const classname = "videoBox-content" + (this._fullScreen ? "-fullScreen" : "") + interactive;
-        return !field ? <div key="loading">Loading</div> :
-            <div className="videoBox-contentContainer" key="container" style={{ mixBlendMode: "multiply" }}>
-                <div className={classname} ref={this.setContentRef} onPointerDown={(e) => this._fullScreen && e.stopPropagation()}>
-                    {this.uIButtons}
-                    <video key="video" autoPlay={this._screenCapture} ref={this.setVideoRef} style={this._fullScreen ? this.fullScreenSize() : {}}
-                        onCanPlay={this.videoLoad}
-                        controls={VideoBox._nativeControls}
-                        onPlay={() => {
-                            // console.log("PLAY from CONTENT")
-                            //this.Play()
-                        }}
-                        onSeeked={this.updateTimecode}
-                        // onPause={() => this.Pause() }
-                        onClick={e => e.preventDefault()}>
-                        <source src={field.url.href} type="video/mp4" />
-                        Not supported.
-                    </video>
-                    {!this.audiopath || this.audiopath === field.url.href ? (null) :
-                        <audio ref={this.setAudioRef} className={`audiobox-control${this.props.isContentActive() ? "-interactive" : ""}`}>
-                            <source src={this.audiopath} type="audio/mpeg" />
-                            Not supported.
-                        </audio>}
-                </div>
-            </div>;
-    }
-
-
-    @action youtubeIframeLoaded = (e: any) => {
-        if (!this._youtubeContentCreated) {
-            this._forceCreateYouTubeIFrame = !this._forceCreateYouTubeIFrame;
-            return;
-        }
-        else this._youtubeContentCreated = false;
-
-        this.loadYouTube(e.target);
-    }
-
-    loadYouTube = (iframe: any) => {
-        let started = true;
-        const onYoutubePlayerStateChange = (event: any) => runInAction(() => {
-            if (started && event.data === YT.PlayerState.PLAYING) {
-                started = false;
-                this._youtubePlayer?.unMute();
-                //this.Pause();
-                return;
+            Doc.SetNativeWidth(Doc.GetProto(imageSummary), Doc.NativeWidth(this.layoutDoc));
+            Doc.SetNativeHeight(Doc.GetProto(imageSummary), Doc.NativeHeight(this.layoutDoc));
+            this.props.addDocument?.(imageSummary);
+            const link = DocUtils.MakeLink({ doc: imageSummary }, { doc: this.getAnchor() }, "video snapshot");
+            link && (Doc.GetProto(link.anchor2 as Doc).timecodeToHide = NumCast((link.anchor2 as Doc).timecodeToShow) + 3);
+            setTimeout(() =>
+                  (downX !== undefined && downY !== undefined) && DocumentManager.Instance.getFirstDocumentView(imageSummary)?.startDragging(downX, downY, "move", true));
+      }
+
+
+      getAnchor = () => {
+            const timecode = Cast(this.layoutDoc._currentTimecode, "number", null);
+            const marquee = AnchorMenu.Instance.GetAnchor?.();
+            return CollectionStackedTimeline.createAnchor(this.rootDoc, this.dataDoc, this.annotationKey, "_timecodeToShow"/* videoStart */, "_timecodeToHide" /* videoEnd */, timecode ? timecode : undefined, undefined, marquee) || this.rootDoc;
+      }
+
+
+      // sets video info on load
+      videoLoad = action(() => {
+            const aspect = this.player!.videoWidth / this.player!.videoHeight;
+            Doc.SetNativeWidth(this.dataDoc, this.player!.videoWidth);
+            Doc.SetNativeHeight(this.dataDoc, this.player!.videoHeight);
+            this.layoutDoc._height = NumCast(this.layoutDoc._width) / aspect;
+            if (Number.isFinite(this.player!.duration)) {
+                  this.rawDuration = this.player!.duration;
+            } else this.rawDuration = NumCast(this.dataDoc[this.fieldKey + "-duration"]);
+      });
+
+
+      // updates video time
+      @action
+      updateTimecode = () => {
+            this.player && (this.layoutDoc._currentTimecode = this.player.currentTime);
+            try {
+                  this._youtubePlayer && (this.layoutDoc._currentTimecode = this._youtubePlayer.getCurrentTime?.());
+            } catch (e) {
+                  console.log("Video Timecode Exception:", e);
+            }
+      }
+
+
+      // sets video element ref
+      @action
+      setVideoRef = (vref: HTMLVideoElement | null) => {
+            console.log("SEt video ref " + vref)
+            this._videoRef = vref;
+            if (vref) {
+                  this._videoRef!.ontimeupdate = this.updateTimecode;
+                  // @ts-ignore
+                  // vref.onfullscreenchange = action((e) => this._fullScreen = vref.webkitDisplayingFullscreen);
+                  this._disposers.reactionDisposer?.();
+                  this._disposers.reactionDisposer = reaction(() => NumCast(this.layoutDoc._currentTimecode),
+                        time => {
+                              !this._playing && (vref.currentTime = time);
+                              console.log("vref time = " + vref.currentTime)
+                        }, { fireImmediately: true });
             }
-            if (event.data === YT.PlayerState.PLAYING && !this._playing) this.Play(false);
-            if (event.data === YT.PlayerState.PAUSED && this._playing) this.Pause(false);
-        });
-        const onYoutubePlayerReady = (event: any) => {
-            this._disposers.reactionDisposer?.();
-            this._disposers.youtubeReactionDisposer?.();
-            this._disposers.reactionDisposer = reaction(() => this.layoutDoc._currentTimecode, () => !this._playing && this.Seek(NumCast(this.layoutDoc._currentTimecode)));
-            this._disposers.youtubeReactionDisposer = reaction(
-                () => CurrentUserUtils.SelectedTool === InkTool.None && this.props.isSelected(true) && !SnappingManager.GetIsDragging() && !DocumentDecorations.Instance.Interacting,
-                (interactive) => iframe.style.pointerEvents = interactive ? "all" : "none", { fireImmediately: true });
-        };
-        if (typeof (YT) === undefined) setTimeout(() => this.loadYouTube(iframe), 100);
-        else {
-            (YT as any)?.ready(() => {
-                this._youtubePlayer = new YT.Player(`${this.youtubeVideoId + this._youtubeIframeId}-player`, {
-                    events: {
-                        'onReady': this.props.dontRegisterView ? undefined : onYoutubePlayerReady,
-                        'onStateChange': this.props.dontRegisterView ? undefined : onYoutubePlayerStateChange,
-                    }
-                });
+      }
+
+      // set ref for div that wraps video and controls for fullscreen
+      @action
+      setContentRef = (cref: HTMLDivElement | null) => {
+            this._contentRef = cref;
+            if (cref) {
+                  cref.onfullscreenchange = action((e) => this._fullScreen = (document.fullscreenElement === cref));
+            }
+      }
+
+
+      // context menu
+      specificContextMenu = (e: React.MouseEvent): void => {
+            const field = Cast(this.dataDoc[this.props.fieldKey], VideoField);
+            if (field) {
+                  const url = field.url.href;
+                  const subitems: ContextMenuProps[] = [];
+                  subitems.push({ description: "Full Screen", event: this.FullScreen, icon: "expand" });
+                  subitems.push({ description: "Take Snapshot", event: this.Snapshot, icon: "expand-arrows-alt" });
+                  this.rootDoc.type === DocumentType.SCREENSHOT && subitems.push({
+                        description: "Screen Capture", event: (async () => {
+                              runInAction(() => this._screenCapture = !this._screenCapture);
+                              this._videoRef!.srcObject = !this._screenCapture ? undefined : await (navigator.mediaDevices as any).getDisplayMedia({ video: true });
+                        }), icon: "expand-arrows-alt"
+                  });
+                  subitems.push({ description: (this.layoutDoc.dontAutoFollowLinks ? "" : "Don't") + " follow links when encountered", event: () => this.layoutDoc.dontAutoFollowLinks = !this.layoutDoc.dontAutoFollowLinks, icon: "expand-arrows-alt" });
+                  subitems.push({ description: (this.layoutDoc.dontAutoPlayFollowedLinks ? "" : "Don't") + " play when link is selected", event: () => this.layoutDoc.dontAutoPlayFollowedLinks = !this.layoutDoc.dontAutoPlayFollowedLinks, icon: "expand-arrows-alt" });
+                  subitems.push({ description: (this.layoutDoc.autoPlayAnchors ? "Don't auto play" : "Auto play") + " anchors onClick", event: () => this.layoutDoc.autoPlayAnchors = !this.layoutDoc.autoPlayAnchors, icon: "expand-arrows-alt" });
+                  // subitems.push({ description: "Toggle Native Controls", event: action(() => VideoBox._nativeControls = !VideoBox._nativeControls), icon: "expand-arrows-alt" });
+                  // subitems.push({ description: "Start Trim All", event: () => this.startTrim(TrimScope.All), icon: "expand-arrows-alt" });
+                  // subitems.push({ description: "Start Trim Clip", event: () => this.startTrim(TrimScope.Clip), icon: "expand-arrows-alt" });
+                  // subitems.push({ description: "Stop Trim", event: () => this.finishTrim(), icon: "expand-arrows-alt" });
+                  subitems.push({ description: "Copy path", event: () => { Utils.CopyText(url); }, icon: "expand-arrows-alt" });
+                  // if the videobox was turned from a recording box
+                  if (this.dataDoc[this.fieldKey + "-recorded"] === true) {
+                        subitems.push({
+                              description: "Recreate recording", event: () => {
+                                    this.dataDoc.layout = RecordingBox.LayoutString(this.fieldKey);
+                                    // delete assoicated video data
+                                    this.dataDoc[this.props.fieldKey] = "";
+                                    this.dataDoc[this.fieldKey + "-duration"] = "";
+                                    // delete assoicated presentation data
+                                    this.dataDoc[this.fieldKey + "-presentation"] = "";
+                              }, icon: "expand-arrows-alt"
+                        });
+
+                  }
+                  ContextMenu.Instance.addItem({ description: "Options...", subitems: subitems, icon: "video" });
+            }
+      }
+
+
+      // ref for updating time
+      setAudioRef = (e: HTMLAudioElement | null) => this._audioPlayer = e;
+
+      // renders the video and audio
+      @computed get content() {
+            const field = Cast(this.dataDoc[this.fieldKey], VideoField);
+            const interactive = CurrentUserUtils.SelectedTool !== InkTool.None || !this.props.isSelected() ? "" : "-interactive";
+            const classname = "videoBox-content" + (this._fullScreen ? "-fullScreen" : "") + interactive;
+            return !field ? <div key="loading">Loading</div> :
+                  <div className="videoBox-contentContainer" key="container" style={{ mixBlendMode: "multiply" }}>
+                        <div className={classname} ref={this.setContentRef} onPointerDown={(e) => this._fullScreen && e.stopPropagation()}>
+                              {this.uIButtons}
+                              <video key="video" autoPlay={this._screenCapture} ref={this.setVideoRef} style={this._fullScreen ? this.fullScreenSize() : {}}
+                                    onCanPlay={this.videoLoad}
+                                    controls={VideoBox._nativeControls}
+                                    onPlay={() => {
+                                          // console.log("PLAY from CONTENT")
+                                          //this.Play()
+                                    }}
+                                    onSeeked={this.updateTimecode}
+                                    // onPause={() => this.Pause() }
+                                    onClick={e => e.preventDefault()}>
+                                    <source src={field.url.href} type="video/mp4" />
+                                    Not supported.
+                              </video>
+                              {!this.audiopath || this.audiopath === field.url.href ? (null) :
+                                    <audio ref={this.setAudioRef} className={`audiobox-control${this.props.isContentActive() ? "-interactive" : ""}`}>
+                                          <source src={this.audiopath} type="audio/mpeg" />
+                                          Not supported.
+                                    </audio>}
+                        </div>
+                  </div>;
+      }
+
+
+      @action youtubeIframeLoaded = (e: any) => {
+            if (!this._youtubeContentCreated) {
+                  this._forceCreateYouTubeIFrame = !this._forceCreateYouTubeIFrame;
+                  return;
+            }
+            else this._youtubeContentCreated = false;
+
+            this.loadYouTube(e.target);
+      }
+
+      loadYouTube = (iframe: any) => {
+            let started = true;
+            const onYoutubePlayerStateChange = (event: any) => runInAction(() => {
+                  if (started && event.data === YT.PlayerState.PLAYING) {
+                        started = false;
+                        this._youtubePlayer?.unMute();
+                        //this.Pause();
+                        return;
+                  }
+                  if (event.data === YT.PlayerState.PLAYING && !this._playing) this.Play(false);
+                  if (event.data === YT.PlayerState.PAUSED && this._playing) this.Pause(false);
             });
-        }
-    }
-
-
-    // for play button
-
-    onPlayDown = () => {
-        console.log("PLAY DOWN");
-        this._playing ? this.Pause() : this.Play();
-    }
-
-    // for fullscreen button
-    onFullDown = (e: React.PointerEvent) => {
-        this.FullScreen();
-        e.stopPropagation();
-        e.preventDefault();
-    }
-
-    // for snapshot button
-    onSnapshotDown = (e: React.PointerEvent) => {
-        setupMoveUpEvents(this, e, (e) => {
-            this.Snapshot(e.clientX, e.clientY);
-            return true;
-        }, emptyFunction, () => this.Snapshot());
-    }
-
-    // for show/hide timeline button, transitions between show/hide
-    @action
-    onTimelineHdlDown = (e: React.PointerEvent) => {
-        this._clicking = true;
-        setupMoveUpEvents(this, e,
-            action(encodeURIComponent => {
-                this._clicking = false;
-                if (this.props.isContentActive()) {
-                    // const local = this.props.ScreenToLocalTransform().scale(this.props.scaling?.() || 1).transformPoint(e.clientX, e.clientY);
-                    // this.layoutDoc._timelineHeightPercent = Math.max(0, Math.min(100, local[1] / this.props.PanelHeight() * 100));
-
-                    this.layoutDoc._timelineHeightPercent = 80;
-                }
-                return false;
-            }), emptyFunction,
-            () => {
-                this.layoutDoc._timelineHeightPercent = this.heightPercent !== 100 ? 100 : VideoBox.heightPercent;
-                setTimeout(action(() => this._clicking = false), 500);
-            }, this.props.isContentActive(), this.props.isContentActive());
-    }
-
-
-    // removes video from currently playing display
-    @action
-    removeCurrentlyPlaying = () => {
-        if (CollectionStackedTimeline.CurrentlyPlaying) {
-            const index = CollectionStackedTimeline.CurrentlyPlaying.indexOf(this.layoutDoc);
-            index !== -1 && CollectionStackedTimeline.CurrentlyPlaying.splice(index, 1);
-        }
-    }
-
-    // adds video to currently playing display
-    @action
-    addCurrentlyPlaying = () => {
-        if (!CollectionStackedTimeline.CurrentlyPlaying) {
-            CollectionStackedTimeline.CurrentlyPlaying = [];
-        }
-        if (CollectionStackedTimeline.CurrentlyPlaying.indexOf(this.layoutDoc) === -1) {
-            CollectionStackedTimeline.CurrentlyPlaying.push(this.layoutDoc);
-        }
-    }
-
-
-    @computed get youtubeContent() {
-        this._youtubeIframeId = VideoBox._youtubeIframeCounter++;
-        this._youtubeContentCreated = this._forceCreateYouTubeIFrame ? true : true;
-        const classname = "videoBox-content-YouTube" + (this._fullScreen ? "-fullScreen" : "");
-        const start = untracked(() => Math.round(NumCast(this.layoutDoc._currentTimecode)));
-        return <iframe key={this._youtubeIframeId} id={`${this.youtubeVideoId + this._youtubeIframeId}-player`}
-            onPointerLeave={this.updateTimecode}
-            onLoad={this.youtubeIframeLoaded} className={classname} width={Doc.NativeWidth(this.layoutDoc) || 640} height={Doc.NativeHeight(this.layoutDoc) || 390}
-            src={`https://www.youtube.com/embed/${this.youtubeVideoId}?enablejsapi=1&rel=0&showinfo=1&autoplay=0&mute=1&start=${start}&modestbranding=1&controls=${VideoBox._nativeControls ? 1 : 0}`} />;
-    }
-
-
-    // for annotating, adds doc with time info
-    @action.bound
-    addDocWithTimecode(doc: Doc | Doc[]): boolean {
-        const docs = doc instanceof Doc ? [doc] : doc;
-        const curTime = NumCast(this.layoutDoc._currentTimecode);
-        docs.forEach(doc => doc._timecodeToHide = (doc._timecodeToShow = curTime) + 1);
-        return this.addDocument(doc);
-    }
-
-
-    // play back the audio from seekTimeInSeconds, fullPlay tells whether clip is being played to end vs link range
-    @action
-    playFrom = (seekTimeInSeconds: number, endTime?: number, fullPlay: boolean = false) => {
-        clearTimeout(this._playRegionTimer);
-        if (Number.isNaN(this.player?.duration)) {
-            setTimeout(() => this.playFrom(seekTimeInSeconds, endTime), 500);
-        }
-        else if (this.player) {
-            // trimBounds override requested playback bounds
-            const end = Math.min(this.timeline?.trimEnd ?? this.rawDuration, endTime ?? this.timeline?.trimEnd ?? this.rawDuration);
-            const start = Math.max(this.timeline?.trimStart ?? 0, seekTimeInSeconds);
-            const playRegionDuration = end - start;
-            // checks if times are within clip range
-            if (seekTimeInSeconds >= 0 && (this.timeline?.trimStart || 0) <= end && seekTimeInSeconds <= (this.timeline?.trimEnd || this.rawDuration)) {
-                this.player.currentTime = start;
-                this._audioPlayer && (this._audioPlayer.currentTime = seekTimeInSeconds);
-                this.player.play();
-                this._audioPlayer?.play();
-                this._playing = true;
-                this.addCurrentlyPlaying();
-                this._playRegionTimer = setTimeout(
-                    () => {
-                        // need to keep track of if end of clip is reached so on next play, clip restarts
-                        if (fullPlay) {
-                            Doc.UserDoc().presentationMode = 'none';
-                            this._finished = true;
-                         }
-                        // removes from currently playing if playback has reached end of range marker
-                        else this.removeCurrentlyPlaying();
+            const onYoutubePlayerReady = (event: any) => {
+                  this._disposers.reactionDisposer?.();
+                  this._disposers.youtubeReactionDisposer?.();
+                  this._disposers.reactionDisposer = reaction(() => this.layoutDoc._currentTimecode, () => !this._playing && this.Seek(NumCast(this.layoutDoc._currentTimecode)));
+                  this._disposers.youtubeReactionDisposer = reaction(
+                        () => CurrentUserUtils.SelectedTool === InkTool.None && this.props.isSelected(true) && !SnappingManager.GetIsDragging() && !DocumentDecorations.Instance.Interacting,
+                        (interactive) => iframe.style.pointerEvents = interactive ? "all" : "none", { fireImmediately: true });
+            };
+            if (typeof (YT) === undefined) setTimeout(() => this.loadYouTube(iframe), 100);
+            else {
+                  (YT as any)?.ready(() => {
+                        this._youtubePlayer = new YT.Player(`${this.youtubeVideoId + this._youtubeIframeId}-player`, {
+                              events: {
+                                    'onReady': this.props.dontRegisterView ? undefined : onYoutubePlayerReady,
+                                    'onStateChange': this.props.dontRegisterView ? undefined : onYoutubePlayerStateChange,
+                              }
+                        });
+                  });
+            }
+      }
+
+
+      // for play button
+
+      onPlayDown = () => {
+            console.log("PLAY DOWN");
+            this._playing ? this.Pause() : this.Play();
+      }
+
+      // for fullscreen button
+      onFullDown = (e: React.PointerEvent) => {
+            this.FullScreen();
+            e.stopPropagation();
+            e.preventDefault();
+      }
+
+      // for snapshot button
+      onSnapshotDown = (e: React.PointerEvent) => {
+            setupMoveUpEvents(this, e, (e) => {
+                  this.Snapshot(e.clientX, e.clientY);
+                  return true;
+            }, emptyFunction, () => this.Snapshot());
+      }
+
+      // for show/hide timeline button, transitions between show/hide
+      @action
+      onTimelineHdlDown = (e: React.PointerEvent) => {
+            this._clicking = true;
+            setupMoveUpEvents(this, e,
+                  action(encodeURIComponent => {
+                        this._clicking = false;
+                        if (this.props.isContentActive()) {
+                              // const local = this.props.ScreenToLocalTransform().scale(this.props.scaling?.() || 1).transformPoint(e.clientX, e.clientY);
+                              // this.layoutDoc._timelineHeightPercent = Math.max(0, Math.min(100, local[1] / this.props.PanelHeight() * 100));
+
+                              this.layoutDoc._timelineHeightPercent = 80;
+                        }
+                        return false;
+                  }), emptyFunction,
+                  () => {
+                        this.layoutDoc._timelineHeightPercent = this.heightPercent !== 100 ? 100 : VideoBox.heightPercent;
+                        setTimeout(action(() => this._clicking = false), 500);
+                  }, this.props.isContentActive(), this.props.isContentActive());
+      }
+
+
+      // removes video from currently playing display
+      @action
+      removeCurrentlyPlaying = () => {
+            if (CollectionStackedTimeline.CurrentlyPlaying) {
+                  const index = CollectionStackedTimeline.CurrentlyPlaying.indexOf(this.layoutDoc);
+                  index !== -1 && CollectionStackedTimeline.CurrentlyPlaying.splice(index, 1);
+            }
+      }
+
+      // adds video to currently playing display
+      @action
+      addCurrentlyPlaying = () => {
+            if (!CollectionStackedTimeline.CurrentlyPlaying) {
+                  CollectionStackedTimeline.CurrentlyPlaying = [];
+            }
+            if (CollectionStackedTimeline.CurrentlyPlaying.indexOf(this.layoutDoc) === -1) {
+                  CollectionStackedTimeline.CurrentlyPlaying.push(this.layoutDoc);
+            }
+      }
+
+
+      @computed get youtubeContent() {
+            this._youtubeIframeId = VideoBox._youtubeIframeCounter++;
+            this._youtubeContentCreated = this._forceCreateYouTubeIFrame ? true : true;
+            const classname = "videoBox-content-YouTube" + (this._fullScreen ? "-fullScreen" : "");
+            const start = untracked(() => Math.round(NumCast(this.layoutDoc._currentTimecode)));
+            return <iframe key={this._youtubeIframeId} id={`${this.youtubeVideoId + this._youtubeIframeId}-player`}
+                  onPointerLeave={this.updateTimecode}
+                  onLoad={this.youtubeIframeLoaded} className={classname} width={Doc.NativeWidth(this.layoutDoc) || 640} height={Doc.NativeHeight(this.layoutDoc) || 390}
+                  src={`https://www.youtube.com/embed/${this.youtubeVideoId}?enablejsapi=1&rel=0&showinfo=1&autoplay=0&mute=1&start=${start}&modestbranding=1&controls=${VideoBox._nativeControls ? 1 : 0}`} />;
+      }
+
+
+      // for annotating, adds doc with time info
+      @action.bound
+      addDocWithTimecode(doc: Doc | Doc[]): boolean {
+            const docs = doc instanceof Doc ? [doc] : doc;
+            const curTime = NumCast(this.layoutDoc._currentTimecode);
+            docs.forEach(doc => doc._timecodeToHide = (doc._timecodeToShow = curTime) + 1);
+            return this.addDocument(doc);
+      }
+
+
+      // play back the audio from seekTimeInSeconds, fullPlay tells whether clip is being played to end vs link range
+      @action
+      playFrom = (seekTimeInSeconds: number, endTime?: number, fullPlay: boolean = false) => {
+            clearTimeout(this._playRegionTimer);
+            if (Number.isNaN(this.player?.duration)) {
+                  setTimeout(() => this.playFrom(seekTimeInSeconds, endTime), 500);
+            }
+            else if (this.player) {
+                  // trimBounds override requested playback bounds
+                  const end = Math.min(this.timeline?.trimEnd ?? this.rawDuration, endTime ?? this.timeline?.trimEnd ?? this.rawDuration);
+                  const start = Math.max(this.timeline?.trimStart ?? 0, seekTimeInSeconds);
+                  const playRegionDuration = end - start;
+                  // checks if times are within clip range
+                  if (seekTimeInSeconds >= 0 && (this.timeline?.trimStart || 0) <= end && seekTimeInSeconds <= (this.timeline?.trimEnd || this.rawDuration)) {
+                        this.player.currentTime = start;
+                        this._audioPlayer && (this._audioPlayer.currentTime = seekTimeInSeconds);
+                        this.player.play();
+                        this._audioPlayer?.play();
+                        this._playing = true;
+                        this.addCurrentlyPlaying();
+                        this._playRegionTimer = setTimeout(
+                              () => {
+                                    // need to keep track of if end of clip is reached so on next play, clip restarts
+                                    if (fullPlay) {
+                                          Doc.UserDoc().presentationMode = 'none';
+                                          this._finished = true;
+                                    }
+                                    // removes from currently playing if playback has reached end of range marker
+                                    else this.removeCurrentlyPlaying();
+                                    this.Pause();
+                              }, playRegionDuration * 1000);
+                  } else {
                         this.Pause();
-                    }, playRegionDuration * 1000);
-            } else {
-                this.Pause();
+                  }
             }
-        }
-    }
-
-
-    // ends trim, hides trim controls and displays new clip
-    @undoBatch
-    finishTrim = action(() => {
-        this.Pause();
-        this.setPlayheadTime(Math.max(Math.min(this.timeline?.trimEnd || 0, this.player!.currentTime), this.timeline?.trimStart || 0));
-        this.timeline?.StopTrimming();
-    });
-
-    // displays trim controls to start trimming clip
-    startTrim = (scope: TrimScope) => {
-        this.Pause();
-        this.timeline?.StartTrimming(scope);
-    }
-
-    // for trim button, double click displays full clip, single displays curr trim bounds
-    onClipPointerDown = (e: React.PointerEvent) => {
-        // if timeline isn't shown, show first then trim
-        this.heightPercent >= 100 && this.onTimelineHdlDown(e);
-        this.timeline && setupMoveUpEvents(this, e, returnFalse, returnFalse, action((e: PointerEvent, doubleTap?: boolean) => {
-            if (doubleTap) {
-                this.startTrim(TrimScope.All);
-            } else if (this.timeline) {
-                this.Pause();
-                this.timeline.IsTrimming !== TrimScope.None ? this.finishTrim() : this.startTrim(TrimScope.Clip);
+      }
+
+
+      // ends trim, hides trim controls and displays new clip
+      @undoBatch
+      finishTrim = action(() => {
+            this.Pause();
+            this.setPlayheadTime(Math.max(Math.min(this.timeline?.trimEnd || 0, this.player!.currentTime), this.timeline?.trimStart || 0));
+            this.timeline?.StopTrimming();
+      });
+
+      // displays trim controls to start trimming clip
+      startTrim = (scope: TrimScope) => {
+            this.Pause();
+            this.timeline?.StartTrimming(scope);
+      }
+
+      // for trim button, double click displays full clip, single displays curr trim bounds
+      onClipPointerDown = (e: React.PointerEvent) => {
+            // if timeline isn't shown, show first then trim
+            this.heightPercent >= 100 && this.onTimelineHdlDown(e);
+            this.timeline && setupMoveUpEvents(this, e, returnFalse, returnFalse, action((e: PointerEvent, doubleTap?: boolean) => {
+                  if (doubleTap) {
+                        this.startTrim(TrimScope.All);
+                  } else if (this.timeline) {
+                        this.Pause();
+                        this.timeline.IsTrimming !== TrimScope.None ? this.finishTrim() : this.startTrim(TrimScope.Clip);
+                  }
+            }));
+      }
+
+
+      // for volume slider sets volume
+      @action
+      setVolume = (volume: number) => {
+            if (this.player) {
+                  this._volume = volume;
+                  this.player.volume = volume;
+                  if (this._muted) {
+                        this.toggleMute();
+                  }
             }
-        }));
-    }
-
-
-    // for volume slider sets volume
-    @action
-    setVolume = (volume: number) => {
-        if (this.player) {
-            this._volume = volume;
-            this.player.volume = volume;
-            if (this._muted) {
-                this.toggleMute();
+      }
+
+      // toggles video mute
+      @action
+      toggleMute = () => {
+            if (this.player) {
+                  this._muted = !this._muted;
+                  this.player.muted = this._muted;
             }
-        }
-    }
+      }
 
-    // toggles video mute
-    @action
-    toggleMute = () => {
-        if (this.player) {
-            this._muted = !this._muted;
-            this.player.muted = this._muted;
-        }
-    }
-
-
-    // stretches vertically or horizontally depending on video orientation so video fits full screen
-    fullScreenSize() {
-        if (this._videoRef && this._videoRef.videoHeight / this._videoRef.videoWidth > 1) {
-            return { height: "100%" };
-        }
-        else {
-            return { width: "100%" };
-        }
-    }
-
-
-    // for zoom slider, sets timeline waveform zoom
-    zoom = (zoom: number) => {
-        this.timeline?.setZoom(zoom);
-    }
-
-
-    // plays link
-    playLink = (doc: Doc) => {
-        const startTime = Math.max(0, (this._stackedTimeline?.anchorStart(doc) || 0));
-        const endTime = this.timeline?.anchorEnd(doc);
-        if (startTime !== undefined) {
-            if (!this.layoutDoc.dontAutoPlayFollowedLinks) endTime ? this.playFrom(startTime, endTime) : this.playFrom(startTime);
-            else this.Seek(startTime);
-        }
-    }
-
-
-    // starts marquee selection
-    marqueeDown = (e: React.PointerEvent) => {
-        if (!e.altKey && e.button === 0 && this.layoutDoc._viewScale === 1 && this.props.isContentActive(true) && ![InkTool.Highlighter, InkTool.Pen].includes(CurrentUserUtils.SelectedTool)) {
-            setupMoveUpEvents(this, e, action(e => {
-                MarqueeAnnotator.clearAnnotations(this._savedAnnotations);
-                this._marqueeing = [e.clientX, e.clientY];
-                return true;
-            }), returnFalse, () => MarqueeAnnotator.clearAnnotations(this._savedAnnotations), false);
-        }
-    }
-
-    // ends marquee selection
-    @action
-    finishMarquee = () => {
-        this._marqueeing = undefined;
-        this.props.select(true);
-    }
-
-    timelineWhenChildContentsActiveChanged = action((isActive: boolean) => this.props.whenChildContentsActiveChanged(this._isAnyChildContentActive = isActive));
-
-    timelineScreenToLocal = () => this.props.ScreenToLocalTransform().scale(this.scaling()).translate(0, -this.heightPercent / 100 * this.props.PanelHeight());
-
-    setPlayheadTime = (time: number) => this.player!.currentTime = this.layoutDoc._currentTimecode = time;
-
-    timelineHeight = () => this.props.PanelHeight() * (100 - this.heightPercent) / 100;
-
-    playing = () => this._playing;
-
-    contentFunc = () => [this.youtubeVideoId ? this.youtubeContent : this.content];
-
-    scaling = () => this.props.scaling?.() || 1;
-
-    panelWidth = () => this.props.PanelWidth() * this.heightPercent / 100;
-    panelHeight = () => this.layoutDoc._fitWidth ? this.panelWidth() / (Doc.NativeAspect(this.rootDoc) || 1) : this.props.PanelHeight() * this.heightPercent / 100;
-
-    screenToLocalTransform = () => {
-        const offset = (this.props.PanelWidth() - this.panelWidth()) / 2 / this.scaling();
-        return this.props.ScreenToLocalTransform().translate(-offset, 0).scale(100 / this.heightPercent);
-    }
-
-    marqueeFitScaling = () => (this.props.scaling?.() || 1) * this.heightPercent / 100;
-    marqueeOffset = () => [this.panelWidth() / 2 * (1 - this.heightPercent / 100) / (this.heightPercent / 100), 0];
-
-    timelineDocFilter = () => [`_timelineLabel:true,${Utils.noRecursionHack}:x`];
-
-
-    // renders video controls
-    @computed get uIButtons() {
-        const curTime = NumCast(this.layoutDoc._currentTimecode) - (this.timeline?.clipStart || 0);
-        return <div className="videoBox-ui" style={this._fullScreen || this.heightPercent === 100 ? { fontSize: "40px", minWidth: "80%" } : {}}>
-            <div className="videobox-button"
-                title={this._playing ? "play" : "pause"}
-                onPointerDown={this.onPlayDown}>
-                <FontAwesomeIcon icon={this._playing ? "pause" : "play"} />
-            </div>
-
-            {this.timeline && <div className="timecode-controls">
-                <div className="timecode-current">
-                    {formatTime(curTime)}
-                </div>
-
-                {this._fullScreen || this.heightPercent === 100 ?
-                    <div className="timeline-slider">
-                        <input type="range" step="0.1" min={this.timeline.clipStart} max={this.timeline.clipEnd} value={curTime}
-                            className="toolbar-slider time-progress"
-                            onPointerDown={(e: React.PointerEvent) => { e.stopPropagation(); }}
-                            onChange={(e: React.ChangeEvent<HTMLInputElement>) => this.setPlayheadTime(Number(e.target.value))}
-                        />
-                    </div>
-                    :
-                    <div>/</div>}
-
-                <div className="timecode-end">
-                    {formatTime(this.timeline.clipDuration)}
-                </div>
-            </div>}
-
-            <div className="videobox-button"
-                title={"full screen"}
-                onPointerDown={this.onFullDown}>
-                <FontAwesomeIcon icon="expand" />
-            </div>
-
-            {!this._fullScreen && <div className="videobox-button"
-                title={"show timeline"}
-                onPointerDown={this.onTimelineHdlDown}>
-                <FontAwesomeIcon icon="eye" />
-            </div>}
-
-            {!this._fullScreen && <div className="videobox-button"
-                title={this.timeline?.IsTrimming !== TrimScope.None ? "finish trimming" : "start trim"}
-                onPointerDown={this.onClipPointerDown}>
-                <FontAwesomeIcon icon={this.timeline?.IsTrimming !== TrimScope.None ? "check" : "cut"} />
-            </div>}
-
-            <div className="videobox-button show-slider"
-                title={this._muted ? "unmute" : "mute"}
-                onPointerDown={(e) => { e.stopPropagation(); this.toggleMute(); }}>
-                <FontAwesomeIcon icon={this._muted ? "volume-mute" : "volume-up"} />
-            </div>
-            <input type="range" step="0.1" min="0" max="1" value={this._muted ? 0 : this._volume}
-                className="toolbar-slider volume"
-                onPointerDown={(e: React.PointerEvent) => e.stopPropagation()}
-                onChange={(e: React.ChangeEvent<HTMLInputElement>) => this.setVolume(Number(e.target.value))}
-            />
-
-            {!this._fullScreen && this.heightPercent !== 100 &&
-                <>
-                    <div className="videobox-button" title="zoom">
-                        <FontAwesomeIcon icon="search-plus" />
-                    </div>
-                    <input type="range" step="0.1" min="1" max="5" value={this.timeline?._zoomFactor}
-                        className="toolbar-slider zoom"
-                        onPointerDown={(e: React.PointerEvent) => { e.stopPropagation(); }}
-                        onChange={(e: React.ChangeEvent<HTMLInputElement>) => { this.zoom(Number(e.target.value)); }}
-                    />
-                </>}
-        </div>;
-    }
-
-    // renders CollectionStackedTimeline
-    @computed get renderTimeline() {
-        return <div className="videoBox-stackPanel" style={{ transition: this.transition, height: `${100 - this.heightPercent}%` }}>
-            <CollectionStackedTimeline ref={action((r: any) => this._stackedTimeline = r)} {...this.props}
-                fieldKey={this.annotationKey}
-                dictationKey={this.fieldKey + "-dictation"}
-                mediaPath={this.audiopath}
-                renderDepth={this.props.renderDepth + 1}
-                startTag={"_timecodeToShow" /* videoStart */}
-                endTag={"_timecodeToHide" /* videoEnd */}
-                bringToFront={emptyFunction}
-                CollectionView={undefined}
-                playFrom={this.playFrom}
-                setTime={this.setPlayheadTime}
-                playing={this.playing}
-                isAnyChildContentActive={this.isAnyChildContentActive}
-                whenChildContentsActiveChanged={this.timelineWhenChildContentsActiveChanged}
-                moveDocument={this.moveDocument}
-                addDocument={this.addDocument}
-                removeDocument={this.removeDocument}
-                ScreenToLocalTransform={this.timelineScreenToLocal}
-                Play={this.Play}
-                Pause={this.Pause}
-                playLink={this.playLink}
-                PanelHeight={this.timelineHeight}
-                rawDuration={this.rawDuration}
-            />
-        </div>;
-    }
-
-    // renders annotation layer
-    @computed get annotationLayer() {
-        return <div className="videoBox-annotationLayer" style={{ transition: this.transition, height: `${this.heightPercent}%` }} ref={this._annotationLayer} />;
-    }
-
-    render() {
-        const borderRad = this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BorderRounding);
-        const borderRadius = borderRad?.includes("px") ? `${Number(borderRad.split("px")[0]) / this.scaling()}px` : borderRad;
-        return (<div className="videoBox" onContextMenu={this.specificContextMenu} ref={this._mainCont}
-            style={{
-                pointerEvents: this.props.layerProvider?.(this.layoutDoc) === false ? "none" : undefined,
-                borderRadius,
-                overflow: this.props.docViewPath?.().slice(-1)[0].fitWidth ? "auto" : undefined
-            }} onWheel={e => { e.stopPropagation(); e.preventDefault(); }}>
-            <div className="videoBox-viewer" onPointerDown={this.marqueeDown} >
-                <div style={{
-                    position: "absolute", transition: this.transition,
-                    width: this.panelWidth(),
-                    height: this.panelHeight(),
-                    top: 0,
-                    left: (this.props.PanelWidth() - this.panelWidth()) / 2
-                }}>
-                    <CollectionFreeFormView {...OmitKeys(this.props, ["NativeWidth", "NativeHeight", "setContentView"]).omit}
-                        renderDepth={this.props.renderDepth + 1}
+
+      // stretches vertically or horizontally depending on video orientation so video fits full screen
+      fullScreenSize() {
+            if (this._videoRef && this._videoRef.videoHeight / this._videoRef.videoWidth > 1) {
+                  return { height: "100%" };
+            }
+            else {
+                  return { width: "100%" };
+            }
+      }
+
+
+      // for zoom slider, sets timeline waveform zoom
+      zoom = (zoom: number) => {
+            this.timeline?.setZoom(zoom);
+      }
+
+
+      // plays link
+      playLink = (doc: Doc) => {
+            const startTime = Math.max(0, (this._stackedTimeline?.anchorStart(doc) || 0));
+            const endTime = this.timeline?.anchorEnd(doc);
+            if (startTime !== undefined) {
+                  if (!this.layoutDoc.dontAutoPlayFollowedLinks) endTime ? this.playFrom(startTime, endTime) : this.playFrom(startTime);
+                  else this.Seek(startTime);
+            }
+      }
+
+
+      // starts marquee selection
+      marqueeDown = (e: React.PointerEvent) => {
+            if (!e.altKey && e.button === 0 && this.layoutDoc._viewScale === 1 && this.props.isContentActive(true) && ![InkTool.Highlighter, InkTool.Pen].includes(CurrentUserUtils.SelectedTool)) {
+                  setupMoveUpEvents(this, e, action(e => {
+                        MarqueeAnnotator.clearAnnotations(this._savedAnnotations);
+                        this._marqueeing = [e.clientX, e.clientY];
+                        return true;
+                  }), returnFalse, () => MarqueeAnnotator.clearAnnotations(this._savedAnnotations), false);
+            }
+      }
+
+      // ends marquee selection
+      @action
+      finishMarquee = () => {
+            this._marqueeing = undefined;
+            this.props.select(true);
+      }
+
+      timelineWhenChildContentsActiveChanged = action((isActive: boolean) => this.props.whenChildContentsActiveChanged(this._isAnyChildContentActive = isActive));
+
+      timelineScreenToLocal = () => this.props.ScreenToLocalTransform().scale(this.scaling()).translate(0, -this.heightPercent / 100 * this.props.PanelHeight());
+
+      setPlayheadTime = (time: number) => this.player!.currentTime = this.layoutDoc._currentTimecode = time;
+
+      timelineHeight = () => this.props.PanelHeight() * (100 - this.heightPercent) / 100;
+
+      playing = () => this._playing;
+
+      contentFunc = () => [this.youtubeVideoId ? this.youtubeContent : this.content];
+
+      scaling = () => this.props.scaling?.() || 1;
+
+      panelWidth = () => this.props.PanelWidth() * this.heightPercent / 100;
+      panelHeight = () => this.layoutDoc._fitWidth ? this.panelWidth() / (Doc.NativeAspect(this.rootDoc) || 1) : this.props.PanelHeight() * this.heightPercent / 100;
+
+      screenToLocalTransform = () => {
+            const offset = (this.props.PanelWidth() - this.panelWidth()) / 2 / this.scaling();
+            return this.props.ScreenToLocalTransform().translate(-offset, 0).scale(100 / this.heightPercent);
+      }
+
+      marqueeFitScaling = () => (this.props.scaling?.() || 1) * this.heightPercent / 100;
+      marqueeOffset = () => [this.panelWidth() / 2 * (1 - this.heightPercent / 100) / (this.heightPercent / 100), 0];
+
+      timelineDocFilter = () => [`_timelineLabel:true,${Utils.noRecursionHack}:x`];
+
+
+      // renders video controls
+      @computed get uIButtons() {
+            const curTime = NumCast(this.layoutDoc._currentTimecode) - (this.timeline?.clipStart || 0);
+            return <div className="videoBox-ui" style={this._fullScreen || this.heightPercent === 100 ? { fontSize: "40px", minWidth: "80%" } : {}}>
+                  <div className="videobox-button"
+                        title={this._playing ? "play" : "pause"}
+                        onPointerDown={this.onPlayDown}>
+                        <FontAwesomeIcon icon={this._playing ? "pause" : "play"} />
+                  </div>
+
+                  {this.timeline && <div className="timecode-controls">
+                        <div className="timecode-current">
+                              {formatTime(curTime)}
+                        </div>
+
+                        {this._fullScreen || this.heightPercent === 100 ?
+                              <div className="timeline-slider">
+                                    <input type="range" step="0.1" min={this.timeline.clipStart} max={this.timeline.clipEnd} value={curTime}
+                                          className="toolbar-slider time-progress"
+                                          onPointerDown={(e: React.PointerEvent) => { e.stopPropagation(); }}
+                                          onChange={(e: React.ChangeEvent<HTMLInputElement>) => this.setPlayheadTime(Number(e.target.value))}
+                                    />
+                              </div>
+                              :
+                              <div>/</div>}
+
+                        <div className="timecode-end">
+                              {formatTime(this.timeline.clipDuration)}
+                        </div>
+                  </div>}
+
+                  <div className="videobox-button"
+                        title={"full screen"}
+                        onPointerDown={this.onFullDown}>
+                        <FontAwesomeIcon icon="expand" />
+                  </div>
+
+                  {!this._fullScreen && <div className="videobox-button"
+                        title={"show timeline"}
+                        onPointerDown={this.onTimelineHdlDown}>
+                        <FontAwesomeIcon icon="eye" />
+                  </div>}
+
+                  {!this._fullScreen && <div className="videobox-button"
+                        title={this.timeline?.IsTrimming !== TrimScope.None ? "finish trimming" : "start trim"}
+                        onPointerDown={this.onClipPointerDown}>
+                        <FontAwesomeIcon icon={this.timeline?.IsTrimming !== TrimScope.None ? "check" : "cut"} />
+                  </div>}
+
+                  <div className="videobox-button show-slider"
+                        title={this._muted ? "unmute" : "mute"}
+                        onPointerDown={(e) => { e.stopPropagation(); this.toggleMute(); }}>
+                        <FontAwesomeIcon icon={this._muted ? "volume-mute" : "volume-up"} />
+                  </div>
+                  <input type="range" step="0.1" min="0" max="1" value={this._muted ? 0 : this._volume}
+                        className="toolbar-slider volume"
+                        onPointerDown={(e: React.PointerEvent) => e.stopPropagation()}
+                        onChange={(e: React.ChangeEvent<HTMLInputElement>) => this.setVolume(Number(e.target.value))}
+                  />
+
+                  {!this._fullScreen && this.heightPercent !== 100 &&
+                        <>
+                              <div className="videobox-button" title="zoom">
+                                    <FontAwesomeIcon icon="search-plus" />
+                              </div>
+                              <input type="range" step="0.1" min="1" max="5" value={this.timeline?._zoomFactor}
+                                    className="toolbar-slider zoom"
+                                    onPointerDown={(e: React.PointerEvent) => { e.stopPropagation(); }}
+                                    onChange={(e: React.ChangeEvent<HTMLInputElement>) => { this.zoom(Number(e.target.value)); }}
+                              />
+                        </>}
+            </div>;
+      }
+
+      // renders CollectionStackedTimeline
+      @computed get renderTimeline() {
+            return <div className="videoBox-stackPanel" style={{ transition: this.transition, height: `${100 - this.heightPercent}%` }}>
+                  <CollectionStackedTimeline ref={action((r: any) => this._stackedTimeline = r)} {...this.props}
                         fieldKey={this.annotationKey}
+                        dictationKey={this.fieldKey + "-dictation"}
+                        mediaPath={this.audiopath}
+                        renderDepth={this.props.renderDepth + 1}
+                        startTag={"_timecodeToShow" /* videoStart */}
+                        endTag={"_timecodeToHide" /* videoEnd */}
+                        bringToFront={emptyFunction}
                         CollectionView={undefined}
-                        isAnnotationOverlay={true}
-                        annotationLayerHostsContent={true}
-                        PanelWidth={this.panelWidth}
-                        PanelHeight={this.panelHeight}
-                        ScreenToLocalTransform={this.screenToLocalTransform}
-                        docFilters={this.timelineDocFilter}
-                        select={emptyFunction}
-                        scaling={returnOne}
-                        whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
-                        removeDocument={this.removeDocument}
+                        playFrom={this.playFrom}
+                        setTime={this.setPlayheadTime}
+                        playing={this.playing}
+                        isAnyChildContentActive={this.isAnyChildContentActive}
+                        whenChildContentsActiveChanged={this.timelineWhenChildContentsActiveChanged}
                         moveDocument={this.moveDocument}
-                        addDocument={this.addDocWithTimecode}>
-                        {this.contentFunc}
-                    </CollectionFreeFormView>
-                </div>
-                {this.annotationLayer}
-                {!this._marqueeing || !this._mainCont.current || !this._annotationLayer.current ? (null) :
-                    <MarqueeAnnotator
-                        rootDoc={this.rootDoc}
-                        scrollTop={0}
-                        down={this._marqueeing}
-                        scaling={this.marqueeFitScaling}
-                        docView={this.props.docViewPath().slice(-1)[0]}
-                        containerOffset={this.marqueeOffset}
-                        addDocument={this.addDocWithTimecode}
-                        finishMarquee={this.finishMarquee}
-                        savedAnnotations={this._savedAnnotations}
-                        annotationLayer={this._annotationLayer.current}
-                        mainCont={this._mainCont.current}
-                    />}
-                {this.renderTimeline}
-            </div>
-        </div >);
-    }
+                        addDocument={this.addDocument}
+                        removeDocument={this.removeDocument}
+                        ScreenToLocalTransform={this.timelineScreenToLocal}
+                        Play={this.Play}
+                        Pause={this.Pause}
+                        playLink={this.playLink}
+                        PanelHeight={this.timelineHeight}
+                        rawDuration={this.rawDuration}
+                  />
+            </div>;
+      }
+
+      // renders annotation layer
+      @computed get annotationLayer() {
+            return <div className="videoBox-annotationLayer" style={{ transition: this.transition, height: `${this.heightPercent}%` }} ref={this._annotationLayer} />;
+      }
+
+      render() {
+            const borderRad = this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BorderRounding);
+            const borderRadius = borderRad?.includes("px") ? `${Number(borderRad.split("px")[0]) / this.scaling()}px` : borderRad;
+            return (<div className="videoBox" onContextMenu={this.specificContextMenu} ref={this._mainCont}
+                  style={{
+                        pointerEvents: this.props.layerProvider?.(this.layoutDoc) === false ? "none" : undefined,
+                        borderRadius,
+                        overflow: this.props.docViewPath?.().slice(-1)[0].fitWidth ? "auto" : undefined
+                  }} onWheel={e => { e.stopPropagation(); e.preventDefault(); }}>
+                  <div className="videoBox-viewer" onPointerDown={this.marqueeDown} >
+                        <div style={{
+                              position: "absolute", transition: this.transition,
+                              width: this.panelWidth(),
+                              height: this.panelHeight(),
+                              top: 0,
+                              left: (this.props.PanelWidth() - this.panelWidth()) / 2
+                        }}>
+                              <CollectionFreeFormView {...OmitKeys(this.props, ["NativeWidth", "NativeHeight", "setContentView"]).omit}
+                                    renderDepth={this.props.renderDepth + 1}
+                                    fieldKey={this.annotationKey}
+                                    CollectionView={undefined}
+                                    isAnnotationOverlay={true}
+                                    annotationLayerHostsContent={true}
+                                    PanelWidth={this.panelWidth}
+                                    PanelHeight={this.panelHeight}
+                                    ScreenToLocalTransform={this.screenToLocalTransform}
+                                    docFilters={this.timelineDocFilter}
+                                    select={emptyFunction}
+                                    scaling={returnOne}
+                                    whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
+                                    removeDocument={this.removeDocument}
+                                    moveDocument={this.moveDocument}
+                                    addDocument={this.addDocWithTimecode}>
+                                    {this.contentFunc}
+                              </CollectionFreeFormView>
+                        </div>
+                        {this.annotationLayer}
+                        {!this._marqueeing || !this._mainCont.current || !this._annotationLayer.current ? (null) :
+                              <MarqueeAnnotator
+                                    rootDoc={this.rootDoc}
+                                    scrollTop={0}
+                                    down={this._marqueeing}
+                                    scaling={this.marqueeFitScaling}
+                                    docView={this.props.docViewPath().slice(-1)[0]}
+                                    containerOffset={this.marqueeOffset}
+                                    addDocument={this.addDocWithTimecode}
+                                    finishMarquee={this.finishMarquee}
+                                    savedAnnotations={this._savedAnnotations}
+                                    annotationLayer={this._annotationLayer.current}
+                                    mainCont={this._mainCont.current}
+                              />}
+                        {this.renderTimeline}
+                  </div>
+            </div >);
+      }
 }
 
 VideoBox._nativeControls = false;
\ No newline at end of file
-- 
cgit v1.2.3-70-g09d2


From 19f0eca0f5f1b0d12ac369f149caf787f88dd9d6 Mon Sep 17 00:00:00 2001
From: Jenny Yu <jennyyu212@outlook.com>
Date: Thu, 12 May 2022 01:39:26 -0400
Subject: feat: added checkbox for optional screen tracking

---
 .../views/nodes/RecordingBox/RecordingBox.tsx      | 11 ++-
 .../views/nodes/RecordingBox/RecordingView.scss    | 30 +++++---
 .../views/nodes/RecordingBox/RecordingView.tsx     | 83 +++++++---------------
 src/client/views/nodes/VideoBox.tsx                |  5 +-
 4 files changed, 54 insertions(+), 75 deletions(-)

(limited to 'src/client/views/nodes/RecordingBox/RecordingBox.tsx')

diff --git a/src/client/views/nodes/RecordingBox/RecordingBox.tsx b/src/client/views/nodes/RecordingBox/RecordingBox.tsx
index 807624394..e5b71c183 100644
--- a/src/client/views/nodes/RecordingBox/RecordingBox.tsx
+++ b/src/client/views/nodes/RecordingBox/RecordingBox.tsx
@@ -38,26 +38,23 @@ export class RecordingBox extends ViewBoxBaseComponent() {
       }
 
       @action
-      setResult = (info: Upload.FileInformation) => {
-            // console.log("Setting result to " + info)
+      setResult = (info: Upload.FileInformation, trackScreen: boolean) => {
             this.result = info
-            // console.log(this.result.accessPaths.agnostic.client)
             this.dataDoc.type = DocumentType.VID;
-            // console.log(this.videoDuration)
             this.dataDoc[this.fieldKey + "-duration"] = this.videoDuration;
 
             this.dataDoc.layout = VideoBox.LayoutString(this.fieldKey);
             this.dataDoc[this.props.fieldKey] = new VideoField(this.result.accessPaths.agnostic.client);
             this.dataDoc[this.fieldKey + "-recorded"] = true;
             // stringify the presenation and store it
-            this.dataDoc[this.fieldKey + "-presentation"] = JSON.stringify(RecordingApi.Instance.clear());
+            if (trackScreen) {
+                  this.dataDoc[this.fieldKey + "-presentation"] = JSON.stringify(RecordingApi.Instance.clear());
+            }
       }
 
       render() {
             return <div className="recordingBox" ref={this._ref}>
                   {!this.result && <RecordingView setResult={this.setResult} setDuration={this.setVideoDuration} />}
-                  {/* {!this.result ? <RecordingView setResult={this.setResult} /> :
-                        <p>video box</p>} */}
             </div>;
       }
 }
diff --git a/src/client/views/nodes/RecordingBox/RecordingView.scss b/src/client/views/nodes/RecordingBox/RecordingView.scss
index c55af5952..9b2f6d070 100644
--- a/src/client/views/nodes/RecordingBox/RecordingView.scss
+++ b/src/client/views/nodes/RecordingBox/RecordingView.scss
@@ -1,5 +1,5 @@
 video {
-    // flex: 100%;
+      // flex: 100%;
       width: 100%;
       // min-height: 400px;
       //height: auto;
@@ -22,12 +22,12 @@ button {
 }
 
 .video-wrapper {
-    //   max-width: 600px;
-    //   max-width: 700px;
+      //   max-width: 600px;
+      //   max-width: 700px;
       position: relative;
       display: flex;
       justify-content: center;
-    //   overflow: hidden;
+      //   overflow: hidden;
       border-radius: 10px;
       margin: 0;
 }
@@ -176,7 +176,7 @@ button {
 
 }
 
-.video-edit-wrapper {
+.options-wrapper {
       height: 100%;
       display: flex;
       flex-direction: row;
@@ -184,10 +184,24 @@ button {
       position: absolute;
       top: 0;
       bottom: 0;
-      right: 50% - 15;
 
-      .video-edit-buttons {
-            margin: 0 5px;
+      &.video-edit-wrapper {
+
+            right: 50% - 15;
+
+            .track-screen {
+                  font-weight: 200;
+            }
+
       }
 
+      &.track-screen-wrapper {
+
+            right: 50% - 30;
+
+            .track-screen {
+                  font-weight: 200;
+            }
+
+      }
 }
\ No newline at end of file
diff --git a/src/client/views/nodes/RecordingBox/RecordingView.tsx b/src/client/views/nodes/RecordingBox/RecordingView.tsx
index efacf7735..e9450a5dd 100644
--- a/src/client/views/nodes/RecordingBox/RecordingView.tsx
+++ b/src/client/views/nodes/RecordingBox/RecordingView.tsx
@@ -10,20 +10,13 @@ import { Upload } from '../../../../server/SharedMediaTypes';
 
 import { RecordingApi } from '../../../util/RecordingApi';
 
-
-enum RecordingStatus {
-      Recording,
-      Stopped,
-      Paused
-}
-
 interface MediaSegment {
       videoChunks: any[],
       endTime: number
 }
 
 interface IRecordingViewProps {
-      setResult: (info: Upload.FileInformation) => void
+      setResult: (info: Upload.FileInformation, trackScreen: boolean) => void
       setDuration: (seconds: number) => void
 }
 
@@ -36,27 +29,21 @@ export function RecordingView(props: IRecordingViewProps) {
       const [recordingTimer, setRecordingTimer] = useState(0); // unit is 0.01 second
       const [playing, setPlaying] = useState(false);
       const [progress, setProgress] = useState(0);
-      const [speed, setSpeed] = useState(1);
-      const [muted, setMuted] = useState(false);
 
       const [videos, setVideos] = useState<MediaSegment[]>([]);
-      const [currentVid, setCurrentVid] = useState<number>(0);
       const videoRecorder = useRef<MediaRecorder | null>(null);
-      const audioRecorder = useRef<MediaRecorder | null>(null);
       const videoElementRef = useRef<HTMLVideoElement | null>(null);
 
-    const [finished, setFinished] = useState<Boolean>(false)
+      const [finished, setFinished] = useState<boolean>(false)
+      const [trackScreen, setTrackScreen] = useState<boolean>(true)
 
 
 
       const DEFAULT_MEDIA_CONSTRAINTS = {
-            // video: true,
-            // audio: true
             video: {
                   width: 1280,
                   height: 720,
             },
-            // audio: true,
             audio: {
                   echoCancellation: true,
                   noiseSuppression: true,
@@ -80,7 +67,7 @@ export function RecordingView(props: IRecordingViewProps) {
                         .then((data) => {
                               const result = data[0].result
                               if (!(result instanceof Error)) { // convert this screenshotBox into normal videoBox
-                                    props.setResult(result)
+                                    props.setResult(result, trackScreen)
                               } else {
                                     alert("video conversion failed");
                               }
@@ -138,11 +125,9 @@ export function RecordingView(props: IRecordingViewProps) {
       const record = async () => {
             const stream = await startShowingStream();
             videoRecorder.current = new MediaRecorder(stream)
-            // audioRecorder.current = new MediaRecorder(await navigator.mediaDevices.getUserMedia({ audio: true }));
 
             // temporary chunks of video
             let videoChunks: any = []
-            // let audioChunks: any = []
 
             videoRecorder.current.ondataavailable = (event: any) => {
                   if (event.data.size > 0) {
@@ -150,16 +135,9 @@ export function RecordingView(props: IRecordingViewProps) {
                   }
             }
 
-            // audioRecorder.current.ondataavailable = (event: any) => {
-            //       if (event.data.size > 0) {
-            //             audioChunks.push(event.data)
-            //       }
-            // }
-
             videoRecorder.current.onstart = (event: any) => {
-                setRecording(true);
-                // RecordingApi.Instance.clear();
-                RecordingApi.Instance.start();
+                  setRecording(true);
+                  trackScreen && RecordingApi.Instance.start();
             }
 
             videoRecorder.current.onstop = () => {
@@ -172,8 +150,8 @@ export function RecordingView(props: IRecordingViewProps) {
                   // reset the temporary chunks
                   videoChunks = []
                   setRecording(false);
-                setFinished(true);
-                RecordingApi.Instance.pause();
+                  setFinished(true);
+                  trackScreen && RecordingApi.Instance.pause();
             }
 
             // recording paused
@@ -183,14 +161,14 @@ export function RecordingView(props: IRecordingViewProps) {
 
                   // reset the temporary chunks
                   videoChunks = []
-                setRecording(false);
-                RecordingApi.Instance.pause();
+                  setRecording(false);
+                  trackScreen && RecordingApi.Instance.pause();
             }
 
             videoRecorder.current.onresume = async (event: any) => {
                   await startShowingStream();
-                setRecording(true);
-                RecordingApi.Instance.resume();
+                  setRecording(true);
+                  trackScreen && RecordingApi.Instance.resume();
             }
 
             videoRecorder.current.start(200)
@@ -229,18 +207,6 @@ export function RecordingView(props: IRecordingViewProps) {
             setVideos(videos.filter((_, idx) => idx !== numVideos - 1));
       }
 
-      // const handleVideoProgress = (event: any) => {
-      //       const manualChange = Number(event.target.value);
-      //       videoElement!.currentTime = (videoElement!.duration / 100) * manualChange;
-      //       setProgress(manualChange)
-      // };
-
-      // const handleVideoSpeed = (event: any) => {
-      //       const newSpeed = Number(event.target.value);
-      //       videoElement!.playbackRate = speed;
-      //       setSpeed(newSpeed)
-      // };
-
       const handleOnTimeUpdate = () => {
             if (playing) {
                   setVideoProgressHelper(videoElementRef.current!.currentTime)
@@ -256,7 +222,7 @@ export function RecordingView(props: IRecordingViewProps) {
             return toTwoDigit(minutes) + " : " + toTwoDigit(seconds);
       }
 
-    return (
+      return (
             <div className="recording-container">
                   <div className="video-wrapper">
                         <video id="video"
@@ -277,9 +243,10 @@ export function RecordingView(props: IRecordingViewProps) {
                                                 <button className="record-button" onClick={startOrResume} />
                                           }
                                     </div>
-                                    {!recording && videos.length > 0 ?
 
-                                          <div className="video-edit-wrapper">
+                                    {!recording && (videos.length > 0 ?
+
+                                          <div className="options-wrapper video-edit-wrapper">
                                                 {/* <IconContext.Provider value={{ color: "grey", className: "video-edit-buttons" }}>
                                                       <MdBackspace onClick={clearPrevious} />
                                                 </IconContext.Provider> */}
@@ -288,7 +255,13 @@ export function RecordingView(props: IRecordingViewProps) {
                                                 </IconContext.Provider>
                                           </div>
 
-                                          : <></>}
+                                          : <div className="options-wrapper track-screen-wrapper">
+                                                <label className="track-screen">
+                                                      <input type="checkbox" checked={trackScreen} onChange={(e) => { setTrackScreen(e.target.checked) }} />
+                                                      <span className="checkmark"></span>
+                                                      Track Screen
+                                                </label>
+                                          </div>)}
 
                               </div>
 
@@ -297,15 +270,7 @@ export function RecordingView(props: IRecordingViewProps) {
                                     marks={videos.map((elt) => elt.endTime / MAXTIME * 100)}
                               // playSegment={playSegment}
                               />
-
-                              {/* <button className="mute-btn" onClick={() => setMuted(!muted)}>
-                                          {!muted ? (
-                                                <i className="bx bxs-volume-full"></i>
-                                          ) : (
-                                                <i className="bx bxs-volume-mute"></i>
-                                          )}
-                                    </button>  */}
-                  </div>
+                        </div>
                   </div>
             </div>)
 }
\ No newline at end of file
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index 58e9f390c..902f19716 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -163,8 +163,11 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
 
             // if presentation isn't null, call followmovements on the recording api
             if (this.presentation) {
+                  console.log("presentation isn't null")
                   const err = RecordingApi.Instance.playMovements(this.presentation, this.player?.currentTime || 0, this);
                   err && console.log(err)
+            } else {
+                  console.log("presentation is null")
             }
 
             this._playing = true;
@@ -354,7 +357,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
       // sets video element ref
       @action
       setVideoRef = (vref: HTMLVideoElement | null) => {
-            console.log("SEt video ref " + vref)
+            console.log("Set video ref " + vref)
             this._videoRef = vref;
             if (vref) {
                   this._videoRef!.ontimeupdate = this.updateTimecode;
-- 
cgit v1.2.3-70-g09d2


From 50a16adbcd253ac943d37110e0e3a2a55439984d Mon Sep 17 00:00:00 2001
From: Jenny Yu <jennyyu212@outlook.com>
Date: Thu, 12 May 2022 14:10:13 -0400
Subject: fix: gives recording box unique id to allow multiple streams at the
 same time'

---
 .../views/nodes/RecordingBox/RecordingBox.tsx      |    6 +-
 .../views/nodes/RecordingBox/RecordingView.tsx     |    5 +-
 src/fields/Doc.ts                                  | 2552 ++++++++++----------
 3 files changed, 1283 insertions(+), 1280 deletions(-)

(limited to 'src/client/views/nodes/RecordingBox/RecordingBox.tsx')

diff --git a/src/client/views/nodes/RecordingBox/RecordingBox.tsx b/src/client/views/nodes/RecordingBox/RecordingBox.tsx
index e5b71c183..159271223 100644
--- a/src/client/views/nodes/RecordingBox/RecordingBox.tsx
+++ b/src/client/views/nodes/RecordingBox/RecordingBox.tsx
@@ -9,7 +9,8 @@ import { VideoBox } from "../VideoBox";
 import { RecordingView } from './RecordingView';
 import { DocumentType } from "../../../documents/DocumentTypes";
 import { RecordingApi } from "../../../util/RecordingApi";
-import { Doc } from "../../../../fields/Doc";
+import { Doc, FieldsSym } from "../../../../fields/Doc";
+import { Id } from "../../../../fields/FieldSymbols";
 
 
 @observer
@@ -53,8 +54,9 @@ export class RecordingBox extends ViewBoxBaseComponent() {
       }
 
       render() {
+            // console.log("Proto[Is]: ", this.rootDoc.proto?.[Id])
             return <div className="recordingBox" ref={this._ref}>
-                  {!this.result && <RecordingView setResult={this.setResult} setDuration={this.setVideoDuration} />}
+                  {!this.result && <RecordingView setResult={this.setResult} setDuration={this.setVideoDuration} id={this.rootDoc.proto?.[Id]} />}
             </div>;
       }
 }
diff --git a/src/client/views/nodes/RecordingBox/RecordingView.tsx b/src/client/views/nodes/RecordingBox/RecordingView.tsx
index e9450a5dd..87716e9cc 100644
--- a/src/client/views/nodes/RecordingBox/RecordingView.tsx
+++ b/src/client/views/nodes/RecordingBox/RecordingView.tsx
@@ -18,6 +18,7 @@ interface MediaSegment {
 interface IRecordingViewProps {
       setResult: (info: Upload.FileInformation, trackScreen: boolean) => void
       setDuration: (seconds: number) => void
+      id: string
 }
 
 const MAXTIME = 100000;
@@ -88,7 +89,7 @@ export function RecordingView(props: IRecordingViewProps) {
 
       useEffect(() => {
             // get access to the video element on every render
-            videoElementRef.current = document.getElementById('video') as HTMLVideoElement;
+            videoElementRef.current = document.getElementById(`video-${props.id}`) as HTMLVideoElement;
       })
 
       useEffect(() => {
@@ -225,7 +226,7 @@ export function RecordingView(props: IRecordingViewProps) {
       return (
             <div className="recording-container">
                   <div className="video-wrapper">
-                        <video id="video"
+                        <video id={`video-${props.id}`}
                               autoPlay
                               muted
                               onTimeUpdate={handleOnTimeUpdate}
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index 50e5fcbc4..b07b3a7aa 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -27,37 +27,37 @@ import { deleteProperty, GetEffectiveAcl, getField, getter, inheritParentAcls, m
 import JSZip = require("jszip");
 
 export namespace Field {
-    export function toKeyValueString(doc: Doc, key: string): string {
-        const onDelegate = Object.keys(doc).includes(key);
-        const field = ComputedField.WithoutComputed(() => FieldValue(doc[key]));
-        return !Field.IsField(field) ? "" : (onDelegate ? "=" : "") + (field instanceof ComputedField ? `:=${field.script.originalScript}` : Field.toScriptString(field));
-    }
-    export function toScriptString(field: Field): string {
-        if (typeof field === "string") return `"${field}"`;
-        if (typeof field === "number" || typeof field === "boolean") return String(field);
-        if (field === undefined || field === null) return "null";
-        return field[ToScriptString]();
-    }
-    export function toString(field: Field): string {
-        if (typeof field === "string") return field;
-        if (typeof field === "number" || typeof field === "boolean") return String(field);
-        if (field instanceof ObjectField) return field[ToString]();
-        if (field instanceof RefField) return field[ToString]();
-        return "";
-    }
-    export function IsField(field: any): field is Field;
-    export function IsField(field: any, includeUndefined: true): field is Field | undefined;
-    export function IsField(field: any, includeUndefined: boolean = false): field is Field | undefined {
-        return (typeof field === "string")
-            || (typeof field === "number")
-            || (typeof field === "boolean")
-            || (field instanceof ObjectField)
-            || (field instanceof RefField)
-            || (includeUndefined && field === undefined);
-    }
-    export function Copy(field: any) {
-        return field instanceof ObjectField ? ObjectField.MakeCopy(field) : field;
-    }
+      export function toKeyValueString(doc: Doc, key: string): string {
+            const onDelegate = Object.keys(doc).includes(key);
+            const field = ComputedField.WithoutComputed(() => FieldValue(doc[key]));
+            return !Field.IsField(field) ? "" : (onDelegate ? "=" : "") + (field instanceof ComputedField ? `:=${field.script.originalScript}` : Field.toScriptString(field));
+      }
+      export function toScriptString(field: Field): string {
+            if (typeof field === "string") return `"${field}"`;
+            if (typeof field === "number" || typeof field === "boolean") return String(field);
+            if (field === undefined || field === null) return "null";
+            return field[ToScriptString]();
+      }
+      export function toString(field: Field): string {
+            if (typeof field === "string") return field;
+            if (typeof field === "number" || typeof field === "boolean") return String(field);
+            if (field instanceof ObjectField) return field[ToString]();
+            if (field instanceof RefField) return field[ToString]();
+            return "";
+      }
+      export function IsField(field: any): field is Field;
+      export function IsField(field: any, includeUndefined: true): field is Field | undefined;
+      export function IsField(field: any, includeUndefined: boolean = false): field is Field | undefined {
+            return (typeof field === "string")
+                  || (typeof field === "number")
+                  || (typeof field === "boolean")
+                  || (field instanceof ObjectField)
+                  || (field instanceof RefField)
+                  || (includeUndefined && field === undefined);
+      }
+      export function Copy(field: any) {
+            return field instanceof ObjectField ? ObjectField.MakeCopy(field) : field;
+      }
 }
 export type Field = number | string | boolean | ObjectField | RefField;
 export type Opt<T> = T | undefined;
@@ -73,8 +73,8 @@ export type FieldResult<T extends Field = Field> = Opt<T> | FieldWaiting<Extract
 export function DocListCastAsync(field: FieldResult): Promise<Doc[] | undefined>;
 export function DocListCastAsync(field: FieldResult, defaultValue: Doc[]): Promise<Doc[]>;
 export function DocListCastAsync(field: FieldResult, defaultValue?: Doc[]) {
-    const list = Cast(field, listSpec(Doc));
-    return list ? Promise.all(list).then(() => list) : Promise.resolve(defaultValue);
+      const list = Cast(field, listSpec(Doc));
+      return list ? Promise.all(list).then(() => list) : Promise.resolve(defaultValue);
 }
 
 export async function DocCastAsync(field: FieldResult): Promise<Opt<Doc>> { return Cast(field, Doc); }
@@ -104,1304 +104,1304 @@ export const ForceServerWrite = Symbol("ForceServerWrite");
 export const CachedUpdates = Symbol("Cached updates");
 
 const AclMap = new Map<string, symbol>([
-    ["None", AclUnset],
-    [SharingPermissions.None, AclPrivate],
-    [SharingPermissions.View, AclReadonly],
-    [SharingPermissions.Augment, AclAugment],
-    [SharingPermissions.SelfEdit, AclSelfEdit],
-    [SharingPermissions.Edit, AclEdit],
-    [SharingPermissions.Admin, AclAdmin]
+      ["None", AclUnset],
+      [SharingPermissions.None, AclPrivate],
+      [SharingPermissions.View, AclReadonly],
+      [SharingPermissions.Augment, AclAugment],
+      [SharingPermissions.SelfEdit, AclSelfEdit],
+      [SharingPermissions.Edit, AclEdit],
+      [SharingPermissions.Admin, AclAdmin]
 ]);
 
 // caches the document access permissions for the current user.
 // this recursively updates all protos as well.
 export function updateCachedAcls(doc: Doc) {
-    if (!doc) return;
-    const permissions: { [key: string]: symbol } = {};
+      if (!doc) return;
+      const permissions: { [key: string]: symbol } = {};
 
-    doc[UpdatingFromServer] = true;
-    Object.keys(doc).filter(key => key.startsWith("acl") && (permissions[key] = AclMap.get(StrCast(doc[key]))!));
-    doc[UpdatingFromServer] = false;
+      doc[UpdatingFromServer] = true;
+      Object.keys(doc).filter(key => key.startsWith("acl") && (permissions[key] = AclMap.get(StrCast(doc[key]))!));
+      doc[UpdatingFromServer] = false;
 
-    if (Object.keys(permissions).length) {
-        doc[AclSym] = permissions;
-    }
+      if (Object.keys(permissions).length) {
+            doc[AclSym] = permissions;
+      }
 
-    if (doc.proto instanceof Promise) {
-        doc.proto.then(updateCachedAcls);
-        return doc.proto;
-    }
+      if (doc.proto instanceof Promise) {
+            doc.proto.then(updateCachedAcls);
+            return doc.proto;
+      }
 }
 
 @scriptingGlobal
 @Deserializable("Doc", updateCachedAcls).withFields(["id"])
 export class Doc extends RefField {
-    constructor(id?: FieldId, forceSave?: boolean) {
-        super(id);
-        const doc = new Proxy<this>(this, {
-            set: setter,
-            get: getter,
-            // getPrototypeOf: (target) => Cast(target[SelfProxy].proto, Doc) || null, // TODO this might be able to replace the proto logic in getter
-            has: (target, key) => GetEffectiveAcl(target) !== AclPrivate && key in target.__fields,
-            ownKeys: target => {
-                const obj = {} as any;
-                if (GetEffectiveAcl(target) !== AclPrivate) Object.assign(obj, target.___fieldKeys);
-                runInAction(() => obj.__LAYOUT__ = target.__LAYOUT__);
-                return Object.keys(obj);
-            },
-            getOwnPropertyDescriptor: (target, prop) => {
-                if (prop.toString() === "__LAYOUT__") {
-                    return Reflect.getOwnPropertyDescriptor(target, prop);
-                }
-                if (prop in target.__fieldKeys) {
-                    return {
-                        configurable: true,//TODO Should configurable be true?
-                        enumerable: true,
-                        value: 0//() => target.__fields[prop])
-                    };
-                }
-                return Reflect.getOwnPropertyDescriptor(target, prop);
-            },
-            deleteProperty: deleteProperty,
-            defineProperty: () => { throw new Error("Currently properties can't be defined on documents using Object.defineProperty"); },
-        });
-        this[SelfProxy] = doc;
-        if (!id || forceSave) {
-            DocServer.CreateField(doc);
-        }
-        return doc;
-    }
-
-    proto: Opt<Doc>;
-    [key: string]: FieldResult;
-
-    @serializable(alias("fields", map(autoObject(), { afterDeserialize: afterDocDeserialize })))
-    private get __fields() { return this.___fields; }
-    private set __fields(value) {
-        this.___fields = value;
-        for (const key in value) {
-            const field = value[key];
-            (field !== undefined) && (this.__fieldKeys[key] = true);
-            if (!(field instanceof ObjectField)) continue;
-            field[Parent] = this[Self];
-            field[OnUpdate] = updateFunction(this[Self], key, field, this[SelfProxy]);
-        }
-    }
-    private get __fieldKeys() { return this.___fieldKeys; }
-    private set __fieldKeys(value) { this.___fieldKeys = value; }
-
-    @observable private ___fields: any = {};
-    @observable private ___fieldKeys: any = {};
-    @observable public [AclSym]: { [key: string]: symbol };
-    @observable public [DirectLinksSym]: Set<Doc> = new Set();
-
-    private [UpdatingFromServer]: boolean = false;
-    private [ForceServerWrite]: boolean = false;
-    public [Initializing]: boolean = false;
-
-    private [Update] = (diff: any) => {
-        (!this[UpdatingFromServer] || this[ForceServerWrite]) && DocServer.UpdateField(this[Id], diff);
-    }
-
-    private [Self] = this;
-    private [SelfProxy]: any;
-    public [FieldsSym] = () => this[Self].___fields; // Object.keys(this).reduce((fields, key) => { fields[key] = this[key]; return fields; }, {} as any);
-    public [WidthSym] = () => NumCast(this[SelfProxy]._width);
-    public [HeightSym] = () => NumCast(this[SelfProxy]._height);
-    public [ToScriptString] = () => `idToDoc("${this[Self][Id]}")`;
-    public [ToString] = () => `Doc(${GetEffectiveAcl(this[SelfProxy]) === AclPrivate ? "-inaccessible-" : this[SelfProxy].title})`;
-    public get [LayoutSym]() { return this[SelfProxy].__LAYOUT__; }
-    public get [DataSym]() {
-        const self = this[SelfProxy];
-        return self.resolvedDataDoc && !self.isTemplateForField ? self :
-            Doc.GetProto(Cast(Doc.Layout(self).resolvedDataDoc, Doc, null) || self);
-    }
-    @computed get __LAYOUT__(): Doc | undefined {
-        const templateLayoutDoc = Cast(Doc.LayoutField(this[SelfProxy]), Doc, null);
-        if (templateLayoutDoc) {
-            let renderFieldKey: any;
-            const layoutField = templateLayoutDoc[StrCast(templateLayoutDoc.layoutKey, "layout")];
-            if (typeof layoutField === "string") {
-                renderFieldKey = layoutField.split("fieldKey={'")[1].split("'")[0];//layoutField.split("'")[1];
-            } else {
-                return Cast(layoutField, Doc, null);
+      constructor(id?: FieldId, forceSave?: boolean) {
+            super(id);
+            const doc = new Proxy<this>(this, {
+                  set: setter,
+                  get: getter,
+                  // getPrototypeOf: (target) => Cast(target[SelfProxy].proto, Doc) || null, // TODO this might be able to replace the proto logic in getter
+                  has: (target, key) => GetEffectiveAcl(target) !== AclPrivate && key in target.__fields,
+                  ownKeys: target => {
+                        const obj = {} as any;
+                        if (GetEffectiveAcl(target) !== AclPrivate) Object.assign(obj, target.___fieldKeys);
+                        runInAction(() => obj.__LAYOUT__ = target.__LAYOUT__);
+                        return Object.keys(obj);
+                  },
+                  getOwnPropertyDescriptor: (target, prop) => {
+                        if (prop.toString() === "__LAYOUT__") {
+                              return Reflect.getOwnPropertyDescriptor(target, prop);
+                        }
+                        if (prop in target.__fieldKeys) {
+                              return {
+                                    configurable: true,//TODO Should configurable be true?
+                                    enumerable: true,
+                                    value: 0//() => target.__fields[prop])
+                              };
+                        }
+                        return Reflect.getOwnPropertyDescriptor(target, prop);
+                  },
+                  deleteProperty: deleteProperty,
+                  defineProperty: () => { throw new Error("Currently properties can't be defined on documents using Object.defineProperty"); },
+            });
+            this[SelfProxy] = doc;
+            if (!id || forceSave) {
+                  DocServer.CreateField(doc);
+            }
+            return doc;
+      }
+
+      proto: Opt<Doc>;
+      [key: string]: FieldResult;
+
+      @serializable(alias("fields", map(autoObject(), { afterDeserialize: afterDocDeserialize })))
+      private get __fields() { return this.___fields; }
+      private set __fields(value) {
+            this.___fields = value;
+            for (const key in value) {
+                  const field = value[key];
+                  (field !== undefined) && (this.__fieldKeys[key] = true);
+                  if (!(field instanceof ObjectField)) continue;
+                  field[Parent] = this[Self];
+                  field[OnUpdate] = updateFunction(this[Self], key, field, this[SelfProxy]);
             }
-            return Cast(this[SelfProxy][renderFieldKey + "-layout[" + templateLayoutDoc[Id] + "]"], Doc, null) || templateLayoutDoc;
-        }
-        return undefined;
-
-    }
-
-    private [CachedUpdates]: { [key: string]: () => void | Promise<any> } = {};
-    public static CurrentUserEmail: string = "";
-    public static get CurrentUserEmailNormalized() { return normalizeEmail(Doc.CurrentUserEmail); }
-    public async [HandleUpdate](diff: any) {
-        const set = diff.$set;
-        const sameAuthor = this.author === Doc.CurrentUserEmail;
-        if (set) {
-            for (const key in set) {
-                const fprefix = "fields.";
-                if (!key.startsWith(fprefix)) {
-                    continue;
-                }
-                const fKey = key.substring(fprefix.length);
-                const fn = async () => {
-                    const value = await SerializationHelper.Deserialize(set[key]);
-                    const prev = GetEffectiveAcl(this);
-                    this[UpdatingFromServer] = true;
-                    this[fKey] = value;
-                    this[UpdatingFromServer] = false;
-                    if (fKey.startsWith("acl")) {
-                        updateCachedAcls(this);
-                    }
-                    if (prev === AclPrivate && GetEffectiveAcl(this) !== AclPrivate) {
-                        DocServer.GetRefField(this[Id], true);
-                    }
-                };
-                const writeMode = DocServer.getFieldWriteMode(fKey);
-                if (fKey.startsWith("acl") || writeMode !== DocServer.WriteMode.Playground) {
-                    delete this[CachedUpdates][fKey];
-                    await fn();
-                } else {
-                    this[CachedUpdates][fKey] = fn;
-                }
+      }
+      private get __fieldKeys() { return this.___fieldKeys; }
+      private set __fieldKeys(value) { this.___fieldKeys = value; }
+
+      @observable private ___fields: any = {};
+      @observable private ___fieldKeys: any = {};
+      @observable public [AclSym]: { [key: string]: symbol };
+      @observable public [DirectLinksSym]: Set<Doc> = new Set();
+
+      private [UpdatingFromServer]: boolean = false;
+      private [ForceServerWrite]: boolean = false;
+      public [Initializing]: boolean = false;
+
+      private [Update] = (diff: any) => {
+            (!this[UpdatingFromServer] || this[ForceServerWrite]) && DocServer.UpdateField(this[Id], diff);
+      }
+
+      private [Self] = this;
+      private [SelfProxy]: any;
+      public [FieldsSym] = () => this[Self].___fields; // Object.keys(this).reduce((fields, key) => { fields[key] = this[key]; return fields; }, {} as any);
+      public [WidthSym] = () => NumCast(this[SelfProxy]._width);
+      public [HeightSym] = () => NumCast(this[SelfProxy]._height);
+      public [ToScriptString] = () => `idToDoc("${this[Self][Id]}")`;
+      public [ToString] = () => `Doc(${GetEffectiveAcl(this[SelfProxy]) === AclPrivate ? "-inaccessible-" : this[SelfProxy].title})`;
+      public get [LayoutSym]() { return this[SelfProxy].__LAYOUT__; }
+      public get [DataSym]() {
+            const self = this[SelfProxy];
+            return self.resolvedDataDoc && !self.isTemplateForField ? self :
+                  Doc.GetProto(Cast(Doc.Layout(self).resolvedDataDoc, Doc, null) || self);
+      }
+      @computed get __LAYOUT__(): Doc | undefined {
+            const templateLayoutDoc = Cast(Doc.LayoutField(this[SelfProxy]), Doc, null);
+            if (templateLayoutDoc) {
+                  let renderFieldKey: any;
+                  const layoutField = templateLayoutDoc[StrCast(templateLayoutDoc.layoutKey, "layout")];
+                  if (typeof layoutField === "string") {
+                        renderFieldKey = layoutField.split("fieldKey={'")[1].split("'")[0];//layoutField.split("'")[1];
+                  } else {
+                        return Cast(layoutField, Doc, null);
+                  }
+                  return Cast(this[SelfProxy][renderFieldKey + "-layout[" + templateLayoutDoc[Id] + "]"], Doc, null) || templateLayoutDoc;
             }
-        }
-        const unset = diff.$unset;
-        if (unset) {
-            for (const key in unset) {
-                if (!key.startsWith("fields.")) {
-                    continue;
-                }
-                const fKey = key.substring(7);
-                const fn = () => {
-                    this[UpdatingFromServer] = true;
-                    delete this[fKey];
-                    this[UpdatingFromServer] = false;
-                };
-                if (sameAuthor || DocServer.getFieldWriteMode(fKey) !== DocServer.WriteMode.Playground) {
-                    delete this[CachedUpdates][fKey];
-                    await fn();
-                } else {
-                    this[CachedUpdates][fKey] = fn;
-                }
+            return undefined;
+
+      }
+
+      private [CachedUpdates]: { [key: string]: () => void | Promise<any> } = {};
+      public static CurrentUserEmail: string = "";
+      public static get CurrentUserEmailNormalized() { return normalizeEmail(Doc.CurrentUserEmail); }
+      public async [HandleUpdate](diff: any) {
+            const set = diff.$set;
+            const sameAuthor = this.author === Doc.CurrentUserEmail;
+            if (set) {
+                  for (const key in set) {
+                        const fprefix = "fields.";
+                        if (!key.startsWith(fprefix)) {
+                              continue;
+                        }
+                        const fKey = key.substring(fprefix.length);
+                        const fn = async () => {
+                              const value = await SerializationHelper.Deserialize(set[key]);
+                              const prev = GetEffectiveAcl(this);
+                              this[UpdatingFromServer] = true;
+                              this[fKey] = value;
+                              this[UpdatingFromServer] = false;
+                              if (fKey.startsWith("acl")) {
+                                    updateCachedAcls(this);
+                              }
+                              if (prev === AclPrivate && GetEffectiveAcl(this) !== AclPrivate) {
+                                    DocServer.GetRefField(this[Id], true);
+                              }
+                        };
+                        const writeMode = DocServer.getFieldWriteMode(fKey);
+                        if (fKey.startsWith("acl") || writeMode !== DocServer.WriteMode.Playground) {
+                              delete this[CachedUpdates][fKey];
+                              await fn();
+                        } else {
+                              this[CachedUpdates][fKey] = fn;
+                        }
+                  }
+            }
+            const unset = diff.$unset;
+            if (unset) {
+                  for (const key in unset) {
+                        if (!key.startsWith("fields.")) {
+                              continue;
+                        }
+                        const fKey = key.substring(7);
+                        const fn = () => {
+                              this[UpdatingFromServer] = true;
+                              delete this[fKey];
+                              this[UpdatingFromServer] = false;
+                        };
+                        if (sameAuthor || DocServer.getFieldWriteMode(fKey) !== DocServer.WriteMode.Playground) {
+                              delete this[CachedUpdates][fKey];
+                              await fn();
+                        } else {
+                              this[CachedUpdates][fKey] = fn;
+                        }
+                  }
             }
-        }
-    }
+      }
 }
 
 export namespace Doc {
-    // export function GetAsync(doc: Doc, key: string, ignoreProto: boolean = false): Promise<Field | undefined> {
-    //     const self = doc[Self];
-    //     return new Promise(res => getField(self, key, ignoreProto, res));
-    // }
-    // export function GetTAsync<T extends Field>(doc: Doc, key: string, ctor: ToConstructor<T>, ignoreProto: boolean = false): Promise<T | undefined> {
-    //     return new Promise(async res => {
-    //         const field = await GetAsync(doc, key, ignoreProto);
-    //         return Cast(field, ctor);
-    //     });
-    // }
-
-    export function RunCachedUpdate(doc: Doc, field: string) {
-        const update = doc[CachedUpdates][field];
-        if (update) {
-            update();
-            delete doc[CachedUpdates][field];
-        }
-    }
-    export function AddCachedUpdate(doc: Doc, field: string, oldValue: any) {
-        const val = oldValue;
-        doc[CachedUpdates][field] = () => {
-            doc[UpdatingFromServer] = true;
-            doc[field] = val;
-            doc[UpdatingFromServer] = false;
-        };
-    }
-    export function MakeReadOnly(): { end(): void } {
-        makeReadOnly();
-        return {
-            end() {
-                makeEditable();
+      // export function GetAsync(doc: Doc, key: string, ignoreProto: boolean = false): Promise<Field | undefined> {
+      //     const self = doc[Self];
+      //     return new Promise(res => getField(self, key, ignoreProto, res));
+      // }
+      // export function GetTAsync<T extends Field>(doc: Doc, key: string, ctor: ToConstructor<T>, ignoreProto: boolean = false): Promise<T | undefined> {
+      //     return new Promise(async res => {
+      //         const field = await GetAsync(doc, key, ignoreProto);
+      //         return Cast(field, ctor);
+      //     });
+      // }
+
+      export function RunCachedUpdate(doc: Doc, field: string) {
+            const update = doc[CachedUpdates][field];
+            if (update) {
+                  update();
+                  delete doc[CachedUpdates][field];
             }
-        };
-    }
+      }
+      export function AddCachedUpdate(doc: Doc, field: string, oldValue: any) {
+            const val = oldValue;
+            doc[CachedUpdates][field] = () => {
+                  doc[UpdatingFromServer] = true;
+                  doc[field] = val;
+                  doc[UpdatingFromServer] = false;
+            };
+      }
+      export function MakeReadOnly(): { end(): void } {
+            makeReadOnly();
+            return {
+                  end() {
+                        makeEditable();
+                  }
+            };
+      }
 
-    export function Get(doc: Doc, key: string, ignoreProto: boolean = false): FieldResult {
-        try {
-            return getField(doc[Self], key, ignoreProto);
-        } catch {
+      export function Get(doc: Doc, key: string, ignoreProto: boolean = false): FieldResult {
+            try {
+                  return getField(doc[Self], key, ignoreProto);
+            } catch {
+                  return doc;
+            }
+      }
+      export function GetT<T extends Field>(doc: Doc, key: string, ctor: ToConstructor<T>, ignoreProto: boolean = false): FieldResult<T> {
+            return Cast(Get(doc, key, ignoreProto), ctor) as FieldResult<T>;
+      }
+      export function IsPrototype(doc: Doc) {
+            return GetT(doc, "isPrototype", "boolean", true);
+      }
+      export function IsBaseProto(doc: Doc) {
+            return GetT(doc, "baseProto", "boolean", true);
+      }
+      export function IsSystem(doc: Doc) {
+            return GetT(doc, "system", "boolean", true);
+      }
+      export async function SetInPlace(doc: Doc, key: string, value: Field | undefined, defaultProto: boolean) {
+            if (key.startsWith("_")) key = key.substring(1);
+            const hasProto = doc.proto instanceof Doc;
+            const onDeleg = Object.getOwnPropertyNames(doc).indexOf(key) !== -1;
+            const onProto = hasProto && Object.getOwnPropertyNames(doc.proto).indexOf(key) !== -1;
+            if (onDeleg || !hasProto || (!onProto && !defaultProto)) {
+                  doc[key] = value;
+            } else doc.proto![key] = value;
+      }
+      export async function SetOnPrototype(doc: Doc, key: string, value: Field) {
+            const proto = Object.getOwnPropertyNames(doc).indexOf("isPrototype") === -1 ? doc.proto : doc;
+
+            if (proto) {
+                  proto[key] = value;
+            }
+      }
+      export function GetAllPrototypes(doc: Doc): Doc[] {
+            const protos: Doc[] = [];
+            let d: Opt<Doc> = doc;
+            while (d) {
+                  protos.push(d);
+                  d = FieldValue(d.proto);
+            }
+            return protos;
+      }
+
+      /**
+       * This function is intended to model Object.assign({}, {}) [https://mzl.la/1Mo3l21], which copies
+       * the values of the properties of a source object into the target.
+       *
+       * This is just a specific, Dash-authored version that serves the same role for our
+       * Doc class.
+       *
+       * @param doc the target document into which you'd like to insert the new fields
+       * @param fields the fields to project onto the target. Its type signature defines a mapping from some string key
+       * to a potentially undefined field, where each entry in this mapping is optional.
+       */
+      export function assign<K extends string>(doc: Doc, fields: Partial<Record<K, Opt<Field>>>, skipUndefineds: boolean = false, isInitializing = false) {
+            isInitializing && (doc[Initializing] = true);
+            for (const key in fields) {
+                  if (fields.hasOwnProperty(key)) {
+                        const value = fields[key];
+                        if (!skipUndefineds || value !== undefined) { // Do we want to filter out undefineds?
+                              doc[key] = value;
+                        }
+                  }
+            }
+            isInitializing && (doc[Initializing] = false);
             return doc;
-        }
-    }
-    export function GetT<T extends Field>(doc: Doc, key: string, ctor: ToConstructor<T>, ignoreProto: boolean = false): FieldResult<T> {
-        return Cast(Get(doc, key, ignoreProto), ctor) as FieldResult<T>;
-    }
-    export function IsPrototype(doc: Doc) {
-        return GetT(doc, "isPrototype", "boolean", true);
-    }
-    export function IsBaseProto(doc: Doc) {
-        return GetT(doc, "baseProto", "boolean", true);
-    }
-    export function IsSystem(doc: Doc) {
-        return GetT(doc, "system", "boolean", true);
-    }
-    export async function SetInPlace(doc: Doc, key: string, value: Field | undefined, defaultProto: boolean) {
-        if (key.startsWith("_")) key = key.substring(1);
-        const hasProto = doc.proto instanceof Doc;
-        const onDeleg = Object.getOwnPropertyNames(doc).indexOf(key) !== -1;
-        const onProto = hasProto && Object.getOwnPropertyNames(doc.proto).indexOf(key) !== -1;
-        if (onDeleg || !hasProto || (!onProto && !defaultProto)) {
-            doc[key] = value;
-        } else doc.proto![key] = value;
-    }
-    export async function SetOnPrototype(doc: Doc, key: string, value: Field) {
-        const proto = Object.getOwnPropertyNames(doc).indexOf("isPrototype") === -1 ? doc.proto : doc;
-
-        if (proto) {
-            proto[key] = value;
-        }
-    }
-    export function GetAllPrototypes(doc: Doc): Doc[] {
-        const protos: Doc[] = [];
-        let d: Opt<Doc> = doc;
-        while (d) {
-            protos.push(d);
-            d = FieldValue(d.proto);
-        }
-        return protos;
-    }
-
-    /**
-     * This function is intended to model Object.assign({}, {}) [https://mzl.la/1Mo3l21], which copies
-     * the values of the properties of a source object into the target.
-     *
-     * This is just a specific, Dash-authored version that serves the same role for our
-     * Doc class.
-     *
-     * @param doc the target document into which you'd like to insert the new fields
-     * @param fields the fields to project onto the target. Its type signature defines a mapping from some string key
-     * to a potentially undefined field, where each entry in this mapping is optional.
-     */
-    export function assign<K extends string>(doc: Doc, fields: Partial<Record<K, Opt<Field>>>, skipUndefineds: boolean = false, isInitializing = false) {
-        isInitializing && (doc[Initializing] = true);
-        for (const key in fields) {
-            if (fields.hasOwnProperty(key)) {
-                const value = fields[key];
-                if (!skipUndefineds || value !== undefined) { // Do we want to filter out undefineds?
-                    doc[key] = value;
-                }
+      }
+
+      // compare whether documents or their protos match
+      export function AreProtosEqual(doc?: Doc, other?: Doc) {
+            if (!doc || !other) return false;
+            const r = (doc === other);
+            const r2 = (Doc.GetProto(doc) === other);
+            const r3 = (Doc.GetProto(other) === doc);
+            const r4 = (Doc.GetProto(doc) === Doc.GetProto(other) && Doc.GetProto(other) !== undefined);
+            return r || r2 || r3 || r4;
+      }
+
+      // Gets the data document for the document.  Note: this is mis-named -- it does not specifically
+      // return the doc's proto, but rather recursively searches through the proto inheritance chain
+      // and returns the document who's proto is undefined or whose proto is marked as a base prototype ('isPrototype').
+      export function GetProto(doc: Doc): Doc {
+            if (doc instanceof Promise) {
+                  // console.log("GetProto: warning: got Promise insead of Doc");
             }
-        }
-        isInitializing && (doc[Initializing] = false);
-        return doc;
-    }
-
-    // compare whether documents or their protos match
-    export function AreProtosEqual(doc?: Doc, other?: Doc) {
-        if (!doc || !other) return false;
-        const r = (doc === other);
-        const r2 = (Doc.GetProto(doc) === other);
-        const r3 = (Doc.GetProto(other) === doc);
-        const r4 = (Doc.GetProto(doc) === Doc.GetProto(other) && Doc.GetProto(other) !== undefined);
-        return r || r2 || r3 || r4;
-    }
-
-    // Gets the data document for the document.  Note: this is mis-named -- it does not specifically
-    // return the doc's proto, but rather recursively searches through the proto inheritance chain
-    // and returns the document who's proto is undefined or whose proto is marked as a base prototype ('isPrototype').
-    export function GetProto(doc: Doc): Doc {
-        if (doc instanceof Promise) {
-            // console.log("GetProto: warning: got Promise insead of Doc");
-        }
-        const proto = doc && (Doc.GetT(doc, "isPrototype", "boolean", true) ? doc : (doc.proto || doc));
-        return proto === doc ? proto : Doc.GetProto(proto);
-    }
-    export function GetDataDoc(doc: Doc): Doc {
-        const proto = Doc.GetProto(doc);
-        return proto === doc ? proto : Doc.GetDataDoc(proto);
-    }
-
-    export function allKeys(doc: Doc): string[] {
-        const results: Set<string> = new Set;
-
-        let proto: Doc | undefined = doc;
-        while (proto) {
-            Object.keys(proto).forEach(key => results.add(key));
-            proto = proto.proto;
-        }
-
-        return Array.from(results);
-    }
-
-    /**
-     * @returns the index of doc toFind in list of docs, -1 otherwise
-     */
-    export function IndexOf(toFind: Doc, list: Doc[], allowProtos: boolean = true) {
-        let index = list.reduce((p, v, i) => (v instanceof Doc && v === toFind) ? i : p, -1);
-        index = allowProtos && index !== -1 ? index : list.reduce((p, v, i) => (v instanceof Doc && Doc.AreProtosEqual(v, toFind)) ? i : p, -1);
-        return index; // list.findIndex(doc => doc === toFind || Doc.AreProtosEqual(doc, toFind));
-    }
-
-    /**
-     * Removes doc from the list of Docs at listDoc[fieldKey]
-     * @returns true if successful, false otherwise.
-     */
-    export function RemoveDocFromList(listDoc: Doc, fieldKey: string | undefined, doc: Doc) {
-        const key = fieldKey ? fieldKey : Doc.LayoutFieldKey(listDoc);
-        if (listDoc[key] === undefined) {
-            Doc.GetProto(listDoc)[key] = new List<Doc>();
-        }
-        const list = Cast(listDoc[key], listSpec(Doc));
-        if (list) {
-            const ind = list.indexOf(doc);
-            if (ind !== -1) {
-                list.splice(ind, 1);
-                return true;
+            const proto = doc && (Doc.GetT(doc, "isPrototype", "boolean", true) ? doc : (doc.proto || doc));
+            return proto === doc ? proto : Doc.GetProto(proto);
+      }
+      export function GetDataDoc(doc: Doc): Doc {
+            const proto = Doc.GetProto(doc);
+            return proto === doc ? proto : Doc.GetDataDoc(proto);
+      }
+
+      export function allKeys(doc: Doc): string[] {
+            const results: Set<string> = new Set;
+
+            let proto: Doc | undefined = doc;
+            while (proto) {
+                  Object.keys(proto).forEach(key => results.add(key));
+                  proto = proto.proto;
             }
-        }
-        return false;
-    }
-
-    /**
-     * Adds doc to the list of Docs stored at listDoc[fieldKey].
-     * @returns true if successful, false otherwise.
-     */
-    export function AddDocToList(listDoc: Doc, fieldKey: string | undefined, doc: Doc, relativeTo?: Doc, before?: boolean, first?: boolean, allowDuplicates?: boolean, reversed?: boolean) {
-        const key = fieldKey ? fieldKey : Doc.LayoutFieldKey(listDoc);
-        if (listDoc[key] === undefined) {
-            Doc.GetProto(listDoc)[key] = new List<Doc>();
-        }
-        const list = Cast(listDoc[key], listSpec(Doc));
-        if (list) {
-            if (allowDuplicates !== true) {
-                const pind = list.reduce((l, d, i) => d instanceof Doc && d[Id] === doc[Id] ? i : l, -1);
-                if (pind !== -1) {
-                    return true;
-                    //list.splice(pind, 1);  // bcz: this causes schemaView docs in the Catalog to move to the bottom of the schema view when they are dragged even though they haven't left the collection
-                }
+
+            return Array.from(results);
+      }
+
+      /**
+       * @returns the index of doc toFind in list of docs, -1 otherwise
+       */
+      export function IndexOf(toFind: Doc, list: Doc[], allowProtos: boolean = true) {
+            let index = list.reduce((p, v, i) => (v instanceof Doc && v === toFind) ? i : p, -1);
+            index = allowProtos && index !== -1 ? index : list.reduce((p, v, i) => (v instanceof Doc && Doc.AreProtosEqual(v, toFind)) ? i : p, -1);
+            return index; // list.findIndex(doc => doc === toFind || Doc.AreProtosEqual(doc, toFind));
+      }
+
+      /**
+       * Removes doc from the list of Docs at listDoc[fieldKey]
+       * @returns true if successful, false otherwise.
+       */
+      export function RemoveDocFromList(listDoc: Doc, fieldKey: string | undefined, doc: Doc) {
+            const key = fieldKey ? fieldKey : Doc.LayoutFieldKey(listDoc);
+            if (listDoc[key] === undefined) {
+                  Doc.GetProto(listDoc)[key] = new List<Doc>();
             }
-            if (first) {
-                list.splice(0, 0, doc);
+            const list = Cast(listDoc[key], listSpec(Doc));
+            if (list) {
+                  const ind = list.indexOf(doc);
+                  if (ind !== -1) {
+                        list.splice(ind, 1);
+                        return true;
+                  }
             }
-            else {
-                const ind = relativeTo ? list.indexOf(relativeTo) : -1;
-                if (ind === -1) {
-                    if (reversed) list.splice(0, 0, doc);
-                    else list.push(doc);
-                }
-                else {
-                    if (reversed) list.splice(before ? (list.length - ind) + 1 : list.length - ind, 0, doc);
-                    else list.splice(before ? ind : ind + 1, 0, doc);
-                }
+            return false;
+      }
+
+      /**
+       * Adds doc to the list of Docs stored at listDoc[fieldKey].
+       * @returns true if successful, false otherwise.
+       */
+      export function AddDocToList(listDoc: Doc, fieldKey: string | undefined, doc: Doc, relativeTo?: Doc, before?: boolean, first?: boolean, allowDuplicates?: boolean, reversed?: boolean) {
+            const key = fieldKey ? fieldKey : Doc.LayoutFieldKey(listDoc);
+            if (listDoc[key] === undefined) {
+                  Doc.GetProto(listDoc)[key] = new List<Doc>();
             }
-            return true;
-        }
-        return false;
-    }
-
-    /**
-     * Computes the bounds of the contents of a set of documents.
-     */
-    export function ComputeContentBounds(docList: Doc[]) {
-        const bounds = docList.reduce((bounds, doc) => {
-            const [sptX, sptY] = [NumCast(doc.x), NumCast(doc.y)];
-            const [bptX, bptY] = [sptX + doc[WidthSym](), sptY + doc[HeightSym]()];
-            return {
-                x: Math.min(sptX, bounds.x), y: Math.min(sptY, bounds.y),
-                r: Math.max(bptX, bounds.r), b: Math.max(bptY, bounds.b)
-            };
-        }, { x: Number.MAX_VALUE, y: Number.MAX_VALUE, r: -Number.MAX_VALUE, b: -Number.MAX_VALUE });
-        return bounds;
-    }
-
-    export function MakeAlias(doc: Doc, id?: string) {
-        const alias = !GetT(doc, "isPrototype", "boolean", true) && doc.proto ? Doc.MakeCopy(doc, undefined, id) : Doc.MakeDelegate(doc, id);
-        const layout = Doc.LayoutField(alias);
-        if (layout instanceof Doc && layout !== alias && layout === Doc.Layout(alias)) {
-            Doc.SetLayout(alias, Doc.MakeAlias(layout));
-        }
-        alias.aliasOf = doc;
-        alias.aliasNumber = Doc.GetProto(doc).aliasNumber = NumCast(Doc.GetProto(doc).aliasNumber) + 1;
-        alias.title = ComputedField.MakeFunction(`renameAlias(this)`);
-        alias.author = Doc.CurrentUserEmail;
-
-        Doc.AddDocToList(Doc.GetProto(doc)[DataSym], "aliases", alias);
-
-        return alias;
-    }
-
-    export async function makeClone(doc: Doc, cloneMap: Map<string, Doc>, linkMap: Map<Doc, Doc>, rtfs: { copy: Doc, key: string, field: RichTextField }[], exclusions: string[], dontCreate: boolean, asBranch: boolean): Promise<Doc> {
-        if (Doc.IsBaseProto(doc)) return doc;
-        if (cloneMap.get(doc[Id])) return cloneMap.get(doc[Id])!;
-        const copy = dontCreate ? asBranch ? (Cast(doc.branchMaster, Doc, null) || doc) : doc : new Doc(undefined, true);
-        cloneMap.set(doc[Id], copy);
-        const fieldExclusions = doc.type === DocumentType.MARKER ? exclusions.filter(ex => ex !== "annotationOn") : exclusions;
-        const filter = [...fieldExclusions, ...Cast(doc.cloneFieldFilter, listSpec("string"), [])];
-        await Promise.all(Object.keys(doc).map(async key => {
-            if (filter.includes(key)) return;
-            const assignKey = (val: any) => !dontCreate && (copy[key] = val);
-            const cfield = ComputedField.WithoutComputed(() => FieldValue(doc[key]));
-            const field = ProxyField.WithoutProxy(() => doc[key]);
-            const copyObjectField = async (field: ObjectField) => {
-                const list = await Cast(doc[key], listSpec(Doc));
-                const docs = list && (await DocListCastAsync(list))?.filter(d => d instanceof Doc);
-                if (docs !== undefined && docs.length) {
-                    const clones = await Promise.all(docs.map(async d => Doc.makeClone(d, cloneMap, linkMap, rtfs, exclusions, dontCreate, asBranch)));
-                    !dontCreate && assignKey(new List<Doc>(clones));
-                } else if (doc[key] instanceof Doc) {
-                    assignKey(key.includes("layout[") ? undefined : key.startsWith("layout") ? doc[key] as Doc : await Doc.makeClone(doc[key] as Doc, cloneMap, linkMap, rtfs, exclusions, dontCreate, asBranch)); // reference documents except copy documents that are expanded template fields
-                } else {
-                    !dontCreate && assignKey(ObjectField.MakeCopy(field));
-                    if (field instanceof RichTextField) {
-                        if (field.Data.includes('"audioId":') || field.Data.includes('"textId":') || field.Data.includes('"anchorId":')) {
-                            rtfs.push({ copy, key, field });
+            const list = Cast(listDoc[key], listSpec(Doc));
+            if (list) {
+                  if (allowDuplicates !== true) {
+                        const pind = list.reduce((l, d, i) => d instanceof Doc && d[Id] === doc[Id] ? i : l, -1);
+                        if (pind !== -1) {
+                              return true;
+                              //list.splice(pind, 1);  // bcz: this causes schemaView docs in the Catalog to move to the bottom of the schema view when they are dragged even though they haven't left the collection
                         }
-                    }
-                }
-            };
-            if (key === "proto") {
-                if (doc[key] instanceof Doc) {
-                    assignKey(await Doc.makeClone(doc[key] as Doc, cloneMap, linkMap, rtfs, exclusions, dontCreate, asBranch));
-                }
-            } else if (key === "anchor1" || key === "anchor2") {
-                if (doc[key] instanceof Doc) {
-                    assignKey(await Doc.makeClone(doc[key] as Doc, cloneMap, linkMap, rtfs, exclusions, true, asBranch));
-                }
-            } else {
-                if (field instanceof RefField) {
-                    assignKey(field);
-                } else if (cfield instanceof ComputedField) {
-                    !dontCreate && assignKey(ComputedField.MakeFunction(cfield.script.originalScript));
-                } else if (field instanceof ObjectField) {
-                    await copyObjectField(field);
-                } else if (field instanceof Promise) {
-                    debugger; //This shouldn't happen...
-                } else {
-                    assignKey(field);
-                }
+                  }
+                  if (first) {
+                        list.splice(0, 0, doc);
+                  }
+                  else {
+                        const ind = relativeTo ? list.indexOf(relativeTo) : -1;
+                        if (ind === -1) {
+                              if (reversed) list.splice(0, 0, doc);
+                              else list.push(doc);
+                        }
+                        else {
+                              if (reversed) list.splice(before ? (list.length - ind) + 1 : list.length - ind, 0, doc);
+                              else list.splice(before ? ind : ind + 1, 0, doc);
+                        }
+                  }
+                  return true;
             }
-        }));
-        for (const link of Array.from(doc[DirectLinksSym])) {
-            const linkClone = await Doc.makeClone(link, cloneMap, linkMap, rtfs, exclusions, dontCreate, asBranch);
-            linkMap.set(link, linkClone);
-        }
-        if (!dontCreate) {
-            Doc.SetInPlace(copy, "title", (asBranch ? "BRANCH: " : "CLONE: ") + doc.title, true);
-            asBranch ? (copy.branchOf = doc) : (copy.cloneOf = doc);
-            if (!Doc.IsPrototype(copy)) {
-                Doc.AddDocToList(doc, "branches", Doc.GetProto(copy));
+            return false;
+      }
+
+      /**
+       * Computes the bounds of the contents of a set of documents.
+       */
+      export function ComputeContentBounds(docList: Doc[]) {
+            const bounds = docList.reduce((bounds, doc) => {
+                  const [sptX, sptY] = [NumCast(doc.x), NumCast(doc.y)];
+                  const [bptX, bptY] = [sptX + doc[WidthSym](), sptY + doc[HeightSym]()];
+                  return {
+                        x: Math.min(sptX, bounds.x), y: Math.min(sptY, bounds.y),
+                        r: Math.max(bptX, bounds.r), b: Math.max(bptY, bounds.b)
+                  };
+            }, { x: Number.MAX_VALUE, y: Number.MAX_VALUE, r: -Number.MAX_VALUE, b: -Number.MAX_VALUE });
+            return bounds;
+      }
+
+      export function MakeAlias(doc: Doc, id?: string) {
+            const alias = !GetT(doc, "isPrototype", "boolean", true) && doc.proto ? Doc.MakeCopy(doc, undefined, id) : Doc.MakeDelegate(doc, id);
+            const layout = Doc.LayoutField(alias);
+            if (layout instanceof Doc && layout !== alias && layout === Doc.Layout(alias)) {
+                  Doc.SetLayout(alias, Doc.MakeAlias(layout));
             }
+            alias.aliasOf = doc;
+            alias.aliasNumber = Doc.GetProto(doc).aliasNumber = NumCast(Doc.GetProto(doc).aliasNumber) + 1;
+            alias.title = ComputedField.MakeFunction(`renameAlias(this)`);
+            alias.author = Doc.CurrentUserEmail;
+
+            Doc.AddDocToList(Doc.GetProto(doc)[DataSym], "aliases", alias);
+
+            return alias;
+      }
+
+      export async function makeClone(doc: Doc, cloneMap: Map<string, Doc>, linkMap: Map<Doc, Doc>, rtfs: { copy: Doc, key: string, field: RichTextField }[], exclusions: string[], dontCreate: boolean, asBranch: boolean): Promise<Doc> {
+            if (Doc.IsBaseProto(doc)) return doc;
+            if (cloneMap.get(doc[Id])) return cloneMap.get(doc[Id])!;
+            const copy = dontCreate ? asBranch ? (Cast(doc.branchMaster, Doc, null) || doc) : doc : new Doc(undefined, true);
             cloneMap.set(doc[Id], copy);
-        }
-        return copy;
-    }
-    export async function MakeClone(doc: Doc, dontCreate: boolean = false, asBranch = false, cloneMap: Map<string, Doc> = new Map()) {
-        const linkMap = new Map<Doc, Doc>();
-        const rtfMap: { copy: Doc, key: string, field: RichTextField }[] = [];
-        const copy = await Doc.makeClone(doc, cloneMap, linkMap, rtfMap, ["cloneOf", "branches", "branchOf"], dontCreate, asBranch);
-        Array.from(linkMap.entries()).map((links: Doc[]) => LinkManager.Instance.addLink(links[1], true));
-        rtfMap.map(({ copy, key, field }) => {
-            const replacer = (match: any, attr: string, id: string, offset: any, string: any) => {
-                const mapped = cloneMap.get(id);
-                return attr + "\"" + (mapped ? mapped[Id] : id) + "\"";
-            };
-            const replacer2 = (match: any, href: string, id: string, offset: any, string: any) => {
-                const mapped = cloneMap.get(id);
-                return href + (mapped ? mapped[Id] : id);
-            };
-            const regex = `(${Doc.localServerPath()})([^"]*)`;
-            const re = new RegExp(regex, "g");
-            copy[key] = new RichTextField(field.Data.replace(/("textId":|"audioId":|"anchorId":)"([^"]+)"/g, replacer).replace(re, replacer2), field.Text);
-        });
-        return { clone: copy, map: cloneMap };
-    }
-
-    export async function Zip(doc: Doc) {
-        // const a = document.createElement("a");
-        // const url = Utils.prepend(`/downloadId/${this.props.Document[Id]}`);
-        // a.href = url;
-        // a.download = `DocExport-${this.props.Document[Id]}.zip`;
-        // a.click();
-        const { clone, map } = await Doc.MakeClone(doc, true);
-        function replacer(key: any, value: any) {
-            if (["branchOf", "cloneOf", "context", "cursors"].includes(key)) return undefined;
-            else if (value instanceof Doc) {
-                if (key !== "field" && Number.isNaN(Number(key))) {
-                    const __fields = value[FieldsSym]();
-                    return { id: value[Id], __type: "Doc", fields: __fields };
-                } else {
-                    return { fieldId: value[Id], __type: "proxy" };
-                }
+            const fieldExclusions = doc.type === DocumentType.MARKER ? exclusions.filter(ex => ex !== "annotationOn") : exclusions;
+            const filter = [...fieldExclusions, ...Cast(doc.cloneFieldFilter, listSpec("string"), [])];
+            await Promise.all(Object.keys(doc).map(async key => {
+                  if (filter.includes(key)) return;
+                  const assignKey = (val: any) => !dontCreate && (copy[key] = val);
+                  const cfield = ComputedField.WithoutComputed(() => FieldValue(doc[key]));
+                  const field = ProxyField.WithoutProxy(() => doc[key]);
+                  const copyObjectField = async (field: ObjectField) => {
+                        const list = await Cast(doc[key], listSpec(Doc));
+                        const docs = list && (await DocListCastAsync(list))?.filter(d => d instanceof Doc);
+                        if (docs !== undefined && docs.length) {
+                              const clones = await Promise.all(docs.map(async d => Doc.makeClone(d, cloneMap, linkMap, rtfs, exclusions, dontCreate, asBranch)));
+                              !dontCreate && assignKey(new List<Doc>(clones));
+                        } else if (doc[key] instanceof Doc) {
+                              assignKey(key.includes("layout[") ? undefined : key.startsWith("layout") ? doc[key] as Doc : await Doc.makeClone(doc[key] as Doc, cloneMap, linkMap, rtfs, exclusions, dontCreate, asBranch)); // reference documents except copy documents that are expanded template fields
+                        } else {
+                              !dontCreate && assignKey(ObjectField.MakeCopy(field));
+                              if (field instanceof RichTextField) {
+                                    if (field.Data.includes('"audioId":') || field.Data.includes('"textId":') || field.Data.includes('"anchorId":')) {
+                                          rtfs.push({ copy, key, field });
+                                    }
+                              }
+                        }
+                  };
+                  if (key === "proto") {
+                        if (doc[key] instanceof Doc) {
+                              assignKey(await Doc.makeClone(doc[key] as Doc, cloneMap, linkMap, rtfs, exclusions, dontCreate, asBranch));
+                        }
+                  } else if (key === "anchor1" || key === "anchor2") {
+                        if (doc[key] instanceof Doc) {
+                              assignKey(await Doc.makeClone(doc[key] as Doc, cloneMap, linkMap, rtfs, exclusions, true, asBranch));
+                        }
+                  } else {
+                        if (field instanceof RefField) {
+                              assignKey(field);
+                        } else if (cfield instanceof ComputedField) {
+                              !dontCreate && assignKey(ComputedField.MakeFunction(cfield.script.originalScript));
+                        } else if (field instanceof ObjectField) {
+                              await copyObjectField(field);
+                        } else if (field instanceof Promise) {
+                              debugger; //This shouldn't happen...
+                        } else {
+                              assignKey(field);
+                        }
+                  }
+            }));
+            for (const link of Array.from(doc[DirectLinksSym])) {
+                  const linkClone = await Doc.makeClone(link, cloneMap, linkMap, rtfs, exclusions, dontCreate, asBranch);
+                  linkMap.set(link, linkClone);
+            }
+            if (!dontCreate) {
+                  Doc.SetInPlace(copy, "title", (asBranch ? "BRANCH: " : "CLONE: ") + doc.title, true);
+                  asBranch ? (copy.branchOf = doc) : (copy.cloneOf = doc);
+                  if (!Doc.IsPrototype(copy)) {
+                        Doc.AddDocToList(doc, "branches", Doc.GetProto(copy));
+                  }
+                  cloneMap.set(doc[Id], copy);
             }
-            else if (value instanceof ScriptField) return { script: value.script, __type: "script" };
-            else if (value instanceof RichTextField) return { Data: value.Data, Text: value.Text, __type: "RichTextField" };
-            else if (value instanceof ImageField) return { url: value.url.href, __type: "image" };
-            else if (value instanceof PdfField) return { url: value.url.href, __type: "pdf" };
-            else if (value instanceof AudioField) return { url: value.url.href, __type: "audio" };
-            else if (value instanceof VideoField) return { url: value.url.href, __type: "video" };
-            else if (value instanceof WebField) return { url: value.url.href, __type: "web" };
-            else if (value instanceof MapField) return { url: value.url.href, __type: "map" };
-            else if (value instanceof DateField) return { date: value.toString(), __type: "date" };
-            else if (value instanceof ProxyField) return { fieldId: value.fieldId, __type: "proxy" };
-            else if (value instanceof Array && key !== "fields") return { fields: value, __type: "list" };
-            else if (value instanceof ComputedField) return { script: value.script, __type: "computed" };
-            else return value;
-        }
-
-        const docs: { [id: string]: any } = {};
-        Array.from(map.entries()).forEach(f => docs[f[0]] = f[1]);
-        const docString = JSON.stringify({ id: doc[Id], docs }, replacer);
-
-        const zip = new JSZip();
-
-        zip.file(doc.title + ".json", docString);
-
-        // // Generate a directory within the Zip file structure
-        // var img = zip.folder("images");
-
-        // // Add a file to the directory, in this case an image with data URI as contents
-        // img.file("smile.gif", imgData, {base64: true});
-
-        // Generate the zip file asynchronously
-        zip.generateAsync({ type: "blob" })
-            .then((content: any) => {
-                // Force down of the Zip file
-                saveAs(content, doc.title + ".zip"); // glr: Possibly change the name of the document to match the title?
+            return copy;
+      }
+      export async function MakeClone(doc: Doc, dontCreate: boolean = false, asBranch = false, cloneMap: Map<string, Doc> = new Map()) {
+            const linkMap = new Map<Doc, Doc>();
+            const rtfMap: { copy: Doc, key: string, field: RichTextField }[] = [];
+            const copy = await Doc.makeClone(doc, cloneMap, linkMap, rtfMap, ["cloneOf", "branches", "branchOf"], dontCreate, asBranch);
+            Array.from(linkMap.entries()).map((links: Doc[]) => LinkManager.Instance.addLink(links[1], true));
+            rtfMap.map(({ copy, key, field }) => {
+                  const replacer = (match: any, attr: string, id: string, offset: any, string: any) => {
+                        const mapped = cloneMap.get(id);
+                        return attr + "\"" + (mapped ? mapped[Id] : id) + "\"";
+                  };
+                  const replacer2 = (match: any, href: string, id: string, offset: any, string: any) => {
+                        const mapped = cloneMap.get(id);
+                        return href + (mapped ? mapped[Id] : id);
+                  };
+                  const regex = `(${Doc.localServerPath()})([^"]*)`;
+                  const re = new RegExp(regex, "g");
+                  copy[key] = new RichTextField(field.Data.replace(/("textId":|"audioId":|"anchorId":)"([^"]+)"/g, replacer).replace(re, replacer2), field.Text);
             });
-    }
-    //
-    // Determines whether the layout needs to be expanded (as a template).
-    // template expansion is rquired when the layout is a template doc/field and there's a datadoc which isn't equal to the layout template
-    //
-    export function WillExpandTemplateLayout(layoutDoc: Doc, dataDoc?: Doc) {
-        return (layoutDoc.isTemplateForField || layoutDoc.isTemplateDoc) && dataDoc && layoutDoc !== dataDoc;
-    }
-
-    const _pendingMap: Map<string, boolean> = new Map();
-    //
-    // Returns an expanded template layout for a target data document if there is a template relationship
-    // between the two. If so, the layoutDoc is expanded into a new document that inherits the properties
-    // of the original layout while allowing for individual layout properties to be overridden in the expanded layout.
-    // templateArgs should be equivalent to the layout key that generates the template since that's where the template parameters are stored in ()'s at the end of the key.
-    // NOTE:  the template will have references to "@params" -- the template arguments will be assigned to the '@params' field
-    // so that when the @params key is accessed, it will be rewritten as the key that is stored in the 'params' field and
-    // the derefence will then occur on the rootDocument (the original document).
-    // in the future, field references could be written as @<someparam> and then arguments would be passed in the layout key as:
-    //   layout_mytemplate(somparam=somearg).
-    // then any references to @someparam would be rewritten as accesses to 'somearg' on the rootDocument
-    export function expandTemplateLayout(templateLayoutDoc: Doc, targetDoc?: Doc, templateArgs?: string) {
-        const args = templateArgs?.match(/\(([a-zA-Z0-9._\-]*)\)/)?.[1].replace("()", "") || StrCast(templateLayoutDoc.PARAMS);
-        if (!args && !WillExpandTemplateLayout(templateLayoutDoc, targetDoc) || !targetDoc) return templateLayoutDoc;
-
-        const templateField = StrCast(templateLayoutDoc.isTemplateForField);  // the field that the template renders
-        // First it checks if an expanded layout already exists -- if so it will be stored on the dataDoc
-        // using the template layout doc's id as the field key.
-        // If it doesn't find the expanded layout, then it makes a delegate of the template layout and
-        // saves it on the data doc indexed by the template layout's id.
-        //
-        const params = args.split("=").length > 1 ? args.split("=")[0] : "PARAMS";
-        const layoutFielddKey = Doc.LayoutFieldKey(templateLayoutDoc);
-        const expandedLayoutFieldKey = (templateField || layoutFielddKey) + "-layout[" + templateLayoutDoc[Id] + (args ? `(${args})` : "") + "]";
-        let expandedTemplateLayout = targetDoc?.[expandedLayoutFieldKey];
-
-        if (templateLayoutDoc.resolvedDataDoc instanceof Promise) {
-            expandedTemplateLayout = undefined;
-            _pendingMap.set(targetDoc[Id] + expandedLayoutFieldKey, true);
-        }
-        else if (expandedTemplateLayout === undefined && !_pendingMap.get(targetDoc[Id] + expandedLayoutFieldKey + args)) {
-            if (templateLayoutDoc.resolvedDataDoc === (targetDoc.rootDocument || Doc.GetProto(targetDoc)) && templateLayoutDoc.PARAMS === StrCast(targetDoc.PARAMS)) {
-                expandedTemplateLayout = templateLayoutDoc; // reuse an existing template layout if its for the same document with the same params
-            } else {
-                templateLayoutDoc.resolvedDataDoc && (templateLayoutDoc = Cast(templateLayoutDoc.proto, Doc, null) || templateLayoutDoc); // if the template has already been applied (ie, a nested template), then use the template's prototype
-                if (!targetDoc[expandedLayoutFieldKey]) {
-                    _pendingMap.set(targetDoc[Id] + expandedLayoutFieldKey + args, true);
-                    setTimeout(action(() => {
-                        const newLayoutDoc = Doc.MakeDelegate(templateLayoutDoc, undefined, "[" + templateLayoutDoc.title + "]");
-                        // the template's arguments are stored in params which is derefenced to find
-                        // the actual field key where the parameterized template data is stored.
-                        newLayoutDoc[params] = args !== "..." ? args : ""; // ... signifies the layout has sub template(s) -- so we have to expand the layout for them so that they can get the correct 'rootDocument' field, but we don't need to reassign their params.  it would be better if the 'rootDocument' field could be passed dynamically to avoid have to create instances
-                        newLayoutDoc.rootDocument = targetDoc;
-                        const dataDoc = Doc.GetProto(targetDoc);
-                        newLayoutDoc.resolvedDataDoc = dataDoc;
-                        if (dataDoc[templateField] === undefined && templateLayoutDoc[templateField] instanceof List && (templateLayoutDoc[templateField] as any).length) {
-                            dataDoc[templateField] = ComputedField.MakeFunction(`ObjectField.MakeCopy(templateLayoutDoc["${templateField}"] as List)`, { templateLayoutDoc: Doc.name }, { templateLayoutDoc });
+            return { clone: copy, map: cloneMap };
+      }
+
+      export async function Zip(doc: Doc) {
+            // const a = document.createElement("a");
+            // const url = Utils.prepend(`/downloadId/${this.props.Document[Id]}`);
+            // a.href = url;
+            // a.download = `DocExport-${this.props.Document[Id]}.zip`;
+            // a.click();
+            const { clone, map } = await Doc.MakeClone(doc, true);
+            function replacer(key: any, value: any) {
+                  if (["branchOf", "cloneOf", "context", "cursors"].includes(key)) return undefined;
+                  else if (value instanceof Doc) {
+                        if (key !== "field" && Number.isNaN(Number(key))) {
+                              const __fields = value[FieldsSym]();
+                              return { id: value[Id], __type: "Doc", fields: __fields };
+                        } else {
+                              return { fieldId: value[Id], __type: "proxy" };
                         }
-                        targetDoc[expandedLayoutFieldKey] = newLayoutDoc;
+                  }
+                  else if (value instanceof ScriptField) return { script: value.script, __type: "script" };
+                  else if (value instanceof RichTextField) return { Data: value.Data, Text: value.Text, __type: "RichTextField" };
+                  else if (value instanceof ImageField) return { url: value.url.href, __type: "image" };
+                  else if (value instanceof PdfField) return { url: value.url.href, __type: "pdf" };
+                  else if (value instanceof AudioField) return { url: value.url.href, __type: "audio" };
+                  else if (value instanceof VideoField) return { url: value.url.href, __type: "video" };
+                  else if (value instanceof WebField) return { url: value.url.href, __type: "web" };
+                  else if (value instanceof MapField) return { url: value.url.href, __type: "map" };
+                  else if (value instanceof DateField) return { date: value.toString(), __type: "date" };
+                  else if (value instanceof ProxyField) return { fieldId: value.fieldId, __type: "proxy" };
+                  else if (value instanceof Array && key !== "fields") return { fields: value, __type: "list" };
+                  else if (value instanceof ComputedField) return { script: value.script, __type: "computed" };
+                  else return value;
+            }
 
-                        _pendingMap.delete(targetDoc[Id] + expandedLayoutFieldKey + args);
-                    }));
-                }
+            const docs: { [id: string]: any } = {};
+            Array.from(map.entries()).forEach(f => docs[f[0]] = f[1]);
+            const docString = JSON.stringify({ id: doc[Id], docs }, replacer);
+
+            const zip = new JSZip();
+
+            zip.file(doc.title + ".json", docString);
+
+            // // Generate a directory within the Zip file structure
+            // var img = zip.folder("images");
+
+            // // Add a file to the directory, in this case an image with data URI as contents
+            // img.file("smile.gif", imgData, {base64: true});
+
+            // Generate the zip file asynchronously
+            zip.generateAsync({ type: "blob" })
+                  .then((content: any) => {
+                        // Force down of the Zip file
+                        saveAs(content, doc.title + ".zip"); // glr: Possibly change the name of the document to match the title?
+                  });
+      }
+      //
+      // Determines whether the layout needs to be expanded (as a template).
+      // template expansion is rquired when the layout is a template doc/field and there's a datadoc which isn't equal to the layout template
+      //
+      export function WillExpandTemplateLayout(layoutDoc: Doc, dataDoc?: Doc) {
+            return (layoutDoc.isTemplateForField || layoutDoc.isTemplateDoc) && dataDoc && layoutDoc !== dataDoc;
+      }
+
+      const _pendingMap: Map<string, boolean> = new Map();
+      //
+      // Returns an expanded template layout for a target data document if there is a template relationship
+      // between the two. If so, the layoutDoc is expanded into a new document that inherits the properties
+      // of the original layout while allowing for individual layout properties to be overridden in the expanded layout.
+      // templateArgs should be equivalent to the layout key that generates the template since that's where the template parameters are stored in ()'s at the end of the key.
+      // NOTE:  the template will have references to "@params" -- the template arguments will be assigned to the '@params' field
+      // so that when the @params key is accessed, it will be rewritten as the key that is stored in the 'params' field and
+      // the derefence will then occur on the rootDocument (the original document).
+      // in the future, field references could be written as @<someparam> and then arguments would be passed in the layout key as:
+      //   layout_mytemplate(somparam=somearg).
+      // then any references to @someparam would be rewritten as accesses to 'somearg' on the rootDocument
+      export function expandTemplateLayout(templateLayoutDoc: Doc, targetDoc?: Doc, templateArgs?: string) {
+            const args = templateArgs?.match(/\(([a-zA-Z0-9._\-]*)\)/)?.[1].replace("()", "") || StrCast(templateLayoutDoc.PARAMS);
+            if (!args && !WillExpandTemplateLayout(templateLayoutDoc, targetDoc) || !targetDoc) return templateLayoutDoc;
+
+            const templateField = StrCast(templateLayoutDoc.isTemplateForField);  // the field that the template renders
+            // First it checks if an expanded layout already exists -- if so it will be stored on the dataDoc
+            // using the template layout doc's id as the field key.
+            // If it doesn't find the expanded layout, then it makes a delegate of the template layout and
+            // saves it on the data doc indexed by the template layout's id.
+            //
+            const params = args.split("=").length > 1 ? args.split("=")[0] : "PARAMS";
+            const layoutFielddKey = Doc.LayoutFieldKey(templateLayoutDoc);
+            const expandedLayoutFieldKey = (templateField || layoutFielddKey) + "-layout[" + templateLayoutDoc[Id] + (args ? `(${args})` : "") + "]";
+            let expandedTemplateLayout = targetDoc?.[expandedLayoutFieldKey];
+
+            if (templateLayoutDoc.resolvedDataDoc instanceof Promise) {
+                  expandedTemplateLayout = undefined;
+                  _pendingMap.set(targetDoc[Id] + expandedLayoutFieldKey, true);
             }
-        }
-        return expandedTemplateLayout instanceof Doc ? expandedTemplateLayout : undefined; // layout is undefined if the expandedTemplateLayout is pending.
-    }
-
-    // if the childDoc is a template for a field, then this will return the expanded layout with its data doc.
-    // otherwise, it just returns the childDoc
-    export function GetLayoutDataDocPair(containerDoc: Doc, containerDataDoc: Opt<Doc>, childDoc: Doc) {
-        if (!childDoc || childDoc instanceof Promise || !Doc.GetProto(childDoc)) {
-            console.log("No, no, no!");
-            return { layout: childDoc, data: childDoc };
-        }
-        const resolvedDataDoc = (Doc.AreProtosEqual(containerDataDoc, containerDoc) || (!childDoc.isTemplateDoc && !childDoc.isTemplateForField && !childDoc.PARAMS) ? undefined : containerDataDoc);
-        return { layout: Doc.expandTemplateLayout(childDoc, resolvedDataDoc, "(" + StrCast(containerDoc.PARAMS) + ")"), data: resolvedDataDoc };
-    }
-
-    export function Overwrite(doc: Doc, overwrite: Doc, copyProto: boolean = false): Doc {
-        Object.keys(doc).forEach(key => {
-            const field = ProxyField.WithoutProxy(() => doc[key]);
-            if (key === "proto" && copyProto) {
-                if (doc.proto instanceof Doc && overwrite.proto instanceof Doc) {
-                    overwrite[key] = Doc.Overwrite(doc[key]!, overwrite.proto);
-                }
-            } else {
-                if (field instanceof RefField) {
-                    overwrite[key] = field;
-                } else if (field instanceof ObjectField) {
-                    overwrite[key] = ObjectField.MakeCopy(field);
-                } else if (field instanceof Promise) {
-                    debugger; //This shouldn't happend...
-                } else {
-                    overwrite[key] = field;
-                }
+            else if (expandedTemplateLayout === undefined && !_pendingMap.get(targetDoc[Id] + expandedLayoutFieldKey + args)) {
+                  if (templateLayoutDoc.resolvedDataDoc === (targetDoc.rootDocument || Doc.GetProto(targetDoc)) && templateLayoutDoc.PARAMS === StrCast(targetDoc.PARAMS)) {
+                        expandedTemplateLayout = templateLayoutDoc; // reuse an existing template layout if its for the same document with the same params
+                  } else {
+                        templateLayoutDoc.resolvedDataDoc && (templateLayoutDoc = Cast(templateLayoutDoc.proto, Doc, null) || templateLayoutDoc); // if the template has already been applied (ie, a nested template), then use the template's prototype
+                        if (!targetDoc[expandedLayoutFieldKey]) {
+                              _pendingMap.set(targetDoc[Id] + expandedLayoutFieldKey + args, true);
+                              setTimeout(action(() => {
+                                    const newLayoutDoc = Doc.MakeDelegate(templateLayoutDoc, undefined, "[" + templateLayoutDoc.title + "]");
+                                    // the template's arguments are stored in params which is derefenced to find
+                                    // the actual field key where the parameterized template data is stored.
+                                    newLayoutDoc[params] = args !== "..." ? args : ""; // ... signifies the layout has sub template(s) -- so we have to expand the layout for them so that they can get the correct 'rootDocument' field, but we don't need to reassign their params.  it would be better if the 'rootDocument' field could be passed dynamically to avoid have to create instances
+                                    newLayoutDoc.rootDocument = targetDoc;
+                                    const dataDoc = Doc.GetProto(targetDoc);
+                                    newLayoutDoc.resolvedDataDoc = dataDoc;
+                                    if (dataDoc[templateField] === undefined && templateLayoutDoc[templateField] instanceof List && (templateLayoutDoc[templateField] as any).length) {
+                                          dataDoc[templateField] = ComputedField.MakeFunction(`ObjectField.MakeCopy(templateLayoutDoc["${templateField}"] as List)`, { templateLayoutDoc: Doc.name }, { templateLayoutDoc });
+                                    }
+                                    targetDoc[expandedLayoutFieldKey] = newLayoutDoc;
+
+                                    _pendingMap.delete(targetDoc[Id] + expandedLayoutFieldKey + args);
+                              }));
+                        }
+                  }
+            }
+            return expandedTemplateLayout instanceof Doc ? expandedTemplateLayout : undefined; // layout is undefined if the expandedTemplateLayout is pending.
+      }
+
+      // if the childDoc is a template for a field, then this will return the expanded layout with its data doc.
+      // otherwise, it just returns the childDoc
+      export function GetLayoutDataDocPair(containerDoc: Doc, containerDataDoc: Opt<Doc>, childDoc: Doc) {
+            if (!childDoc || childDoc instanceof Promise || !Doc.GetProto(childDoc)) {
+                  console.log("No, no, no!");
+                  return { layout: childDoc, data: childDoc };
             }
-        });
-
-        return overwrite;
-    }
-
-    export function MakeCopy(doc: Doc, copyProto: boolean = false, copyProtoId?: string, retitle = false): Doc {
-        const copy = new Doc(copyProtoId, true);
-        const exclude = Cast(doc.cloneFieldFilter, listSpec("string"), []);
-        Object.keys(doc).forEach(key => {
-            if (exclude.includes(key)) return;
-            const cfield = ComputedField.WithoutComputed(() => FieldValue(doc[key]));
-            const field = ProxyField.WithoutProxy(() => doc[key]);
-            if (key === "proto" && copyProto) {
-                if (doc[key] instanceof Doc) {
-                    copy[key] = Doc.MakeCopy(doc[key]!, false);
-                }
+            const resolvedDataDoc = (Doc.AreProtosEqual(containerDataDoc, containerDoc) || (!childDoc.isTemplateDoc && !childDoc.isTemplateForField && !childDoc.PARAMS) ? undefined : containerDataDoc);
+            return { layout: Doc.expandTemplateLayout(childDoc, resolvedDataDoc, "(" + StrCast(containerDoc.PARAMS) + ")"), data: resolvedDataDoc };
+      }
+
+      export function Overwrite(doc: Doc, overwrite: Doc, copyProto: boolean = false): Doc {
+            Object.keys(doc).forEach(key => {
+                  const field = ProxyField.WithoutProxy(() => doc[key]);
+                  if (key === "proto" && copyProto) {
+                        if (doc.proto instanceof Doc && overwrite.proto instanceof Doc) {
+                              overwrite[key] = Doc.Overwrite(doc[key]!, overwrite.proto);
+                        }
+                  } else {
+                        if (field instanceof RefField) {
+                              overwrite[key] = field;
+                        } else if (field instanceof ObjectField) {
+                              overwrite[key] = ObjectField.MakeCopy(field);
+                        } else if (field instanceof Promise) {
+                              debugger; //This shouldn't happend...
+                        } else {
+                              overwrite[key] = field;
+                        }
+                  }
+            });
+
+            return overwrite;
+      }
+
+      export function MakeCopy(doc: Doc, copyProto: boolean = false, copyProtoId?: string, retitle = false): Doc {
+            const copy = new Doc(copyProtoId, true);
+            const exclude = Cast(doc.cloneFieldFilter, listSpec("string"), []);
+            Object.keys(doc).forEach(key => {
+                  if (exclude.includes(key)) return;
+                  const cfield = ComputedField.WithoutComputed(() => FieldValue(doc[key]));
+                  const field = ProxyField.WithoutProxy(() => doc[key]);
+                  if (key === "proto" && copyProto) {
+                        if (doc[key] instanceof Doc) {
+                              copy[key] = Doc.MakeCopy(doc[key]!, false);
+                        }
+                  } else {
+                        if (field instanceof RefField) {
+                              copy[key] = field;
+                        } else if (cfield instanceof ComputedField) {
+                              copy[key] = cfield[Copy]();// ComputedField.MakeFunction(cfield.script.originalScript);
+                        } else if (field instanceof ObjectField) {
+                              copy[key] = doc[key] instanceof Doc ?
+                                    key.includes("layout[") ? undefined : doc[key] : // reference documents except remove documents that are expanded teplate fields
+                                    ObjectField.MakeCopy(field);
+                        } else if (field instanceof Promise) {
+                              debugger; //This shouldn't happend...
+                        } else {
+                              copy[key] = field;
+                        }
+                  }
+            });
+            copy.author = Doc.CurrentUserEmail;
+            if (copyProto) {
+                  Doc.GetProto(copy).context = undefined;
+                  Doc.GetProto(copy).aliases = new List<Doc>([copy]);
             } else {
-                if (field instanceof RefField) {
-                    copy[key] = field;
-                } else if (cfield instanceof ComputedField) {
-                    copy[key] = cfield[Copy]();// ComputedField.MakeFunction(cfield.script.originalScript);
-                } else if (field instanceof ObjectField) {
-                    copy[key] = doc[key] instanceof Doc ?
-                        key.includes("layout[") ? undefined : doc[key] : // reference documents except remove documents that are expanded teplate fields
-                        ObjectField.MakeCopy(field);
-                } else if (field instanceof Promise) {
-                    debugger; //This shouldn't happend...
-                } else {
-                    copy[key] = field;
-                }
+                  Doc.AddDocToList(Doc.GetProto(copy)[DataSym], "aliases", copy);
+            }
+            copy.context = undefined;
+            Doc.UserDoc().defaultAclPrivate && (copy["acl-Public"] = "Not Shared");
+            if (retitle) {
+                  copy.title = incrementTitleCopy(StrCast(copy.title));
+            }
+            return copy;
+      }
+
+
+      export function MakeDelegate(doc: Doc, id?: string, title?: string): Doc;
+      export function MakeDelegate(doc: Opt<Doc>, id?: string, title?: string): Opt<Doc>;
+      export function MakeDelegate(doc: Opt<Doc>, id?: string, title?: string): Opt<Doc> {
+            if (doc) {
+                  const delegate = new Doc(id, true);
+                  delegate[Initializing] = true;
+                  delegate.proto = doc;
+                  delegate.author = Doc.CurrentUserEmail;
+                  if (!Doc.IsSystem(doc)) Doc.AddDocToList(doc[DataSym], "aliases", delegate);
+                  title && (delegate.title = title);
+                  delegate[Initializing] = false;
+                  return delegate;
             }
-        });
-        copy.author = Doc.CurrentUserEmail;
-        if (copyProto) {
-            Doc.GetProto(copy).context = undefined;
-            Doc.GetProto(copy).aliases = new List<Doc>([copy]);
-        } else {
-            Doc.AddDocToList(Doc.GetProto(copy)[DataSym], "aliases", copy);
-        }
-        copy.context = undefined;
-        Doc.UserDoc().defaultAclPrivate && (copy["acl-Public"] = "Not Shared");
-        if (retitle) {
-            copy.title = incrementTitleCopy(StrCast(copy.title));
-        }
-        return copy;
-    }
-
-
-    export function MakeDelegate(doc: Doc, id?: string, title?: string): Doc;
-    export function MakeDelegate(doc: Opt<Doc>, id?: string, title?: string): Opt<Doc>;
-    export function MakeDelegate(doc: Opt<Doc>, id?: string, title?: string): Opt<Doc> {
-        if (doc) {
+            return undefined;
+      }
+
+      // Makes a delegate of a document by first creating a delegate where data should be stored
+      // (ie, the 'data' doc), and then creates another delegate of that (ie, the 'layout' doc).
+      // This is appropriate if you're trying to create a document that behaves like all
+      // regularly created documents (e.g, text docs, pdfs, etc which all have data/layout docs)
+      export function MakeDelegateWithProto(doc: Doc, id?: string, title?: string): Doc {
+            const delegateProto = new Doc();
+            delegateProto[Initializing] = true;
+            delegateProto.proto = doc;
+            delegateProto.author = Doc.CurrentUserEmail;
+            delegateProto.isPrototype = true;
+            title && (delegateProto.title = title);
             const delegate = new Doc(id, true);
             delegate[Initializing] = true;
-            delegate.proto = doc;
+            delegate.proto = delegateProto;
             delegate.author = Doc.CurrentUserEmail;
-            if (!Doc.IsSystem(doc)) Doc.AddDocToList(doc[DataSym], "aliases", delegate);
-            title && (delegate.title = title);
+            Doc.AddDocToList(delegateProto[DataSym], "aliases", delegate);
             delegate[Initializing] = false;
+            delegateProto[Initializing] = false;
             return delegate;
-        }
-        return undefined;
-    }
-
-    // Makes a delegate of a document by first creating a delegate where data should be stored
-    // (ie, the 'data' doc), and then creates another delegate of that (ie, the 'layout' doc).
-    // This is appropriate if you're trying to create a document that behaves like all
-    // regularly created documents (e.g, text docs, pdfs, etc which all have data/layout docs)
-    export function MakeDelegateWithProto(doc: Doc, id?: string, title?: string): Doc {
-        const delegateProto = new Doc();
-        delegateProto[Initializing] = true;
-        delegateProto.proto = doc;
-        delegateProto.author = Doc.CurrentUserEmail;
-        delegateProto.isPrototype = true;
-        title && (delegateProto.title = title);
-        const delegate = new Doc(id, true);
-        delegate[Initializing] = true;
-        delegate.proto = delegateProto;
-        delegate.author = Doc.CurrentUserEmail;
-        Doc.AddDocToList(delegateProto[DataSym], "aliases", delegate);
-        delegate[Initializing] = false;
-        delegateProto[Initializing] = false;
-        return delegate;
-    }
-
-    let _applyCount: number = 0;
-    export function ApplyTemplate(templateDoc: Doc) {
-        if (templateDoc) {
-            const proto = new Doc();
-            proto.author = Doc.CurrentUserEmail;
-            const target = Doc.MakeDelegate(proto);
-            const targetKey = StrCast(templateDoc.layoutKey, "layout");
-            const applied = ApplyTemplateTo(templateDoc, target, targetKey, templateDoc.title + "(..." + _applyCount++ + ")");
-            target.layoutKey = targetKey;
-            applied && (Doc.GetProto(applied).type = templateDoc.type);
-            Doc.UserDoc().defaultAclPrivate && (applied["acl-Public"] = "Not Shared");
-            return applied;
-        }
-        return undefined;
-    }
-    export function ApplyTemplateTo(templateDoc: Doc, target: Doc, targetKey: string, titleTarget: string | undefined) {
-        if (!Doc.AreProtosEqual(target[targetKey] as Doc, templateDoc)) {
-            if (target.resolvedDataDoc) {
-                target[targetKey] = new PrefetchProxy(templateDoc);
-            } else {
-                titleTarget && (Doc.GetProto(target).title = titleTarget);
-                const setDoc = [AclAdmin, AclEdit].includes(GetEffectiveAcl(Doc.GetProto(target))) ? Doc.GetProto(target) : target;
-                setDoc[targetKey] = new PrefetchProxy(templateDoc);
-            }
-        }
-        return target;
-    }
-
-    //
-    //  This function converts a generic field layout display into a field layout that displays a specific
-    // metadata field indicated by the title of the template field (not the default field that it was rendering)
-    //
-    export function MakeMetadataFieldTemplate(templateField: Doc, templateDoc: Opt<Doc>): boolean {
-
-        // find the metadata field key that this template field doc will display (indicated by its title)
-        const metadataFieldKey = StrCast(templateField.isTemplateForField) || StrCast(templateField.title).replace(/^-/, "");
-
-        // update the original template to mark it as a template
-        templateField.isTemplateForField = metadataFieldKey;
-        templateField.title = metadataFieldKey;
-
-        const templateFieldValue = templateField[metadataFieldKey] || templateField[Doc.LayoutFieldKey(templateField)];
-        const templateCaptionValue = templateField.caption;
-        // move any data that the template field had been rendering over to the template doc so that things will still be rendered
-        // when the template field is adjusted to point to the new metadatafield key.
-        // note 1: if the template field contained a list of documents, each of those documents will be converted to templates as well.
-        // note 2: this will not overwrite any field that already exists on the template doc at the field key
-        if (!templateDoc?.[metadataFieldKey] && templateFieldValue instanceof ObjectField) {
-            Cast(templateFieldValue, listSpec(Doc), [])?.map(d => d instanceof Doc && MakeMetadataFieldTemplate(d, templateDoc));
-            (Doc.GetProto(templateField)[metadataFieldKey] = ObjectField.MakeCopy(templateFieldValue));
-        }
-        // get the layout string that the template uses to specify its layout
-        const templateFieldLayoutString = StrCast(Doc.LayoutField(Doc.Layout(templateField)));
-
-        // change it to render the target metadata field instead of what it was rendering before and assign it to the template field layout document.
-        Doc.Layout(templateField).layout = templateFieldLayoutString.replace(/fieldKey={'[^']*'}/, `fieldKey={'${metadataFieldKey}'}`);
-
-        // assign the template field doc a delegate of any extension document that was previously used to render the template field (since extension doc's carry rendering informatino)
-        Doc.Layout(templateField)[metadataFieldKey + "_ext"] = Doc.MakeDelegate(templateField[templateFieldLayoutString?.split("'")[1] + "_ext"] as Doc);
-
-        return true;
-    }
-
-
-    // converts a document id to a url path on the server
-    export function globalServerPath(doc: Doc | string = ""): string {
-        return Utils.prepend("/doc/" + (doc instanceof Doc ? doc[Id] : doc));
-    }
-    // converts a document id to a url path on the server
-    export function localServerPath(doc?: Doc): string {
-        return "/doc/" + (doc ? doc[Id] : "");
-    }
-
-    export function overlapping(doc1: Doc, doc2: Doc, clusterDistance: number) {
-        const doc2Layout = Doc.Layout(doc2);
-        const doc1Layout = Doc.Layout(doc1);
-        const x2 = NumCast(doc2.x) - clusterDistance;
-        const y2 = NumCast(doc2.y) - clusterDistance;
-        const w2 = NumCast(doc2Layout._width) + clusterDistance;
-        const h2 = NumCast(doc2Layout._height) + clusterDistance;
-        const x = NumCast(doc1.x) - clusterDistance;
-        const y = NumCast(doc1.y) - clusterDistance;
-        const w = NumCast(doc1Layout._width) + clusterDistance;
-        const h = NumCast(doc1Layout._height) + clusterDistance;
-        return doc1.z === doc2.z && intersectRect({ left: x, top: y, width: w, height: h }, { left: x2, top: y2, width: w2, height: h2 });
-    }
-
-    export function isBrushedHighlightedDegree(doc: Doc) {
-        return Doc.IsHighlighted(doc) ? DocBrushStatus.highlighted : Doc.IsBrushedDegree(doc);
-    }
-
-    export class DocBrush {
-        BrushedDoc: ObservableMap<Doc, boolean> = new ObservableMap();
-        SearchMatchDoc: ObservableMap<Doc, { searchMatch: number }> = new ObservableMap();
-    }
-    const brushManager = new DocBrush();
-
-    export class DocData {
-        @observable _user_doc: Doc = undefined!;
-        @observable _sharing_doc: Doc = undefined!;
-        @observable _searchQuery: string = "";
-    }
-
-    // the document containing the view layout information - will be the Document itself unless the Document has
-    // a layout field or 'layout' is given.
-    export function Layout(doc: Doc, layout?: Doc): Doc {
-        const overrideLayout = layout && Cast(doc[`${StrCast(layout.isTemplateForField, "data")}-layout[` + layout[Id] + "]"], Doc, null);
-        return overrideLayout || doc[LayoutSym] || doc;
-    }
-    export function SetLayout(doc: Doc, layout: Doc | string) { doc[StrCast(doc.layoutKey, "layout")] = layout; }
-    export function LayoutField(doc: Doc) { return doc[StrCast(doc.layoutKey, "layout")]; }
-    export function LayoutFieldKey(doc: Doc): string { return StrCast(Doc.Layout(doc).layout).split("'")[1]; }
-    export function NativeAspect(doc: Doc, dataDoc?: Doc, useDim?: boolean) {
-        return Doc.NativeWidth(doc, dataDoc, useDim) / (Doc.NativeHeight(doc, dataDoc, useDim) || 1);
-    }
-    export function NativeWidth(doc?: Doc, dataDoc?: Doc, useWidth?: boolean) { return !doc ? 0 : NumCast(doc._nativeWidth, NumCast((dataDoc || doc)[Doc.LayoutFieldKey(doc) + "-nativeWidth"], useWidth ? doc[WidthSym]() : 0)); }
-    export function NativeHeight(doc?: Doc, dataDoc?: Doc, useHeight?: boolean) {
-        const dheight = doc ? NumCast((dataDoc || doc)[Doc.LayoutFieldKey(doc) + "-nativeHeight"], useHeight ? doc[HeightSym]() : 0) : 0;
-        const nheight = doc ? Doc.NativeWidth(doc, dataDoc, useHeight) * doc[HeightSym]() / doc[WidthSym]() : 0;
-        return !doc ? 0 : NumCast(doc._nativeHeight, nheight || dheight);
-    }
-    export function SetNativeWidth(doc: Doc, width: number | undefined, fieldKey?: string) { doc[(fieldKey ?? Doc.LayoutFieldKey(doc)) + "-nativeWidth"] = width; }
-    export function SetNativeHeight(doc: Doc, height: number | undefined, fieldKey?: string) { doc[(fieldKey ?? Doc.LayoutFieldKey(doc)) + "-nativeHeight"] = height; }
-
-
-    const manager = new DocData();
-    export function SearchQuery(): string { return manager._searchQuery; }
-    export function SetSearchQuery(query: string) { runInAction(() => manager._searchQuery = query); }
-    export function UserDoc(): Doc { return manager._user_doc; }
-    export function SharingDoc(): Doc { return Cast(Doc.UserDoc().mySharedDocs, Doc, null); }
-    export function LinkDBDoc(): Doc { return Cast(Doc.UserDoc().myLinkDatabase, Doc, null); }
-    export function SetUserDoc(doc: Doc) { return (manager._user_doc = doc); }
-
-    const isSearchMatchCache = computedFn(function IsSearchMatch(doc: Doc) {
-        return brushManager.SearchMatchDoc.has(doc) ? brushManager.SearchMatchDoc.get(doc) :
-            brushManager.SearchMatchDoc.has(Doc.GetProto(doc)) ? brushManager.SearchMatchDoc.get(Doc.GetProto(doc)) : undefined;
-    });
-    export function IsSearchMatch(doc: Doc) { return isSearchMatchCache(doc); }
-    export function IsSearchMatchUnmemoized(doc: Doc) {
-        return brushManager.SearchMatchDoc.has(doc) ? brushManager.SearchMatchDoc.get(doc) :
-            brushManager.SearchMatchDoc.has(Doc.GetProto(doc)) ? brushManager.SearchMatchDoc.get(Doc.GetProto(doc)) : undefined;
-    }
-    export function SetSearchMatch(doc: Doc, results: { searchMatch: number }) {
-        if (doc && GetEffectiveAcl(doc) !== AclPrivate && GetEffectiveAcl(Doc.GetProto(doc)) !== AclPrivate) {
-            brushManager.SearchMatchDoc.set(doc, results);
-        }
-        return doc;
-    }
-    export function SearchMatchNext(doc: Doc, backward: boolean) {
-        if (!doc || GetEffectiveAcl(doc) === AclPrivate || GetEffectiveAcl(Doc.GetProto(doc)) === AclPrivate) return doc;
-        const result = brushManager.SearchMatchDoc.get(doc);
-        const num = Math.abs(result?.searchMatch || 0) + 1;
-        runInAction(() => result && brushManager.SearchMatchDoc.set(doc, { searchMatch: backward ? -num : num }));
-        return doc;
-    }
-    export function ClearSearchMatches() {
-        brushManager.SearchMatchDoc.clear();
-    }
-
-    const isBrushedCache = computedFn(function IsBrushed(doc: Doc) { return brushManager.BrushedDoc.has(doc) || brushManager.BrushedDoc.has(Doc.GetProto(doc)); });
-    export function IsBrushed(doc: Doc) { return isBrushedCache(doc); }
-
-    export enum DocBrushStatus {
-        unbrushed = 0,
-        protoBrushed = 1,
-        selfBrushed = 2,
-        highlighted = 3
-    }
-    // don't bother memoizing (caching) the result if called from a non-reactive context. (plus this avoids a warning message)
-    export function IsBrushedDegreeUnmemoized(doc: Doc) {
-        if (!doc || GetEffectiveAcl(doc) === AclPrivate || GetEffectiveAcl(Doc.GetProto(doc)) === AclPrivate) return DocBrushStatus.unbrushed;
-        return brushManager.BrushedDoc.has(doc) ? DocBrushStatus.selfBrushed : brushManager.BrushedDoc.has(Doc.GetProto(doc)) ? DocBrushStatus.protoBrushed : DocBrushStatus.unbrushed;
-    }
-    export function IsBrushedDegree(doc: Doc) {
-        return computedFn(function IsBrushDegree(doc: Doc) {
-            return Doc.IsBrushedDegreeUnmemoized(doc);
-        })(doc);
-    }
-    export function BrushDoc(doc: Doc) {
-        if (!doc || GetEffectiveAcl(doc) === AclPrivate || GetEffectiveAcl(Doc.GetProto(doc)) === AclPrivate) return doc;
-        brushManager.BrushedDoc.set(doc, true);
-        brushManager.BrushedDoc.set(Doc.GetProto(doc), true);
-        return doc;
-    }
-    export function UnBrushDoc(doc: Doc) {
-        if (!doc || GetEffectiveAcl(doc) === AclPrivate || GetEffectiveAcl(Doc.GetProto(doc)) === AclPrivate) return doc;
-        brushManager.BrushedDoc.delete(doc);
-        brushManager.BrushedDoc.delete(Doc.GetProto(doc));
-        return doc;
-    }
-
-    export function LinkEndpoint(linkDoc: Doc, anchorDoc: Doc) {
-        return Doc.AreProtosEqual(anchorDoc, (linkDoc.anchor1 as Doc).annotationOn as Doc) ||
-            Doc.AreProtosEqual(anchorDoc, linkDoc.anchor1 as Doc) ? "1" : "2";
-    }
-
-    export function linkFollowUnhighlight() {
-        Doc.UnhighlightAll();
-        document.removeEventListener("pointerdown", linkFollowUnhighlight);
-    }
-
-    let _lastDate = 0;
-    export function linkFollowHighlight(destDoc: Doc, dataAndDisplayDocs = true) {
-        linkFollowUnhighlight();
-        Doc.HighlightDoc(destDoc, dataAndDisplayDocs);
-        document.removeEventListener("pointerdown", linkFollowUnhighlight);
-        document.addEventListener("pointerdown", linkFollowUnhighlight);
-        const lastDate = _lastDate = Date.now();
-        window.setTimeout(() => _lastDate === lastDate && linkFollowUnhighlight(), 5000);
-    }
-
-    export class HighlightBrush {
-        @observable HighlightedDoc: Map<Doc, boolean> = new Map();
-    }
-    const highlightManager = new HighlightBrush();
-    export function IsHighlighted(doc: Doc) {
-        if (!doc || GetEffectiveAcl(doc) === AclPrivate || GetEffectiveAcl(Doc.GetProto(doc)) === AclPrivate) return false;
-        return highlightManager.HighlightedDoc.get(doc) || highlightManager.HighlightedDoc.get(Doc.GetProto(doc));
-    }
-    export function HighlightDoc(doc: Doc, dataAndDisplayDocs = true) {
-        runInAction(() => {
-            highlightManager.HighlightedDoc.set(doc, true);
-            dataAndDisplayDocs && highlightManager.HighlightedDoc.set(Doc.GetProto(doc), true);
-        });
-    }
-    export function UnHighlightDoc(doc: Doc) {
-        runInAction(() => {
-            highlightManager.HighlightedDoc.set(doc, false);
-            highlightManager.HighlightedDoc.set(Doc.GetProto(doc), false);
-        });
-    }
-    export function UnhighlightAll() {
-        const mapEntries = highlightManager.HighlightedDoc.keys();
-        let docEntry: IteratorResult<Doc>;
-        while (!(docEntry = mapEntries.next()).done) {
-            const targetDoc = docEntry.value;
-            targetDoc && Doc.UnHighlightDoc(targetDoc);
-        }
-
-    }
-    export function UnBrushAllDocs() {
-        brushManager.BrushedDoc.clear();
-    }
-
-    export function getDocTemplate(doc?: Doc) {
-        return !doc ? undefined :
-            doc.isTemplateDoc ? doc :
-                Cast(doc.dragFactory, Doc, null)?.isTemplateDoc ? doc.dragFactory :
-                    Cast(Doc.Layout(doc), Doc, null)?.isTemplateDoc ?
-                        (Cast(Doc.Layout(doc), Doc, null).resolvedDataDoc ? Doc.Layout(doc).proto : Doc.Layout(doc)) :
-                        undefined;
-    }
-
-    export function matchFieldValue(doc: Doc, key: string, value: any): boolean {
-        if (Utils.HasTransparencyFilter(value)) {
-            const isTransparent = (color: string) => color !== "" && (DashColor(color).alpha() !== 1);
-            return isTransparent(StrCast(doc[key]));
-        }
-        if (typeof value === "string") {
-            value = value.replace(`,${Utils.noRecursionHack}`, "");
-        }
-        const fieldVal = key === "#" ? (StrCast(doc.tags).includes(":#" + value + ":") ? StrCast(doc.tags) : undefined) : doc[key];
-        if (Cast(fieldVal, listSpec("string"), []).length) {
-            const vals = Cast(fieldVal, listSpec("string"), []);
-            const docs = vals.some(v => (v as any) instanceof Doc);
-            if (docs) return value === Field.toString(fieldVal as Field);
-            return vals.some(v => v.includes(value));  // bcz: arghh: Todo: comparison should be parameterized as exact, or substring
-        }
-        const fieldStr = Field.toString(fieldVal as Field);
-        return fieldStr.includes(value); // bcz: arghh: Todo: comparison should be parameterized as exact, or substring
-    }
-
-    export function deiconifyView(doc: Doc) {
-        StrCast(doc.layoutKey).split("_")[1] === "icon" && setNativeView(doc);
-    }
-
-    export function setNativeView(doc: any) {
-        const prevLayout = StrCast(doc.layoutKey).split("_")[1];
-        const deiconify = prevLayout === "icon" && StrCast(doc.deiconifyLayout) ? "layout_" + StrCast(doc.deiconifyLayout) : "";
-        prevLayout === "icon" && (doc.deiconifyLayout = undefined);
-        doc.layoutKey = deiconify || "layout";
-    }
-    export function setDocRangeFilter(container: Opt<Doc>, key: string, range?: number[]) {
-        if (!container) return;
-        const docRangeFilters = Cast(container._docRangeFilters, listSpec("string"), []);
-        for (let i = 0; i < docRangeFilters.length; i += 3) {
-            if (docRangeFilters[i] === key) {
-                docRangeFilters.splice(i, 3);
-                break;
+      }
+
+      let _applyCount: number = 0;
+      export function ApplyTemplate(templateDoc: Doc) {
+            if (templateDoc) {
+                  const proto = new Doc();
+                  proto.author = Doc.CurrentUserEmail;
+                  const target = Doc.MakeDelegate(proto);
+                  const targetKey = StrCast(templateDoc.layoutKey, "layout");
+                  const applied = ApplyTemplateTo(templateDoc, target, targetKey, templateDoc.title + "(..." + _applyCount++ + ")");
+                  target.layoutKey = targetKey;
+                  applied && (Doc.GetProto(applied).type = templateDoc.type);
+                  Doc.UserDoc().defaultAclPrivate && (applied["acl-Public"] = "Not Shared");
+                  return applied;
             }
-        }
-        if (range !== undefined) {
-            docRangeFilters.push(key);
-            docRangeFilters.push(range[0].toString());
-            docRangeFilters.push(range[1].toString());
-            container._docRangeFilters = new List<string>(docRangeFilters);
-        }
-    }
-
-    // filters document in a container collection:
-    // all documents with the specified value for the specified key are included/excluded
-    // based on the modifiers :"check", "x", undefined
-    export function setDocFilter(container: Opt<Doc>, key: string, value: any, modifiers: "remove" | "match" | "check" | "x" | "exists" | "unset", toggle?: boolean, fieldSuffix?: string, append: boolean = true) {
-        if (!container) return;
-        const filterField = "_" + (fieldSuffix ? fieldSuffix + "-" : "") + "docFilters";
-        const docFilters = Cast(container[filterField], listSpec("string"), []);
-        runInAction(() => {
-            for (let i = 0; i < docFilters.length; i++) {
-                const fields = docFilters[i].split(":"); // split key:value:modifier
-                if (fields[0] === key && (fields[1] === value || modifiers === "match")) {
-                    if (fields[2] === modifiers && modifiers && fields[1] === value) {
-                        if (toggle) modifiers = "remove";
-                        else return;
-                    }
-                    docFilters.splice(i, 1);
-                    container[filterField] = new List<string>(docFilters);
-                    break;
-                }
+            return undefined;
+      }
+      export function ApplyTemplateTo(templateDoc: Doc, target: Doc, targetKey: string, titleTarget: string | undefined) {
+            if (!Doc.AreProtosEqual(target[targetKey] as Doc, templateDoc)) {
+                  if (target.resolvedDataDoc) {
+                        target[targetKey] = new PrefetchProxy(templateDoc);
+                  } else {
+                        titleTarget && (Doc.GetProto(target).title = titleTarget);
+                        const setDoc = [AclAdmin, AclEdit].includes(GetEffectiveAcl(Doc.GetProto(target))) ? Doc.GetProto(target) : target;
+                        setDoc[targetKey] = new PrefetchProxy(templateDoc);
+                  }
             }
-            if (!docFilters.length && modifiers === "match" && value === undefined) {
-                container[filterField] = undefined;
-            } else if (modifiers !== "remove") {
-                !append && (docFilters.length = 0);
-                docFilters.push(key + ":" + value + ":" + modifiers);
-                container[filterField] = new List<string>(docFilters);
+            return target;
+      }
+
+      //
+      //  This function converts a generic field layout display into a field layout that displays a specific
+      // metadata field indicated by the title of the template field (not the default field that it was rendering)
+      //
+      export function MakeMetadataFieldTemplate(templateField: Doc, templateDoc: Opt<Doc>): boolean {
+
+            // find the metadata field key that this template field doc will display (indicated by its title)
+            const metadataFieldKey = StrCast(templateField.isTemplateForField) || StrCast(templateField.title).replace(/^-/, "");
+
+            // update the original template to mark it as a template
+            templateField.isTemplateForField = metadataFieldKey;
+            templateField.title = metadataFieldKey;
+
+            const templateFieldValue = templateField[metadataFieldKey] || templateField[Doc.LayoutFieldKey(templateField)];
+            const templateCaptionValue = templateField.caption;
+            // move any data that the template field had been rendering over to the template doc so that things will still be rendered
+            // when the template field is adjusted to point to the new metadatafield key.
+            // note 1: if the template field contained a list of documents, each of those documents will be converted to templates as well.
+            // note 2: this will not overwrite any field that already exists on the template doc at the field key
+            if (!templateDoc?.[metadataFieldKey] && templateFieldValue instanceof ObjectField) {
+                  Cast(templateFieldValue, listSpec(Doc), [])?.map(d => d instanceof Doc && MakeMetadataFieldTemplate(d, templateDoc));
+                  (Doc.GetProto(templateField)[metadataFieldKey] = ObjectField.MakeCopy(templateFieldValue));
             }
-        });
-    }
-    export function readDocRangeFilter(doc: Doc, key: string) {
-        const docRangeFilters = Cast(doc._docRangeFilters, listSpec("string"), []);
-        for (let i = 0; i < docRangeFilters.length; i += 3) {
-            if (docRangeFilters[i] === key) {
-                return [Number(docRangeFilters[i + 1]), Number(docRangeFilters[i + 2])];
+            // get the layout string that the template uses to specify its layout
+            const templateFieldLayoutString = StrCast(Doc.LayoutField(Doc.Layout(templateField)));
+
+            // change it to render the target metadata field instead of what it was rendering before and assign it to the template field layout document.
+            Doc.Layout(templateField).layout = templateFieldLayoutString.replace(/fieldKey={'[^']*'}/, `fieldKey={'${metadataFieldKey}'}`);
+
+            // assign the template field doc a delegate of any extension document that was previously used to render the template field (since extension doc's carry rendering informatino)
+            Doc.Layout(templateField)[metadataFieldKey + "_ext"] = Doc.MakeDelegate(templateField[templateFieldLayoutString?.split("'")[1] + "_ext"] as Doc);
+
+            return true;
+      }
+
+
+      // converts a document id to a url path on the server
+      export function globalServerPath(doc: Doc | string = ""): string {
+            return Utils.prepend("/doc/" + (doc instanceof Doc ? doc[Id] : doc));
+      }
+      // converts a document id to a url path on the server
+      export function localServerPath(doc?: Doc): string {
+            return "/doc/" + (doc ? doc[Id] : "");
+      }
+
+      export function overlapping(doc1: Doc, doc2: Doc, clusterDistance: number) {
+            const doc2Layout = Doc.Layout(doc2);
+            const doc1Layout = Doc.Layout(doc1);
+            const x2 = NumCast(doc2.x) - clusterDistance;
+            const y2 = NumCast(doc2.y) - clusterDistance;
+            const w2 = NumCast(doc2Layout._width) + clusterDistance;
+            const h2 = NumCast(doc2Layout._height) + clusterDistance;
+            const x = NumCast(doc1.x) - clusterDistance;
+            const y = NumCast(doc1.y) - clusterDistance;
+            const w = NumCast(doc1Layout._width) + clusterDistance;
+            const h = NumCast(doc1Layout._height) + clusterDistance;
+            return doc1.z === doc2.z && intersectRect({ left: x, top: y, width: w, height: h }, { left: x2, top: y2, width: w2, height: h2 });
+      }
+
+      export function isBrushedHighlightedDegree(doc: Doc) {
+            return Doc.IsHighlighted(doc) ? DocBrushStatus.highlighted : Doc.IsBrushedDegree(doc);
+      }
+
+      export class DocBrush {
+            BrushedDoc: ObservableMap<Doc, boolean> = new ObservableMap();
+            SearchMatchDoc: ObservableMap<Doc, { searchMatch: number }> = new ObservableMap();
+      }
+      const brushManager = new DocBrush();
+
+      export class DocData {
+            @observable _user_doc: Doc = undefined!;
+            @observable _sharing_doc: Doc = undefined!;
+            @observable _searchQuery: string = "";
+      }
+
+      // the document containing the view layout information - will be the Document itself unless the Document has
+      // a layout field or 'layout' is given.
+      export function Layout(doc: Doc, layout?: Doc): Doc {
+            const overrideLayout = layout && Cast(doc[`${StrCast(layout.isTemplateForField, "data")}-layout[` + layout[Id] + "]"], Doc, null);
+            return overrideLayout || doc[LayoutSym] || doc;
+      }
+      export function SetLayout(doc: Doc, layout: Doc | string) { doc[StrCast(doc.layoutKey, "layout")] = layout; }
+      export function LayoutField(doc: Doc) { return doc[StrCast(doc.layoutKey, "layout")]; }
+      export function LayoutFieldKey(doc: Doc): string { return StrCast(Doc.Layout(doc).layout).split("'")[1]; }
+      export function NativeAspect(doc: Doc, dataDoc?: Doc, useDim?: boolean) {
+            return Doc.NativeWidth(doc, dataDoc, useDim) / (Doc.NativeHeight(doc, dataDoc, useDim) || 1);
+      }
+      export function NativeWidth(doc?: Doc, dataDoc?: Doc, useWidth?: boolean) { return !doc ? 0 : NumCast(doc._nativeWidth, NumCast((dataDoc || doc)[Doc.LayoutFieldKey(doc) + "-nativeWidth"], useWidth ? doc[WidthSym]() : 0)); }
+      export function NativeHeight(doc?: Doc, dataDoc?: Doc, useHeight?: boolean) {
+            const dheight = doc ? NumCast((dataDoc || doc)[Doc.LayoutFieldKey(doc) + "-nativeHeight"], useHeight ? doc[HeightSym]() : 0) : 0;
+            const nheight = doc ? Doc.NativeWidth(doc, dataDoc, useHeight) * doc[HeightSym]() / doc[WidthSym]() : 0;
+            return !doc ? 0 : NumCast(doc._nativeHeight, nheight || dheight);
+      }
+      export function SetNativeWidth(doc: Doc, width: number | undefined, fieldKey?: string) { doc[(fieldKey ?? Doc.LayoutFieldKey(doc)) + "-nativeWidth"] = width; }
+      export function SetNativeHeight(doc: Doc, height: number | undefined, fieldKey?: string) { doc[(fieldKey ?? Doc.LayoutFieldKey(doc)) + "-nativeHeight"] = height; }
+
+
+      const manager = new DocData();
+      export function SearchQuery(): string { return manager._searchQuery; }
+      export function SetSearchQuery(query: string) { runInAction(() => manager._searchQuery = query); }
+      export function UserDoc(): Doc { return manager._user_doc; }
+      export function SharingDoc(): Doc { return Cast(Doc.UserDoc().mySharedDocs, Doc, null); }
+      export function LinkDBDoc(): Doc { return Cast(Doc.UserDoc().myLinkDatabase, Doc, null); }
+      export function SetUserDoc(doc: Doc) { return (manager._user_doc = doc); }
+
+      const isSearchMatchCache = computedFn(function IsSearchMatch(doc: Doc) {
+            return brushManager.SearchMatchDoc.has(doc) ? brushManager.SearchMatchDoc.get(doc) :
+                  brushManager.SearchMatchDoc.has(Doc.GetProto(doc)) ? brushManager.SearchMatchDoc.get(Doc.GetProto(doc)) : undefined;
+      });
+      export function IsSearchMatch(doc: Doc) { return isSearchMatchCache(doc); }
+      export function IsSearchMatchUnmemoized(doc: Doc) {
+            return brushManager.SearchMatchDoc.has(doc) ? brushManager.SearchMatchDoc.get(doc) :
+                  brushManager.SearchMatchDoc.has(Doc.GetProto(doc)) ? brushManager.SearchMatchDoc.get(Doc.GetProto(doc)) : undefined;
+      }
+      export function SetSearchMatch(doc: Doc, results: { searchMatch: number }) {
+            if (doc && GetEffectiveAcl(doc) !== AclPrivate && GetEffectiveAcl(Doc.GetProto(doc)) !== AclPrivate) {
+                  brushManager.SearchMatchDoc.set(doc, results);
             }
-        }
-    }
-    export function assignDocToField(doc: Doc, field: string, id: string) {
-        DocServer.GetRefField(id).then(layout => layout instanceof Doc && (doc[field] = layout));
-        return id;
-    }
-
-    export function toggleNativeDimensions(layoutDoc: Doc, contentScale: number, panelWidth: number, panelHeight: number) {
-        runInAction(() => {
-            if (Doc.NativeWidth(layoutDoc) || Doc.NativeHeight(layoutDoc)) {
-                layoutDoc._viewScale = NumCast(layoutDoc._viewScale, 1) * contentScale;
-                layoutDoc._nativeWidth = undefined;
-                layoutDoc._nativeHeight = undefined;
+            return doc;
+      }
+      export function SearchMatchNext(doc: Doc, backward: boolean) {
+            if (!doc || GetEffectiveAcl(doc) === AclPrivate || GetEffectiveAcl(Doc.GetProto(doc)) === AclPrivate) return doc;
+            const result = brushManager.SearchMatchDoc.get(doc);
+            const num = Math.abs(result?.searchMatch || 0) + 1;
+            runInAction(() => result && brushManager.SearchMatchDoc.set(doc, { searchMatch: backward ? -num : num }));
+            return doc;
+      }
+      export function ClearSearchMatches() {
+            brushManager.SearchMatchDoc.clear();
+      }
+
+      const isBrushedCache = computedFn(function IsBrushed(doc: Doc) { return brushManager.BrushedDoc.has(doc) || brushManager.BrushedDoc.has(Doc.GetProto(doc)); });
+      export function IsBrushed(doc: Doc) { return isBrushedCache(doc); }
+
+      export enum DocBrushStatus {
+            unbrushed = 0,
+            protoBrushed = 1,
+            selfBrushed = 2,
+            highlighted = 3
+      }
+      // don't bother memoizing (caching) the result if called from a non-reactive context. (plus this avoids a warning message)
+      export function IsBrushedDegreeUnmemoized(doc: Doc) {
+            if (!doc || GetEffectiveAcl(doc) === AclPrivate || GetEffectiveAcl(Doc.GetProto(doc)) === AclPrivate) return DocBrushStatus.unbrushed;
+            return brushManager.BrushedDoc.has(doc) ? DocBrushStatus.selfBrushed : brushManager.BrushedDoc.has(Doc.GetProto(doc)) ? DocBrushStatus.protoBrushed : DocBrushStatus.unbrushed;
+      }
+      export function IsBrushedDegree(doc: Doc) {
+            return computedFn(function IsBrushDegree(doc: Doc) {
+                  return Doc.IsBrushedDegreeUnmemoized(doc);
+            })(doc);
+      }
+      export function BrushDoc(doc: Doc) {
+            if (!doc || GetEffectiveAcl(doc) === AclPrivate || GetEffectiveAcl(Doc.GetProto(doc)) === AclPrivate) return doc;
+            brushManager.BrushedDoc.set(doc, true);
+            brushManager.BrushedDoc.set(Doc.GetProto(doc), true);
+            return doc;
+      }
+      export function UnBrushDoc(doc: Doc) {
+            if (!doc || GetEffectiveAcl(doc) === AclPrivate || GetEffectiveAcl(Doc.GetProto(doc)) === AclPrivate) return doc;
+            brushManager.BrushedDoc.delete(doc);
+            brushManager.BrushedDoc.delete(Doc.GetProto(doc));
+            return doc;
+      }
+
+      export function LinkEndpoint(linkDoc: Doc, anchorDoc: Doc) {
+            return Doc.AreProtosEqual(anchorDoc, (linkDoc.anchor1 as Doc).annotationOn as Doc) ||
+                  Doc.AreProtosEqual(anchorDoc, linkDoc.anchor1 as Doc) ? "1" : "2";
+      }
+
+      export function linkFollowUnhighlight() {
+            Doc.UnhighlightAll();
+            document.removeEventListener("pointerdown", linkFollowUnhighlight);
+      }
+
+      let _lastDate = 0;
+      export function linkFollowHighlight(destDoc: Doc, dataAndDisplayDocs = true) {
+            linkFollowUnhighlight();
+            Doc.HighlightDoc(destDoc, dataAndDisplayDocs);
+            document.removeEventListener("pointerdown", linkFollowUnhighlight);
+            document.addEventListener("pointerdown", linkFollowUnhighlight);
+            const lastDate = _lastDate = Date.now();
+            window.setTimeout(() => _lastDate === lastDate && linkFollowUnhighlight(), 5000);
+      }
+
+      export class HighlightBrush {
+            @observable HighlightedDoc: Map<Doc, boolean> = new Map();
+      }
+      const highlightManager = new HighlightBrush();
+      export function IsHighlighted(doc: Doc) {
+            if (!doc || GetEffectiveAcl(doc) === AclPrivate || GetEffectiveAcl(Doc.GetProto(doc)) === AclPrivate) return false;
+            return highlightManager.HighlightedDoc.get(doc) || highlightManager.HighlightedDoc.get(Doc.GetProto(doc));
+      }
+      export function HighlightDoc(doc: Doc, dataAndDisplayDocs = true) {
+            runInAction(() => {
+                  highlightManager.HighlightedDoc.set(doc, true);
+                  dataAndDisplayDocs && highlightManager.HighlightedDoc.set(Doc.GetProto(doc), true);
+            });
+      }
+      export function UnHighlightDoc(doc: Doc) {
+            runInAction(() => {
+                  highlightManager.HighlightedDoc.set(doc, false);
+                  highlightManager.HighlightedDoc.set(Doc.GetProto(doc), false);
+            });
+      }
+      export function UnhighlightAll() {
+            const mapEntries = highlightManager.HighlightedDoc.keys();
+            let docEntry: IteratorResult<Doc>;
+            while (!(docEntry = mapEntries.next()).done) {
+                  const targetDoc = docEntry.value;
+                  targetDoc && Doc.UnHighlightDoc(targetDoc);
             }
-            else {
-                layoutDoc._autoHeight = false;
-                if (!Doc.NativeWidth(layoutDoc)) {
-                    layoutDoc._nativeWidth = NumCast(layoutDoc._width, panelWidth);
-                    layoutDoc._nativeHeight = NumCast(layoutDoc._height, panelHeight);
-                }
+
+      }
+      export function UnBrushAllDocs() {
+            brushManager.BrushedDoc.clear();
+      }
+
+      export function getDocTemplate(doc?: Doc) {
+            return !doc ? undefined :
+                  doc.isTemplateDoc ? doc :
+                        Cast(doc.dragFactory, Doc, null)?.isTemplateDoc ? doc.dragFactory :
+                              Cast(Doc.Layout(doc), Doc, null)?.isTemplateDoc ?
+                                    (Cast(Doc.Layout(doc), Doc, null).resolvedDataDoc ? Doc.Layout(doc).proto : Doc.Layout(doc)) :
+                                    undefined;
+      }
+
+      export function matchFieldValue(doc: Doc, key: string, value: any): boolean {
+            if (Utils.HasTransparencyFilter(value)) {
+                  const isTransparent = (color: string) => color !== "" && (DashColor(color).alpha() !== 1);
+                  return isTransparent(StrCast(doc[key]));
             }
-        });
-    }
-
-    export function isDocPinned(doc: Doc) {
-        //add this new doc to props.Document
-        const curPres = Cast(Doc.UserDoc().activePresentation, Doc) as Doc;
-        return !curPres ? false : DocListCast(curPres.data).findIndex((val) => Doc.AreProtosEqual(val, doc)) !== -1;
-    }
-
-    export function copyDragFactory(dragFactory: Doc) {
-        const ndoc = dragFactory.isTemplateDoc ? Doc.ApplyTemplate(dragFactory) : Doc.MakeCopy(dragFactory, true);
-        ndoc && Doc.AddDocToList(Cast(Doc.UserDoc().myFileOrphans, Doc, null), "data", Doc.GetProto(ndoc));
-        if (ndoc && dragFactory["dragFactory-count"] !== undefined) {
-            dragFactory["dragFactory-count"] = NumCast(dragFactory["dragFactory-count"]) + 1;
-            Doc.SetInPlace(ndoc, "title", ndoc.title + " " + NumCast(dragFactory["dragFactory-count"]).toString(), true);
-        }
-
-        if (ndoc) inheritParentAcls(CurrentUserUtils.ActiveDashboard, ndoc);
-
-        return ndoc;
-    }
-    export function delegateDragFactory(dragFactory: Doc) {
-        const ndoc = Doc.MakeDelegateWithProto(dragFactory);
-        if (ndoc && dragFactory["dragFactory-count"] !== undefined) {
-            dragFactory["dragFactory-count"] = NumCast(dragFactory["dragFactory-count"]) + 1;
-            Doc.GetProto(ndoc).title = ndoc.title + " " + NumCast(dragFactory["dragFactory-count"]).toString();
-        }
-        return ndoc;
-    }
-
-    export function toIcon(doc?: Doc, isOpen?: boolean) {
-        switch (StrCast(doc?.type)) {
-            case DocumentType.IMG: return "image";
-            case DocumentType.COMPARISON: return "columns";
-            case DocumentType.RTF: return "sticky-note";
-            case DocumentType.COL:
-                const folder: IconProp = isOpen ? "folder-open" : "folder";
-                const chevron: IconProp = isOpen ? "chevron-down" : "chevron-right";
-                return !doc?.isFolder ? folder : chevron;
-            case DocumentType.WEB: return "globe-asia";
-            case DocumentType.SCREENSHOT: return "photo-video";
-            case DocumentType.WEBCAM: return "video";
-            case DocumentType.AUDIO: return "microphone";
-            case DocumentType.BUTTON: return "bolt";
-            case DocumentType.PRES: return "tv";
-            case DocumentType.SCRIPTING: return "terminal";
-            case DocumentType.IMPORT: return "cloud-upload-alt";
-            case DocumentType.VID: return "video";
-            case DocumentType.INK: return "pen-nib";
-            case DocumentType.PDF: return "file-pdf";
-            case DocumentType.LINK: return "link";
-            case DocumentType.MAP: return "map-marker-alt";
-            default: return "question";
-        }
-    }
-
-
-    export namespace Get {
-
-        const primitives = ["string", "number", "boolean"];
-
-        export interface JsonConversionOpts {
-            data: any;
-            title?: string;
-            appendToExisting?: { targetDoc: Doc, fieldKey?: string };
-            excludeEmptyObjects?: boolean;
-        }
-
-        const defaultKey = "json";
-
-        /**
-         * This function takes any valid JSON(-like) data, i.e. parsed or unparsed, and at arbitrarily
-         * deep levels of nesting, converts the data and structure into nested documents with the appropriate fields.
-         *
-         * After building a hierarchy within / below a top-level document, it then returns that top-level parent.
-         *
-         * If we've received a string, treat it like valid JSON and try to parse it into an object. If this fails, the
-         * string is invalid JSON, so we should assume that the input is the result of a JSON.parse()
-         * call that returned a regular string value to be stored as a Field.
-         *
-         * If we've received something other than a string, since the caller might also pass in the results of a
-         * JSON.parse() call, valid input might be an object, an array (still typeof object), a boolean or a number.
-         * Anything else (like a function, etc. passed in naively as any) is meaningless for this operation.
-         *
-         * All TS/JS objects get converted directly to documents, directly preserving the key value structure. Everything else,
-         * lacking the key value structure, gets stored as a field in a wrapper document.
-         *
-         * @param data for convenience and flexibility, either a valid JSON string to be parsed,
-         * or the result of any JSON.parse() call.
-         * @param title an optional title to give to the highest parent document in the hierarchy.
-         * If whether this function creates a new document or appendToExisting is specified and that document already has a title,
-         * because this title field can be left undefined for the opposite behavior, including a title will overwrite the existing title.
-         * @param appendToExisting **if specified**, there are two cases, both of which return the target document:
-         *
-         * 1) the json to be converted can be represented as a document, in which case the target document will act as the root
-         * of the tree and receive all the conversion results as new fields on itself
-         * 2) the json can't be represented as a document, in which case the function will assign the field-level conversion
-         * results to either the specified key on the target document, or to its "json" key by default.
-         *
-         * If not specified, the function creates and returns a new entirely generic document (different from the Doc.Create calls)
-         * to act as the root of the tree.
-         *
-         * One might choose to specify this field if you want to write to a document returned from a Document.Create function call,
-         * say a TreeView document that will be rendered, not just an untyped, identityless doc that would otherwise be created
-         * from a default call to new Doc.
-         *
-         * @param excludeEmptyObjects whether non-primitive objects (TypeScript objects and arrays) should be converted even
-         * if they contain no data. By default, empty objects and arrays are ignored.
-         */
-        export function FromJson({ data, title, appendToExisting, excludeEmptyObjects }: JsonConversionOpts): Opt<Doc> {
-            if (excludeEmptyObjects === undefined) {
-                excludeEmptyObjects = true;
+            if (typeof value === "string") {
+                  value = value.replace(`,${Utils.noRecursionHack}`, "");
             }
-            if (data === undefined || data === null || ![...primitives, "object"].includes(typeof data)) {
-                return undefined;
+            const fieldVal = key === "#" ? (StrCast(doc.tags).includes(":#" + value + ":") ? StrCast(doc.tags) : undefined) : doc[key];
+            if (Cast(fieldVal, listSpec("string"), []).length) {
+                  const vals = Cast(fieldVal, listSpec("string"), []);
+                  const docs = vals.some(v => (v as any) instanceof Doc);
+                  if (docs) return value === Field.toString(fieldVal as Field);
+                  return vals.some(v => v.includes(value));  // bcz: arghh: Todo: comparison should be parameterized as exact, or substring
             }
-            let resolved: any;
-            try {
-                resolved = JSON.parse(typeof data === "string" ? data : JSON.stringify(data));
-            } catch (e) {
-                return undefined;
+            const fieldStr = Field.toString(fieldVal as Field);
+            return fieldStr.includes(value); // bcz: arghh: Todo: comparison should be parameterized as exact, or substring
+      }
+
+      export function deiconifyView(doc: Doc) {
+            StrCast(doc.layoutKey).split("_")[1] === "icon" && setNativeView(doc);
+      }
+
+      export function setNativeView(doc: any) {
+            const prevLayout = StrCast(doc.layoutKey).split("_")[1];
+            const deiconify = prevLayout === "icon" && StrCast(doc.deiconifyLayout) ? "layout_" + StrCast(doc.deiconifyLayout) : "";
+            prevLayout === "icon" && (doc.deiconifyLayout = undefined);
+            doc.layoutKey = deiconify || "layout";
+      }
+      export function setDocRangeFilter(container: Opt<Doc>, key: string, range?: number[]) {
+            if (!container) return;
+            const docRangeFilters = Cast(container._docRangeFilters, listSpec("string"), []);
+            for (let i = 0; i < docRangeFilters.length; i += 3) {
+                  if (docRangeFilters[i] === key) {
+                        docRangeFilters.splice(i, 3);
+                        break;
+                  }
             }
-            let output: Opt<Doc>;
-            if (typeof resolved === "object" && !(resolved instanceof Array)) {
-                output = convertObject(resolved, excludeEmptyObjects, title, appendToExisting?.targetDoc);
-            } else {
-                // give the proper types to the data extracted from the JSON
-                const result = toField(resolved, excludeEmptyObjects);
-                if (appendToExisting) {
-                    (output = appendToExisting.targetDoc)[appendToExisting.fieldKey || defaultKey] = result;
-                } else {
-                    (output = new Doc).json = result;
-                }
+            if (range !== undefined) {
+                  docRangeFilters.push(key);
+                  docRangeFilters.push(range[0].toString());
+                  docRangeFilters.push(range[1].toString());
+                  container._docRangeFilters = new List<string>(docRangeFilters);
             }
-            title && output && (output.title = title);
-            return output;
-        }
-
-        /**
-         * For each value of the object, recursively convert it to its appropriate field value
-         * and store the field at the appropriate key in the document if it is not undefined
-         * @param object the object to convert
-         * @returns the object mapped from JSON to field values, where each mapping
-         * might involve arbitrary recursion (since toField might itself call convertObject)
-         */
-        const convertObject = (object: any, excludeEmptyObjects: boolean, title?: string, target?: Doc): Opt<Doc> => {
-            const hasEntries = Object.keys(object).length;
-            if (hasEntries || !excludeEmptyObjects) {
-                const resolved = target ?? new Doc;
-                if (hasEntries) {
-                    let result: Opt<Field>;
-                    Object.keys(object).map(key => {
-                        // if excludeEmptyObjects is true, any qualifying conversions from toField will
-                        // be undefined, and thus the results that would have
-                        // otherwise been empty (List or Doc)s will just not be written
-                        if (result = toField(object[key], excludeEmptyObjects, key)) {
-                            resolved[key] = result;
+      }
+
+      // filters document in a container collection:
+      // all documents with the specified value for the specified key are included/excluded
+      // based on the modifiers :"check", "x", undefined
+      export function setDocFilter(container: Opt<Doc>, key: string, value: any, modifiers: "remove" | "match" | "check" | "x" | "exists" | "unset", toggle?: boolean, fieldSuffix?: string, append: boolean = true) {
+            if (!container) return;
+            const filterField = "_" + (fieldSuffix ? fieldSuffix + "-" : "") + "docFilters";
+            const docFilters = Cast(container[filterField], listSpec("string"), []);
+            runInAction(() => {
+                  for (let i = 0; i < docFilters.length; i++) {
+                        const fields = docFilters[i].split(":"); // split key:value:modifier
+                        if (fields[0] === key && (fields[1] === value || modifiers === "match")) {
+                              if (fields[2] === modifiers && modifiers && fields[1] === value) {
+                                    if (toggle) modifiers = "remove";
+                                    else return;
+                              }
+                              docFilters.splice(i, 1);
+                              container[filterField] = new List<string>(docFilters);
+                              break;
                         }
-                    });
-                }
-                title && (resolved.title = title);
-                return resolved;
+                  }
+                  if (!docFilters.length && modifiers === "match" && value === undefined) {
+                        container[filterField] = undefined;
+                  } else if (modifiers !== "remove") {
+                        !append && (docFilters.length = 0);
+                        docFilters.push(key + ":" + value + ":" + modifiers);
+                        container[filterField] = new List<string>(docFilters);
+                  }
+            });
+      }
+      export function readDocRangeFilter(doc: Doc, key: string) {
+            const docRangeFilters = Cast(doc._docRangeFilters, listSpec("string"), []);
+            for (let i = 0; i < docRangeFilters.length; i += 3) {
+                  if (docRangeFilters[i] === key) {
+                        return [Number(docRangeFilters[i + 1]), Number(docRangeFilters[i + 2])];
+                  }
             }
-        };
-
-        /**
-         * For each element in the list, recursively convert it to a document or other field
-         * and push the field to the list if it is not undefined
-         * @param list the list to convert
-         * @returns the list mapped from JSON to field values, where each mapping
-         * might involve arbitrary recursion (since toField might itself call convertList)
-         */
-        const convertList = (list: Array<any>, excludeEmptyObjects: boolean): Opt<List<Field>> => {
-            const target = new List();
-            let result: Opt<Field>;
-            // if excludeEmptyObjects is true, any qualifying conversions from toField will
-            // be undefined, and thus the results that would have
-            // otherwise been empty (List or Doc)s will just not be written
-            list.map(item => (result = toField(item, excludeEmptyObjects)) && target.push(result));
-            if (target.length || !excludeEmptyObjects) {
-                return target;
+      }
+      export function assignDocToField(doc: Doc, field: string, id: string) {
+            DocServer.GetRefField(id).then(layout => layout instanceof Doc && (doc[field] = layout));
+            return id;
+      }
+
+      export function toggleNativeDimensions(layoutDoc: Doc, contentScale: number, panelWidth: number, panelHeight: number) {
+            runInAction(() => {
+                  if (Doc.NativeWidth(layoutDoc) || Doc.NativeHeight(layoutDoc)) {
+                        layoutDoc._viewScale = NumCast(layoutDoc._viewScale, 1) * contentScale;
+                        layoutDoc._nativeWidth = undefined;
+                        layoutDoc._nativeHeight = undefined;
+                  }
+                  else {
+                        layoutDoc._autoHeight = false;
+                        if (!Doc.NativeWidth(layoutDoc)) {
+                              layoutDoc._nativeWidth = NumCast(layoutDoc._width, panelWidth);
+                              layoutDoc._nativeHeight = NumCast(layoutDoc._height, panelHeight);
+                        }
+                  }
+            });
+      }
+
+      export function isDocPinned(doc: Doc) {
+            //add this new doc to props.Document
+            const curPres = Cast(Doc.UserDoc().activePresentation, Doc) as Doc;
+            return !curPres ? false : DocListCast(curPres.data).findIndex((val) => Doc.AreProtosEqual(val, doc)) !== -1;
+      }
+
+      export function copyDragFactory(dragFactory: Doc) {
+            const ndoc = dragFactory.isTemplateDoc ? Doc.ApplyTemplate(dragFactory) : Doc.MakeCopy(dragFactory, true);
+            ndoc && Doc.AddDocToList(Cast(Doc.UserDoc().myFileOrphans, Doc, null), "data", Doc.GetProto(ndoc));
+            if (ndoc && dragFactory["dragFactory-count"] !== undefined) {
+                  dragFactory["dragFactory-count"] = NumCast(dragFactory["dragFactory-count"]) + 1;
+                  Doc.SetInPlace(ndoc, "title", ndoc.title + " " + NumCast(dragFactory["dragFactory-count"]).toString(), true);
             }
-        };
 
-        const toField = (data: any, excludeEmptyObjects: boolean, title?: string): Opt<Field> => {
-            if (data === null || data === undefined) {
-                return undefined;
+            if (ndoc) inheritParentAcls(CurrentUserUtils.ActiveDashboard, ndoc);
+
+            return ndoc;
+      }
+      export function delegateDragFactory(dragFactory: Doc) {
+            const ndoc = Doc.MakeDelegateWithProto(dragFactory);
+            if (ndoc && dragFactory["dragFactory-count"] !== undefined) {
+                  dragFactory["dragFactory-count"] = NumCast(dragFactory["dragFactory-count"]) + 1;
+                  Doc.GetProto(ndoc).title = ndoc.title + " " + NumCast(dragFactory["dragFactory-count"]).toString();
             }
-            if (primitives.includes(typeof data)) {
-                return data;
+            return ndoc;
+      }
+
+      export function toIcon(doc?: Doc, isOpen?: boolean) {
+            switch (StrCast(doc?.type)) {
+                  case DocumentType.IMG: return "image";
+                  case DocumentType.COMPARISON: return "columns";
+                  case DocumentType.RTF: return "sticky-note";
+                  case DocumentType.COL:
+                        const folder: IconProp = isOpen ? "folder-open" : "folder";
+                        const chevron: IconProp = isOpen ? "chevron-down" : "chevron-right";
+                        return !doc?.isFolder ? folder : chevron;
+                  case DocumentType.WEB: return "globe-asia";
+                  case DocumentType.SCREENSHOT: return "photo-video";
+                  case DocumentType.WEBCAM: return "video";
+                  case DocumentType.AUDIO: return "microphone";
+                  case DocumentType.BUTTON: return "bolt";
+                  case DocumentType.PRES: return "tv";
+                  case DocumentType.SCRIPTING: return "terminal";
+                  case DocumentType.IMPORT: return "cloud-upload-alt";
+                  case DocumentType.VID: return "video";
+                  case DocumentType.INK: return "pen-nib";
+                  case DocumentType.PDF: return "file-pdf";
+                  case DocumentType.LINK: return "link";
+                  case DocumentType.MAP: return "map-marker-alt";
+                  default: return "question";
             }
-            if (typeof data === "object") {
-                return data instanceof Array ? convertList(data, excludeEmptyObjects) : convertObject(data, excludeEmptyObjects, title, undefined);
+      }
+
+
+      export namespace Get {
+
+            const primitives = ["string", "number", "boolean"];
+
+            export interface JsonConversionOpts {
+                  data: any;
+                  title?: string;
+                  appendToExisting?: { targetDoc: Doc, fieldKey?: string };
+                  excludeEmptyObjects?: boolean;
             }
-            throw new Error(`How did ${data} of type ${typeof data} end up in JSON?`);
-        };
-    }
+
+            const defaultKey = "json";
+
+            /**
+             * This function takes any valid JSON(-like) data, i.e. parsed or unparsed, and at arbitrarily
+             * deep levels of nesting, converts the data and structure into nested documents with the appropriate fields.
+             *
+             * After building a hierarchy within / below a top-level document, it then returns that top-level parent.
+             *
+             * If we've received a string, treat it like valid JSON and try to parse it into an object. If this fails, the
+             * string is invalid JSON, so we should assume that the input is the result of a JSON.parse()
+             * call that returned a regular string value to be stored as a Field.
+             *
+             * If we've received something other than a string, since the caller might also pass in the results of a
+             * JSON.parse() call, valid input might be an object, an array (still typeof object), a boolean or a number.
+             * Anything else (like a function, etc. passed in naively as any) is meaningless for this operation.
+             *
+             * All TS/JS objects get converted directly to documents, directly preserving the key value structure. Everything else,
+             * lacking the key value structure, gets stored as a field in a wrapper document.
+             *
+             * @param data for convenience and flexibility, either a valid JSON string to be parsed,
+             * or the result of any JSON.parse() call.
+             * @param title an optional title to give to the highest parent document in the hierarchy.
+             * If whether this function creates a new document or appendToExisting is specified and that document already has a title,
+             * because this title field can be left undefined for the opposite behavior, including a title will overwrite the existing title.
+             * @param appendToExisting **if specified**, there are two cases, both of which return the target document:
+             *
+             * 1) the json to be converted can be represented as a document, in which case the target document will act as the root
+             * of the tree and receive all the conversion results as new fields on itself
+             * 2) the json can't be represented as a document, in which case the function will assign the field-level conversion
+             * results to either the specified key on the target document, or to its "json" key by default.
+             *
+             * If not specified, the function creates and returns a new entirely generic document (different from the Doc.Create calls)
+             * to act as the root of the tree.
+             *
+             * One might choose to specify this field if you want to write to a document returned from a Document.Create function call,
+             * say a TreeView document that will be rendered, not just an untyped, identityless doc that would otherwise be created
+             * from a default call to new Doc.
+             *
+             * @param excludeEmptyObjects whether non-primitive objects (TypeScript objects and arrays) should be converted even
+             * if they contain no data. By default, empty objects and arrays are ignored.
+             */
+            export function FromJson({ data, title, appendToExisting, excludeEmptyObjects }: JsonConversionOpts): Opt<Doc> {
+                  if (excludeEmptyObjects === undefined) {
+                        excludeEmptyObjects = true;
+                  }
+                  if (data === undefined || data === null || ![...primitives, "object"].includes(typeof data)) {
+                        return undefined;
+                  }
+                  let resolved: any;
+                  try {
+                        resolved = JSON.parse(typeof data === "string" ? data : JSON.stringify(data));
+                  } catch (e) {
+                        return undefined;
+                  }
+                  let output: Opt<Doc>;
+                  if (typeof resolved === "object" && !(resolved instanceof Array)) {
+                        output = convertObject(resolved, excludeEmptyObjects, title, appendToExisting?.targetDoc);
+                  } else {
+                        // give the proper types to the data extracted from the JSON
+                        const result = toField(resolved, excludeEmptyObjects);
+                        if (appendToExisting) {
+                              (output = appendToExisting.targetDoc)[appendToExisting.fieldKey || defaultKey] = result;
+                        } else {
+                              (output = new Doc).json = result;
+                        }
+                  }
+                  title && output && (output.title = title);
+                  return output;
+            }
+
+            /**
+             * For each value of the object, recursively convert it to its appropriate field value
+             * and store the field at the appropriate key in the document if it is not undefined
+             * @param object the object to convert
+             * @returns the object mapped from JSON to field values, where each mapping
+             * might involve arbitrary recursion (since toField might itself call convertObject)
+             */
+            const convertObject = (object: any, excludeEmptyObjects: boolean, title?: string, target?: Doc): Opt<Doc> => {
+                  const hasEntries = Object.keys(object).length;
+                  if (hasEntries || !excludeEmptyObjects) {
+                        const resolved = target ?? new Doc;
+                        if (hasEntries) {
+                              let result: Opt<Field>;
+                              Object.keys(object).map(key => {
+                                    // if excludeEmptyObjects is true, any qualifying conversions from toField will
+                                    // be undefined, and thus the results that would have
+                                    // otherwise been empty (List or Doc)s will just not be written
+                                    if (result = toField(object[key], excludeEmptyObjects, key)) {
+                                          resolved[key] = result;
+                                    }
+                              });
+                        }
+                        title && (resolved.title = title);
+                        return resolved;
+                  }
+            };
+
+            /**
+             * For each element in the list, recursively convert it to a document or other field
+             * and push the field to the list if it is not undefined
+             * @param list the list to convert
+             * @returns the list mapped from JSON to field values, where each mapping
+             * might involve arbitrary recursion (since toField might itself call convertList)
+             */
+            const convertList = (list: Array<any>, excludeEmptyObjects: boolean): Opt<List<Field>> => {
+                  const target = new List();
+                  let result: Opt<Field>;
+                  // if excludeEmptyObjects is true, any qualifying conversions from toField will
+                  // be undefined, and thus the results that would have
+                  // otherwise been empty (List or Doc)s will just not be written
+                  list.map(item => (result = toField(item, excludeEmptyObjects)) && target.push(result));
+                  if (target.length || !excludeEmptyObjects) {
+                        return target;
+                  }
+            };
+
+            const toField = (data: any, excludeEmptyObjects: boolean, title?: string): Opt<Field> => {
+                  if (data === null || data === undefined) {
+                        return undefined;
+                  }
+                  if (primitives.includes(typeof data)) {
+                        return data;
+                  }
+                  if (typeof data === "object") {
+                        return data instanceof Array ? convertList(data, excludeEmptyObjects) : convertObject(data, excludeEmptyObjects, title, undefined);
+                  }
+                  throw new Error(`How did ${data} of type ${typeof data} end up in JSON?`);
+            };
+      }
 
 }
 
@@ -1423,14 +1423,14 @@ ScriptingGlobals.add(function DOC(id: string) { console.log("Can't parse a docum
 ScriptingGlobals.add(function assignDoc(doc: Doc, field: string, id: string) { return Doc.assignDocToField(doc, field, id); });
 ScriptingGlobals.add(function docCast(doc: FieldResult): any { return DocCastAsync(doc); });
 ScriptingGlobals.add(function activePresentationItem() {
-    const curPres = Doc.UserDoc().activePresentation as Doc;
-    return curPres && DocListCast(curPres[Doc.LayoutFieldKey(curPres)])[NumCast(curPres._itemIndex)];
+      const curPres = Doc.UserDoc().activePresentation as Doc;
+      return curPres && DocListCast(curPres[Doc.LayoutFieldKey(curPres)])[NumCast(curPres._itemIndex)];
 });
 ScriptingGlobals.add(function selectedDocs(container: Doc, excludeCollections: boolean, prevValue: any) {
-    const docs = SelectionManager.Views().map(dv => dv.props.Document).
-        filter(d => !Doc.AreProtosEqual(d, container) && !d.annotationOn && d.type !== DocumentType.KVP &&
-            (!excludeCollections || d.type !== DocumentType.COL || !Cast(d.data, listSpec(Doc), null)));
-    return docs.length ? new List(docs) : prevValue;
+      const docs = SelectionManager.Views().map(dv => dv.props.Document).
+            filter(d => !Doc.AreProtosEqual(d, container) && !d.annotationOn && d.type !== DocumentType.KVP &&
+                  (!excludeCollections || d.type !== DocumentType.COL || !Cast(d.data, listSpec(Doc), null)));
+      return docs.length ? new List(docs) : prevValue;
 });
 ScriptingGlobals.add(function setDocFilter(container: Doc, key: string, value: any, modifiers: "match" | "check" | "x" | "remove") { Doc.setDocFilter(container, key, value, modifiers); });
 ScriptingGlobals.add(function setDocRangeFilter(container: Doc, key: string, range: number[]) { Doc.setDocRangeFilter(container, key, range); });
-- 
cgit v1.2.3-70-g09d2


From d3c89f90d376310cd583ae1c6ac4a7eb4c5a03ac Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Tue, 7 Jun 2022 13:39:17 -0400
Subject: fixed dragging items in overlayView to not invalidate DocumentView
 and cause a rerender.  fixed loss of ability to select or drag after dragging
 a recording.

---
 src/Utils.ts                                       |  10 +-
 src/client/util/DragManager.ts                     |   4 -
 src/client/views/OverlayView.tsx                   |  13 +-
 .../views/nodes/RecordingBox/RecordingBox.tsx      |  85 ++--
 .../views/nodes/RecordingBox/RecordingView.tsx     | 491 +++++++++++----------
 5 files changed, 305 insertions(+), 298 deletions(-)

(limited to 'src/client/views/nodes/RecordingBox/RecordingBox.tsx')

diff --git a/src/Utils.ts b/src/Utils.ts
index 6f3a31b49..07e95d438 100644
--- a/src/Utils.ts
+++ b/src/Utils.ts
@@ -698,6 +698,7 @@ export function setupMoveUpEvents(
     (target as any)._lastTap = Date.now();
     (target as any)._downX = (target as any)._lastX = e.clientX;
     (target as any)._downY = (target as any)._lastY = e.clientY;
+    (target as any)._noClick = false;
 
     const _moveEvent = (e: PointerEvent): void => {
         if (Math.abs(e.clientX - (target as any)._downX) > Utils.DRAG_THRESHOLD || Math.abs(e.clientY - (target as any)._downY) > Utils.DRAG_THRESHOLD) {
@@ -729,17 +730,20 @@ export function setupMoveUpEvents(
                 clearTimeout((target as any)._doubleTime);
                 (target as any)._doubleTime = undefined;
             }
-            clickEvent(e, (target as any)._doubleTap);
+            (target as any)._noClick = clickEvent(e, (target as any)._doubleTap);
         }
         document.removeEventListener("pointermove", _moveEvent);
         document.removeEventListener("pointerup", _upEvent);
     };
+    const _clickEvent = (e: MouseEvent): void => {
+        if ((target as any)._noClick) e.stopPropagation();
+        document.removeEventListener("click", _clickEvent, true);
+    }
     if (stopPropagation) {
         e.stopPropagation();
         e.preventDefault();
     }
-    document.removeEventListener("pointermove", _moveEvent);
-    document.removeEventListener("pointerup", _upEvent);
     document.addEventListener("pointermove", _moveEvent);
     document.addEventListener("pointerup", _upEvent);
+    document.addEventListener("click", _clickEvent, true);
 }
\ No newline at end of file
diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts
index f438a8c11..9f8c49081 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -210,10 +210,6 @@ export namespace DragManager {
         options?: DragOptions,
         dropEvent?: () => any
     ) {
-        // stop an 'accidental' on-click drag that may have occured if the user is in presenting mode
-        // note: dragData.dropAction is only undefined when the element itself being dragged without being selected
-        if (Doc.UserDoc()?.presentationMode === 'recording' && dragData.dropAction === undefined) return false;
-
         const addAudioTag = (dropDoc: any) => {
             dropDoc && !dropDoc.creationDate && (dropDoc.creationDate = new DateField);
             dropDoc instanceof Doc && DocUtils.MakeLinkToActiveAudio(() => dropDoc);
diff --git a/src/client/views/OverlayView.tsx b/src/client/views/OverlayView.tsx
index 9d970c06f..6796db51e 100644
--- a/src/client/views/OverlayView.tsx
+++ b/src/client/views/OverlayView.tsx
@@ -1,5 +1,7 @@
+import { docs } from "googleapis/build/src/apis/docs";
 import { action, computed, observable } from "mobx";
 import { observer } from "mobx-react";
+import { computedFn } from "mobx-utils";
 import * as React from "react";
 import ReactLoading from 'react-loading';
 import { Doc, WidthSym, HeightSym } from "../../fields/Doc";
@@ -146,6 +148,12 @@ export class OverlayView extends React.Component {
         return remove;
     }
 
+    removeOverlayDoc = (doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).map(doc => Doc.RemoveDocFromList((Doc.UserDoc().myOverlayDocs as Doc), "data", doc)).length ? true : false;
+
+
+    docScreenToLocalXf = computedFn(function docScreenToLocalXf(this: any, doc: Doc) {
+        return () => new Transform(-NumCast(doc.x), -NumCast(doc.y), 1);
+    }.bind(this));
 
     @computed get overlayDocs() {
         return CurrentUserUtils.OverlayDocs?.map(d => {
@@ -179,17 +187,16 @@ export class OverlayView extends React.Component {
                 offsetx = NumCast(d.x) - e.clientX;
                 offsety = NumCast(d.y) - e.clientY;
             };
-
             return <div className="overlayView-doc" ref={dref} key={d[Id]} onPointerDown={onPointerDown} style={{ top: d.type === 'presentation' ? 0 : undefined, width: NumCast(d._width), height: NumCast(d._height), transform: `translate(${d.x}px, ${d.y}px)` }}>
                 <DocumentView
                     Document={d}
                     rootSelected={returnTrue}
                     bringToFront={emptyFunction}
                     addDocument={undefined}
-                    removeDocument={doc => (doc instanceof Doc ? [doc] : doc).map(doc => Doc.RemoveDocFromList((Doc.UserDoc().myOverlayDocs as Doc), "data", doc)).length ? true : false}
+                    removeDocument={this.removeOverlayDoc}
                     PanelWidth={d[WidthSym]}
                     PanelHeight={d[HeightSym]}
-                    ScreenToLocalTransform={() => new Transform(-NumCast(d.x), -NumCast(d.y), 1)}
+                    ScreenToLocalTransform={this.docScreenToLocalXf(d)}
                     renderDepth={1}
                     isDocumentActive={returnTrue}
                     isContentActive={emptyFunction}
diff --git a/src/client/views/nodes/RecordingBox/RecordingBox.tsx b/src/client/views/nodes/RecordingBox/RecordingBox.tsx
index 159271223..eac1c63f9 100644
--- a/src/client/views/nodes/RecordingBox/RecordingBox.tsx
+++ b/src/client/views/nodes/RecordingBox/RecordingBox.tsx
@@ -16,47 +16,46 @@ import { Id } from "../../../../fields/FieldSymbols";
 @observer
 export class RecordingBox extends ViewBoxBaseComponent() {
 
-      public static LayoutString(fieldKey: string) { return FieldView.LayoutString(RecordingBox, fieldKey); }
-
-      private _ref: React.RefObject<HTMLDivElement> = React.createRef();
-
-      constructor(props: any) {
-            super(props);
-      }
-
-      componentDidMount() {
-            console.log("set native width and height")
-            Doc.SetNativeWidth(this.dataDoc, 1280);
-            Doc.SetNativeHeight(this.dataDoc, 720);
-      }
-
-      @observable result: Upload.FileInformation | undefined = undefined
-      @observable videoDuration: number | undefined = undefined
-
-      @action
-      setVideoDuration = (duration: number) => {
-            this.videoDuration = duration
-      }
-
-      @action
-      setResult = (info: Upload.FileInformation, trackScreen: boolean) => {
-            this.result = info
-            this.dataDoc.type = DocumentType.VID;
-            this.dataDoc[this.fieldKey + "-duration"] = this.videoDuration;
-
-            this.dataDoc.layout = VideoBox.LayoutString(this.fieldKey);
-            this.dataDoc[this.props.fieldKey] = new VideoField(this.result.accessPaths.agnostic.client);
-            this.dataDoc[this.fieldKey + "-recorded"] = true;
-            // stringify the presenation and store it
-            if (trackScreen) {
-                  this.dataDoc[this.fieldKey + "-presentation"] = JSON.stringify(RecordingApi.Instance.clear());
-            }
-      }
-
-      render() {
-            // console.log("Proto[Is]: ", this.rootDoc.proto?.[Id])
-            return <div className="recordingBox" ref={this._ref}>
-                  {!this.result && <RecordingView setResult={this.setResult} setDuration={this.setVideoDuration} id={this.rootDoc.proto?.[Id]} />}
-            </div>;
-      }
+    public static LayoutString(fieldKey: string) { return FieldView.LayoutString(RecordingBox, fieldKey); }
+
+    private _ref: React.RefObject<HTMLDivElement> = React.createRef();
+
+    constructor(props: any) {
+        super(props);
+    }
+
+    componentDidMount() {
+        console.log("set native width and height")
+        Doc.SetNativeWidth(this.dataDoc, 1280);
+        Doc.SetNativeHeight(this.dataDoc, 720);
+    }
+
+    @observable result: Upload.FileInformation | undefined = undefined
+    @observable videoDuration: number | undefined = undefined
+
+    @action
+    setVideoDuration = (duration: number) => {
+        this.videoDuration = duration
+    }
+
+    @action
+    setResult = (info: Upload.FileInformation, trackScreen: boolean) => {
+        this.result = info
+        this.dataDoc.type = DocumentType.VID;
+        this.dataDoc[this.fieldKey + "-duration"] = this.videoDuration;
+
+        this.dataDoc.layout = VideoBox.LayoutString(this.fieldKey);
+        this.dataDoc[this.props.fieldKey] = new VideoField(this.result.accessPaths.agnostic.client);
+        this.dataDoc[this.fieldKey + "-recorded"] = true;
+        // stringify the presenation and store it
+        if (trackScreen) {
+            this.dataDoc[this.fieldKey + "-presentation"] = JSON.stringify(RecordingApi.Instance.clear());
+        }
+    }
+
+    render() {
+        return <div className="recordingBox" ref={this._ref}>
+            {!this.result && <RecordingView setResult={this.setResult} setDuration={this.setVideoDuration} id={this.rootDoc.proto?.[Id]} />}
+        </div>;
+    }
 }
diff --git a/src/client/views/nodes/RecordingBox/RecordingView.tsx b/src/client/views/nodes/RecordingBox/RecordingView.tsx
index 5826abd65..b95335792 100644
--- a/src/client/views/nodes/RecordingBox/RecordingView.tsx
+++ b/src/client/views/nodes/RecordingBox/RecordingView.tsx
@@ -9,273 +9,274 @@ import { Networking } from '../../../Network';
 import { Upload } from '../../../../server/SharedMediaTypes';
 
 import { RecordingApi } from '../../../util/RecordingApi';
-import { emptyFunction, returnFalse, setupMoveUpEvents } from '../../../../Utils';
+import { emptyFunction, returnFalse, returnTrue, setupMoveUpEvents } from '../../../../Utils';
 
 interface MediaSegment {
-      videoChunks: any[],
-      endTime: number
+    videoChunks: any[],
+    endTime: number
 }
 
 interface IRecordingViewProps {
-      setResult: (info: Upload.FileInformation, trackScreen: boolean) => void
-      setDuration: (seconds: number) => void
-      id: string
+    setResult: (info: Upload.FileInformation, trackScreen: boolean) => void
+    setDuration: (seconds: number) => void
+    id: string
 }
 
 const MAXTIME = 100000;
 
 export function RecordingView(props: IRecordingViewProps) {
 
-      const [recording, setRecording] = useState(false);
-      const recordingTimerRef = useRef<number>(0);
-      const [recordingTimer, setRecordingTimer] = useState(0); // unit is 0.01 second
-      const [playing, setPlaying] = useState(false);
-      const [progress, setProgress] = useState(0);
-
-      const [videos, setVideos] = useState<MediaSegment[]>([]);
-      const videoRecorder = useRef<MediaRecorder | null>(null);
-      const videoElementRef = useRef<HTMLVideoElement | null>(null);
-
-      const [finished, setFinished] = useState<boolean>(false)
-      const [trackScreen, setTrackScreen] = useState<boolean>(true)
-
-
-
-      const DEFAULT_MEDIA_CONSTRAINTS = {
-            video: {
-                  width: 1280,
-                  height: 720,
-            },
-            audio: {
-                  echoCancellation: true,
-                  noiseSuppression: true,
-                  sampleRate: 44100
-            }
-      }
-
-      useEffect(() => {
-
-            if (finished) {
-                  props.setDuration(recordingTimer * 100)
-                  let allVideoChunks: any = []
-                  videos.forEach((vid) => {
-                        console.log(vid.videoChunks)
-                        allVideoChunks = allVideoChunks.concat(vid.videoChunks)
-                  })
-
-                  const videoFile = new File(allVideoChunks, "video.mkv", { type: allVideoChunks[0].type, lastModified: Date.now() });
-
-                  Networking.UploadFilesToServer(videoFile)
-                        .then((data) => {
-                              const result = data[0].result
-                              if (!(result instanceof Error)) { // convert this screenshotBox into normal videoBox
-                                    props.setResult(result, trackScreen)
-                              } else {
-                                    alert("video conversion failed");
-                              }
-                        })
-
-            }
-
-
-      }, [finished])
-
-      useEffect(() => {
-            // check if the browser supports media devices on first load
-            if (!navigator.mediaDevices) {
-                  console.log('This browser does not support getUserMedia.')
-            }
-            console.log('This device has the correct media devices.')
-      }, [])
-
-      useEffect(() => {
-            // get access to the video element on every render
-            videoElementRef.current = document.getElementById(`video-${props.id}`) as HTMLVideoElement;
-      })
-
-      useEffect(() => {
-            let interval: any = null;
-            if (recording) {
-                  interval = setInterval(() => {
-                        setRecordingTimer(unit => unit + 1);
-                  }, 10);
-            } else if (!recording && recordingTimer !== 0) {
-                  clearInterval(interval);
-            }
-            return () => clearInterval(interval);
-      }, [recording])
-
-      useEffect(() => {
-            setVideoProgressHelper(recordingTimer)
-            recordingTimerRef.current = recordingTimer;
-      }, [recordingTimer])
-
-      const setVideoProgressHelper = (progress: number) => {
-            const newProgress = (progress / MAXTIME) * 100;
-            setProgress(newProgress)
-      }
-      const startShowingStream = async (mediaConstraints = DEFAULT_MEDIA_CONSTRAINTS) => {
-            const stream = await navigator.mediaDevices.getUserMedia(mediaConstraints)
-
-            videoElementRef.current!.src = ""
-            videoElementRef.current!.srcObject = stream
-            videoElementRef.current!.muted = true
-
-            return stream
-      }
-
-      const record = async () => {
-            const stream = await startShowingStream();
-            videoRecorder.current = new MediaRecorder(stream)
-
-            // temporary chunks of video
-            let videoChunks: any = []
-
-            videoRecorder.current.ondataavailable = (event: any) => {
-                  if (event.data.size > 0) {
-                        videoChunks.push(event.data)
-                  }
-            }
-
-            videoRecorder.current.onstart = (event: any) => {
-                  setRecording(true);
-                  trackScreen && RecordingApi.Instance.start();
-            }
-
-            videoRecorder.current.onstop = () => {
-                  // if we have a last portion
-                  if (videoChunks.length > 1) {
-                        // append the current portion to the video pieces
-                        setVideos(videos => [...videos, { videoChunks: videoChunks, endTime: recordingTimerRef.current }])
-                  }
-
-                  // reset the temporary chunks
-                  videoChunks = []
-                  setRecording(false);
-                  setFinished(true);
-                  trackScreen && RecordingApi.Instance.pause();
-            }
-
-            // recording paused
-            videoRecorder.current.onpause = (event: any) => {
-                  // append the current portion to the video pieces
-                  setVideos(videos => [...videos, { videoChunks: videoChunks, endTime: recordingTimerRef.current }])
+    const [recording, setRecording] = useState(false);
+    const recordingTimerRef = useRef<number>(0);
+    const [recordingTimer, setRecordingTimer] = useState(0); // unit is 0.01 second
+    const [playing, setPlaying] = useState(false);
+    const [progress, setProgress] = useState(0);
+
+    const [videos, setVideos] = useState<MediaSegment[]>([]);
+    const videoRecorder = useRef<MediaRecorder | null>(null);
+    const videoElementRef = useRef<HTMLVideoElement | null>(null);
+
+    const [finished, setFinished] = useState<boolean>(false)
+    const [trackScreen, setTrackScreen] = useState<boolean>(true)
+
+
+
+    const DEFAULT_MEDIA_CONSTRAINTS = {
+        video: {
+            width: 1280,
+            height: 720,
+        },
+        audio: {
+            echoCancellation: true,
+            noiseSuppression: true,
+            sampleRate: 44100
+        }
+    }
+
+    useEffect(() => {
+
+        if (finished) {
+            props.setDuration(recordingTimer * 100)
+            let allVideoChunks: any = []
+            videos.forEach((vid) => {
+                console.log(vid.videoChunks)
+                allVideoChunks = allVideoChunks.concat(vid.videoChunks)
+            })
 
-                  // reset the temporary chunks
-                  videoChunks = []
-                  setRecording(false);
-                  trackScreen && RecordingApi.Instance.pause();
+            const videoFile = new File(allVideoChunks, "video.mkv", { type: allVideoChunks[0].type, lastModified: Date.now() });
+
+            Networking.UploadFilesToServer(videoFile)
+                .then((data) => {
+                    const result = data[0].result
+                    if (!(result instanceof Error)) { // convert this screenshotBox into normal videoBox
+                        props.setResult(result, trackScreen)
+                    } else {
+                        alert("video conversion failed");
+                    }
+                })
+
+        }
+
+
+    }, [finished])
+
+    useEffect(() => {
+        // check if the browser supports media devices on first load
+        if (!navigator.mediaDevices) {
+            console.log('This browser does not support getUserMedia.')
+        }
+        console.log('This device has the correct media devices.')
+    }, [])
+
+    useEffect(() => {
+        // get access to the video element on every render
+        videoElementRef.current = document.getElementById(`video-${props.id}`) as HTMLVideoElement;
+    })
+
+    useEffect(() => {
+        let interval: any = null;
+        if (recording) {
+            interval = setInterval(() => {
+                setRecordingTimer(unit => unit + 1);
+            }, 10);
+        } else if (!recording && recordingTimer !== 0) {
+            clearInterval(interval);
+        }
+        return () => clearInterval(interval);
+    }, [recording])
+
+    useEffect(() => {
+        setVideoProgressHelper(recordingTimer)
+        recordingTimerRef.current = recordingTimer;
+    }, [recordingTimer])
+
+    const setVideoProgressHelper = (progress: number) => {
+        const newProgress = (progress / MAXTIME) * 100;
+        setProgress(newProgress)
+    }
+    const startShowingStream = async (mediaConstraints = DEFAULT_MEDIA_CONSTRAINTS) => {
+        const stream = await navigator.mediaDevices.getUserMedia(mediaConstraints)
+
+        videoElementRef.current!.src = ""
+        videoElementRef.current!.srcObject = stream
+        videoElementRef.current!.muted = true
+
+        return stream
+    }
+
+    const record = async () => {
+        const stream = await startShowingStream();
+        videoRecorder.current = new MediaRecorder(stream)
+
+        // temporary chunks of video
+        let videoChunks: any = []
+
+        videoRecorder.current.ondataavailable = (event: any) => {
+            if (event.data.size > 0) {
+                videoChunks.push(event.data)
             }
-
-            videoRecorder.current.onresume = async (event: any) => {
-                  await startShowingStream();
-                  setRecording(true);
-                  trackScreen && RecordingApi.Instance.resume();
+        }
+
+        videoRecorder.current.onstart = (event: any) => {
+            setRecording(true);
+            trackScreen && RecordingApi.Instance.start();
+        }
+
+        videoRecorder.current.onstop = () => {
+            // if we have a last portion
+            if (videoChunks.length > 1) {
+                // append the current portion to the video pieces
+                setVideos(videos => [...videos, { videoChunks: videoChunks, endTime: recordingTimerRef.current }])
             }
 
-            videoRecorder.current.start(200)
-      }
-
-
-      const stop = () => {
-            if (videoRecorder.current) {
-                  if (videoRecorder.current.state !== "inactive") {
-                        videoRecorder.current.stop();
-                        // recorder.current.stream.getTracks().forEach((track: any) => track.stop())
-                  }
+            // reset the temporary chunks
+            videoChunks = []
+            setRecording(false);
+            setFinished(true);
+            trackScreen && RecordingApi.Instance.pause();
+        }
+
+        // recording paused
+        videoRecorder.current.onpause = (event: any) => {
+            // append the current portion to the video pieces
+            setVideos(videos => [...videos, { videoChunks: videoChunks, endTime: recordingTimerRef.current }])
+
+            // reset the temporary chunks
+            videoChunks = []
+            setRecording(false);
+            trackScreen && RecordingApi.Instance.pause();
+        }
+
+        videoRecorder.current.onresume = async (event: any) => {
+            await startShowingStream();
+            setRecording(true);
+            trackScreen && RecordingApi.Instance.resume();
+        }
+
+        videoRecorder.current.start(200)
+    }
+
+
+    const stop = () => {
+        if (videoRecorder.current) {
+            if (videoRecorder.current.state !== "inactive") {
+                videoRecorder.current.stop();
+                // recorder.current.stream.getTracks().forEach((track: any) => track.stop())
             }
-      }
+        }
+    }
 
-      const pause = () => {
-            if (videoRecorder.current) {
-                  if (videoRecorder.current.state === "recording") {
-                        videoRecorder.current.pause();
-                  }
+    const pause = () => {
+        if (videoRecorder.current) {
+            if (videoRecorder.current.state === "recording") {
+                videoRecorder.current.pause();
             }
-      }
-
-      const startOrResume = (e: React.PointerEvent) => {
-            // the code to start or resume does not get triggered if we start dragging the button
-            setupMoveUpEvents({}, e, returnFalse, returnFalse, () => {
-                  if (!videoRecorder.current || videoRecorder.current.state === "inactive") {
-                        record();
-                  } else if (videoRecorder.current.state === "paused") {
-                        videoRecorder.current.resume();
-                  }
-            })
-      }
-
-      const clearPrevious = () => {
-            const numVideos = videos.length
-            setRecordingTimer(numVideos == 1 ? 0 : videos[numVideos - 2].endTime)
-            setVideoProgressHelper(numVideos == 1 ? 0 : videos[numVideos - 2].endTime)
-            setVideos(videos.filter((_, idx) => idx !== numVideos - 1));
-      }
-
-      const handleOnTimeUpdate = () => {
-            if (playing) {
-                  setVideoProgressHelper(videoElementRef.current!.currentTime)
+        }
+    }
+
+    const startOrResume = (e: React.PointerEvent) => {
+        // the code to start or resume does not get triggered if we start dragging the button
+        setupMoveUpEvents({}, e, returnTrue, returnFalse, e => {
+            if (!videoRecorder.current || videoRecorder.current.state === "inactive") {
+                record();
+            } else if (videoRecorder.current.state === "paused") {
+                videoRecorder.current.resume();
             }
-      };
-
-      const millisecondToMinuteSecond = (milliseconds: number) => {
-            const toTwoDigit = (digit: number) => {
-                  return String(digit).length == 1 ? "0" + digit : digit
-            }
-            const minutes = Math.floor((milliseconds % (1000 * 60 * 60)) / (1000 * 60));
-            const seconds = Math.floor((milliseconds % (1000 * 60)) / 1000);
-            return toTwoDigit(minutes) + " : " + toTwoDigit(seconds);
-      }
-
-      return (
-            <div className="recording-container">
-                  <div className="video-wrapper">
-                        <video id={`video-${props.id}`}
-                              autoPlay
-                              muted
-                              onTimeUpdate={handleOnTimeUpdate}
-                        />
-                        <div className="recording-sign">
-                              <span className="dot" />
-                              <p className="timer">{millisecondToMinuteSecond(recordingTimer * 10)}</p>
+            return true; // cancels propagation to documentView to avoid selecting it.
+        }, false, false);
+    }
+
+    const clearPrevious = () => {
+        const numVideos = videos.length
+        setRecordingTimer(numVideos == 1 ? 0 : videos[numVideos - 2].endTime)
+        setVideoProgressHelper(numVideos == 1 ? 0 : videos[numVideos - 2].endTime)
+        setVideos(videos.filter((_, idx) => idx !== numVideos - 1));
+    }
+
+    const handleOnTimeUpdate = () => {
+        if (playing) {
+            setVideoProgressHelper(videoElementRef.current!.currentTime)
+        }
+    };
+
+    const millisecondToMinuteSecond = (milliseconds: number) => {
+        const toTwoDigit = (digit: number) => {
+            return String(digit).length == 1 ? "0" + digit : digit
+        }
+        const minutes = Math.floor((milliseconds % (1000 * 60 * 60)) / (1000 * 60));
+        const seconds = Math.floor((milliseconds % (1000 * 60)) / 1000);
+        return toTwoDigit(minutes) + " : " + toTwoDigit(seconds);
+    }
+
+    return (
+        <div className="recording-container">
+            <div className="video-wrapper">
+                <video id={`video-${props.id}`}
+                    autoPlay
+                    muted
+                    onTimeUpdate={handleOnTimeUpdate}
+                />
+                <div className="recording-sign">
+                    <span className="dot" />
+                    <p className="timer">{millisecondToMinuteSecond(recordingTimer * 10)}</p>
+                </div>
+                <div className="controls">
+
+                    <div className="controls-inner-container">
+                        <div className="record-button-wrapper">
+                            {recording ?
+                                <button className="stop-button" onPointerDown={pause} /> :
+                                <button className="record-button" onPointerDown={startOrResume} />
+                            }
                         </div>
-                        <div className="controls">
-
-                              <div className="controls-inner-container">
-                                    <div className="record-button-wrapper">
-                                          {recording ?
-                                                <button className="stop-button" onPointerDown={pause} /> :
-                                                <button className="record-button" onPointerDown={startOrResume} />
-                                          }
-                                    </div>
 
-                                    {!recording && (videos.length > 0 ?
+                        {!recording && (videos.length > 0 ?
 
-                                          <div className="options-wrapper video-edit-wrapper">
-                                                {/* <IconContext.Provider value={{ color: "grey", className: "video-edit-buttons" }}>
+                            <div className="options-wrapper video-edit-wrapper">
+                                {/* <IconContext.Provider value={{ color: "grey", className: "video-edit-buttons" }}>
                                                       <MdBackspace onClick={clearPrevious} />
                                                 </IconContext.Provider> */}
-                                                <IconContext.Provider value={{ color: "#cc1c08", className: "video-edit-buttons" }}>
-                                                      <FaCheckCircle onClick={stop} />
-                                                </IconContext.Provider>
-                                          </div>
-
-                                          : <div className="options-wrapper track-screen-wrapper">
-                                                <label className="track-screen">
-                                                      <input type="checkbox" checked={trackScreen} onChange={(e) => { setTrackScreen(e.target.checked) }} />
-                                                      <span className="checkmark"></span>
-                                                      Track Screen
-                                                </label>
-                                          </div>)}
-
-                              </div>
-
-                              <ProgressBar
-                                    progress={progress}
-                                    marks={videos.map((elt) => elt.endTime / MAXTIME * 100)}
-                              // playSegment={playSegment}
-                              />
-                        </div>
-                  </div>
-            </div>)
+                                <IconContext.Provider value={{ color: "#cc1c08", className: "video-edit-buttons" }}>
+                                    <FaCheckCircle onClick={stop} />
+                                </IconContext.Provider>
+                            </div>
+
+                            : <div className="options-wrapper track-screen-wrapper">
+                                <label className="track-screen">
+                                    <input type="checkbox" checked={trackScreen} onChange={(e) => { setTrackScreen(e.target.checked) }} />
+                                    <span className="checkmark"></span>
+                                    Track Screen
+                                </label>
+                            </div>)}
+
+                    </div>
+
+                    <ProgressBar
+                        progress={progress}
+                        marks={videos.map((elt) => elt.endTime / MAXTIME * 100)}
+                    // playSegment={playSegment}
+                    />
+                </div>
+            </div>
+        </div>)
 }
\ No newline at end of file
-- 
cgit v1.2.3-70-g09d2


From 910131b13080a2649606ef8a618c77fd81248343 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Fri, 10 Jun 2022 10:39:00 -0400
Subject: fixed errors

---
 src/client/util/DragManager.ts                       | 2 +-
 src/client/views/nodes/RecordingBox/RecordingBox.tsx | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

(limited to 'src/client/views/nodes/RecordingBox/RecordingBox.tsx')

diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts
index c3c6c22df..dbfba8992 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -455,7 +455,7 @@ export namespace DragManager {
             if (dragData instanceof DocumentDragData) {
                 dragData.userDropAction = e.ctrlKey && e.altKey ? "copy" : e.ctrlKey ? "alias" : dragData.defaultDropAction;
             }
-            if (((e.target as any)?.className === "lm_tabs" || ((e.target as any)?.className === "lm_header" || e?.shiftKey) && dragData.draggedDocuments.length === 1) {
+            if (((e.target as any)?.className === "lm_tabs" || (e.target as any)?.className === "lm_header" || e?.shiftKey) && dragData.draggedDocuments.length === 1) {
                 if (!startWindowDragTimer) {
                     startWindowDragTimer = setTimeout(async () => {
                         startWindowDragTimer = undefined;
diff --git a/src/client/views/nodes/RecordingBox/RecordingBox.tsx b/src/client/views/nodes/RecordingBox/RecordingBox.tsx
index eac1c63f9..10393624b 100644
--- a/src/client/views/nodes/RecordingBox/RecordingBox.tsx
+++ b/src/client/views/nodes/RecordingBox/RecordingBox.tsx
@@ -55,7 +55,7 @@ export class RecordingBox extends ViewBoxBaseComponent() {
 
     render() {
         return <div className="recordingBox" ref={this._ref}>
-            {!this.result && <RecordingView setResult={this.setResult} setDuration={this.setVideoDuration} id={this.rootDoc.proto?.[Id]} />}
+            {!this.result && <RecordingView setResult={this.setResult} setDuration={this.setVideoDuration} id={Doc.GetProto(this.rootDoc)[Id]} />}
         </div>;
     }
 }
-- 
cgit v1.2.3-70-g09d2

