From 11537da75c76fba79a2709d2ad175dfa16a25256 Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Fri, 18 Oct 2019 14:00:25 -0400
Subject: fixes for drawing link anchors to pdf text selections.

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

(limited to 'src/client/views/pdf/Annotation.tsx')

diff --git a/src/client/views/pdf/Annotation.tsx b/src/client/views/pdf/Annotation.tsx
index ad6240c70..e0a3b9171 100644
--- a/src/client/views/pdf/Annotation.tsx
+++ b/src/client/views/pdf/Annotation.tsx
@@ -100,7 +100,7 @@ class RegionAnnotation extends React.Component<IRegionAnnotationProps> {
             let annoGroup = await Cast(this.props.document.group, Doc);
             if (annoGroup) {
                 DocumentManager.Instance.FollowLink(undefined, annoGroup,
-                    (doc: Doc, maxLocation: string) => this.props.addDocTab(doc, undefined, e.ctrlKey ? "onRight" : "inTab"),
+                    (doc: Doc, maxLocation: string) => this.props.addDocTab(doc, undefined, e.ctrlKey ? "inTab" : "onRight"),
                     false, false, undefined);
             }
         }
-- 
cgit v1.2.3-70-g09d2


From 1089aa451fd4571545e06b5674bc02ed1ecf4361 Mon Sep 17 00:00:00 2001
From: bob <bcz@cs.brown.edu>
Date: Tue, 22 Oct 2019 11:32:43 -0400
Subject: added recording to audio documents and added to toolbar

---
 src/client/views/MainView.tsx                      |   3 +-
 src/client/views/nodes/AudioBox.scss               |  14 ++-
 src/client/views/nodes/AudioBox.tsx                | 137 +++++++++++++++++++--
 src/client/views/pdf/Annotation.tsx                |   1 +
 .../authentication/models/current_user_utils.ts    |   1 +
 5 files changed, 142 insertions(+), 14 deletions(-)

(limited to 'src/client/views/pdf/Annotation.tsx')

diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 26e70c5c7..8bad70093 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -1,7 +1,7 @@
 import { library } from '@fortawesome/fontawesome-svg-core';
 import {
     faArrowDown, faArrowUp, faBolt, faCaretUp, faCat, faCheck, faChevronRight, faClone, faCloudUploadAlt, faCommentAlt, faCut, faEllipsisV, faExclamation, faFilePdf, faFilm, faFont, faGlobeAsia, faLongArrowAltRight,
-    faMusic, faObjectGroup, faPause, faMousePointer, faPenNib, faFileAudio, faPen, faEraser, faPlay, faPortrait, faRedoAlt, faThumbtack, faTree, faTv, faUndoAlt, faHighlighter
+    faMusic, faObjectGroup, faPause, faMousePointer, faPenNib, faFileAudio, faPen, faEraser, faPlay, faPortrait, faRedoAlt, faThumbtack, faTree, faTv, faUndoAlt, faHighlighter, faMicrophone
 } from '@fortawesome/free-solid-svg-icons';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { action, computed, configure, observable, reaction, runInAction } from 'mobx';
@@ -108,6 +108,7 @@ export class MainView extends React.Component {
         library.add(faEraser);
         library.add(faFileAudio);
         library.add(faPenNib);
+        library.add(faMicrophone);
         library.add(faFilm);
         library.add(faMusic);
         library.add(faTree);
diff --git a/src/client/views/nodes/AudioBox.scss b/src/client/views/nodes/AudioBox.scss
index 04d98e10d..5c43c3c00 100644
--- a/src/client/views/nodes/AudioBox.scss
+++ b/src/client/views/nodes/AudioBox.scss
@@ -1,8 +1,9 @@
-.audiobox-container {
+.audiobox-container, .audiobox-container-interactive {
     width: 100%;
     height: 100%;
     position: inherit;
     display:inline-block;
+    pointer-events: all;
     .audiobox-control, .audiobox-control-interactive {
         top:0;
         max-height: 32px;
@@ -13,4 +14,15 @@
     .audiobox-control-interactive {
         pointer-events: all;
     }
+    .audiobox-record {
+        pointer-events: all;
+        width:100%;
+        height:100%;
+        position: absolute;
+        pointer-events: none;
+    }
+    .audiobox-record-interactive {
+        pointer-events: all;
+
+    }
 }
\ No newline at end of file
diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx
index 4c1c3a465..ee4e06a2e 100644
--- a/src/client/views/nodes/AudioBox.tsx
+++ b/src/client/views/nodes/AudioBox.tsx
@@ -2,37 +2,150 @@ import React = require("react");
 import { FieldViewProps, FieldView } from './FieldView';
 import { observer } from "mobx-react";
 import "./AudioBox.scss";
-import { Cast } from "../../../new_fields/Types";
+import { Cast, DateCast } from "../../../new_fields/Types";
 import { AudioField } from "../../../new_fields/URLField";
 import { DocExtendableComponent } from "../DocComponent";
-import { makeInterface } from "../../../new_fields/Schema";
+import { makeInterface, createSchema } from "../../../new_fields/Schema";
 import { documentSchema } from "../../../new_fields/documentSchemas";
+import { Utils } from "../../../Utils";
+import { RouteStore } from "../../../server/RouteStore";
+import { runInAction, observable, reaction, IReactionDisposer, computed } from "mobx";
+import { DateField } from "../../../new_fields/DateField";
+import { SelectionManager } from "../../util/SelectionManager";
+import { Doc } from "../../../new_fields/Doc";
+import { ContextMenuProps } from "../ContextMenuItem";
+import { ContextMenu } from "../ContextMenu";
 
-type AudioDocument = makeInterface<[typeof documentSchema]>;
-const AudioDocument = makeInterface(documentSchema);
+interface Window {
+    MediaRecorder: MediaRecorder;
+}
+
+declare class MediaRecorder {
+    // whatever MediaRecorder has
+    constructor(e: any);
+}
+export const audioSchema = createSchema({
+    playOnSelect: "boolean"
+});
+
+type AudioDocument = makeInterface<[typeof documentSchema, typeof audioSchema]>;
+const AudioDocument = makeInterface(documentSchema, audioSchema);
 const defaultField: AudioField = new AudioField(new URL("http://techslides.com/demos/samples/sample.mp3"));
 
 @observer
 export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocument>(AudioDocument) {
 
+    _reactionDisposer: IReactionDisposer | undefined;
+    @observable private _audioState = 0;
+
     public static LayoutString(fieldKey: string) { return FieldView.LayoutString(AudioBox, fieldKey); }
     _ref = React.createRef<HTMLAudioElement>();
 
     componentDidMount() {
-        if (this._ref.current) this._ref.current.currentTime = 1;
+        runInAction(() => this._audioState = this.path ? 2 : 0);
+        this._reactionDisposer = reaction(() => SelectionManager.SelectedDocuments(),
+            selected => {
+                let sel = selected.length ? selected[0].props.Document : undefined;
+                const extensionDoc = this.extensionDoc;
+                let start = extensionDoc && DateCast(extensionDoc.recordingStart);
+                let seek = sel && DateCast(sel.creationDate)
+                if (this._ref.current && start && seek) {
+                    if (this.Document.playOnSelect && sel && !Doc.AreProtosEqual(sel, this.props.Document)) {
+                        let delta = (seek.date.getTime() - start.date.getTime()) / 1000;
+                        if (start && seek && delta > 0 && delta < this._ref.current.duration) {
+                            this._ref.current.currentTime = delta;
+                            this._ref.current.play();
+                        } else {
+                            this._ref.current.pause();
+                        }
+                    } else {
+                        this._ref.current.pause();
+                    }
+                }
+            });
     }
 
-    render() {
+    componentWillUnmount() {
+        this._reactionDisposer && this._reactionDisposer();
+    }
+
+    _recorder: any;
+    recordAudioAnnotation = () => {
+        let gumStream: any;
+        let self = this;
+        const extensionDoc = this.extensionDoc;
+        extensionDoc && navigator.mediaDevices.getUserMedia({
+            audio: true
+        }).then(function (stream) {
+            gumStream = stream;
+            self._recorder = new MediaRecorder(stream);
+            extensionDoc.recordingStart = new DateField(new Date());
+            self._recorder.ondataavailable = async function (e: any) {
+                const formData = new FormData();
+                formData.append("file", e.data);
+                const res = await fetch(Utils.prepend(RouteStore.upload), {
+                    method: 'POST',
+                    body: formData
+                });
+                const files = await res.json();
+                const url = Utils.prepend(files[0].path);
+                // upload to server with known URL 
+                self.props.Document[self.props.fieldKey] = new AudioField(url);
+            };
+            runInAction(() => self._audioState = 1);
+            self._recorder.start();
+            setTimeout(() => {
+                self._recorder.stop();
+                runInAction(() => self._audioState = 2);
+                gumStream.getAudioTracks()[0].stop();
+            }, 60 * 60 * 1000); // stop after an hour?
+        });
+    }
+
+    specificContextMenu = (e: React.MouseEvent): void => {
+        let funcs: ContextMenuProps[] = [];
+        funcs.push({ description: (this.Document.playOnSelect ? "Don't play" : "Play") + " when document selected", event: () => this.Document.playOnSelect = !this.Document.playOnSelect, icon: "expand-arrows-alt" });
+
+        ContextMenu.Instance.addItem({ description: "Audio Funcs...", subitems: funcs, icon: "asterisk" });
+    }
+
+    recordClick = (e: React.MouseEvent) => {
+        if (e.button === 0) {
+            if (this._recorder) {
+                this._recorder.stop();
+                runInAction(() => this._audioState = 2);
+            } else {
+                this.recordAudioAnnotation();
+            }
+            e.stopPropagation();
+        }
+    }
+
+    @computed get path() {
         let field = Cast(this.props.Document[this.props.fieldKey], AudioField, defaultField);
         let path = field.url.href;
+        return path === "https://actions.google.com/sounds/v1/alarms/beep_short.ogg" ? "" : path;
+    }
+
+    @computed get audio() {
+        let interactive = this.active() ? "-interactive" : "";
+        return <audio controls ref={this._ref} className={`audiobox-control${interactive}`}>
+            <source src={this.path} type="audio/mpeg" />
+            Not supported.
+        </audio>;
+    }
+
+    render() {
 
         let interactive = this.active() ? "-interactive" : "";
-        return (
-            <div className="audiobox-container">
-                <audio controls ref={this._ref} className={`audiobox-control${interactive}`}>
-                    <source src={path} type="audio/mpeg" />
-                    Not supported.
-                </audio>
+        return (!this.extensionDoc ? (null) :
+            <div className={`audiobox-container`} onContextMenu={this.specificContextMenu} onClick={!this.path ? this.recordClick : undefined}>
+                {!this.path ?
+                    <button className={`audiobox-record${interactive}`} style={{ backgroundColor: ["black", "red", "blue"][this._audioState] }}>
+                        {this._audioState === 1 ? "STOP" : "RECORD"}
+                    </button> :
+                    this.audio
+                }
             </div>
         );
     }
diff --git a/src/client/views/pdf/Annotation.tsx b/src/client/views/pdf/Annotation.tsx
index e0a3b9171..2d8f47666 100644
--- a/src/client/views/pdf/Annotation.tsx
+++ b/src/client/views/pdf/Annotation.tsx
@@ -102,6 +102,7 @@ class RegionAnnotation extends React.Component<IRegionAnnotationProps> {
                 DocumentManager.Instance.FollowLink(undefined, annoGroup,
                     (doc: Doc, maxLocation: string) => this.props.addDocTab(doc, undefined, e.ctrlKey ? "inTab" : "onRight"),
                     false, false, undefined);
+                e.stopPropagation();
             }
         }
     }
diff --git a/src/server/authentication/models/current_user_utils.ts b/src/server/authentication/models/current_user_utils.ts
index 532f527cc..cf4ae4e6c 100644
--- a/src/server/authentication/models/current_user_utils.ts
+++ b/src/server/authentication/models/current_user_utils.ts
@@ -50,6 +50,7 @@ export class CurrentUserUtils {
             { title: "todo item", icon: "check", ignoreClick: true, drag: 'getCopy(this.dragFactory, true)', dragFactory: notes[notes.length - 1] },
             { title: "web page", icon: "globe-asia", ignoreClick: true, drag: 'Docs.Create.WebDocument("https://en.wikipedia.org/wiki/Hedgehog", { width: 300, height: 300, title: "New Webpage" })' },
             { title: "cat image", icon: "cat", ignoreClick: true, drag: 'Docs.Create.ImageDocument("https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg", { width: 200, title: "an image of a cat" })' },
+            { title: "record", icon: "microphone", ignoreClick: true, drag: 'Docs.Create.AudioDocument("https://actions.google.com/sounds/v1/alarms/beep_short.ogg", { width: 200, title: "ready to record audio" })' },
             { title: "clickable button", icon: "bolt", ignoreClick: true, drag: 'Docs.Create.ButtonDocument({ width: 150, height: 50, title: "Button" })' },
             { title: "presentation", icon: "tv", ignoreClick: true, drag: 'Doc.UserDoc().curPresentation = Docs.Create.PresDocument(new List<Doc>(), { width: 200, height: 500, title: "a presentation trail" })' },
             { title: "import folder", icon: "cloud-upload-alt", ignoreClick: true, drag: 'Docs.Create.DirectoryImportDocument({ title: "Directory Import", width: 400, height: 400 })' },
-- 
cgit v1.2.3-70-g09d2

