From ecf0f5b8f426db9e66c05e759f61294811b15fca Mon Sep 17 00:00:00 2001
From: vellichora <fangrui_tong@brown.edu>
Date: Sat, 1 Feb 2020 14:56:19 -0500
Subject: mobile ink overlay is draggable from desktop

---
 src/client/util/InteractionUtils.tsx | 182 +++++++++++++++++++++++++++++++++++
 1 file changed, 182 insertions(+)
 create mode 100644 src/client/util/InteractionUtils.tsx

(limited to 'src/client/util/InteractionUtils.tsx')

diff --git a/src/client/util/InteractionUtils.tsx b/src/client/util/InteractionUtils.tsx
new file mode 100644
index 000000000..34c7cfa74
--- /dev/null
+++ b/src/client/util/InteractionUtils.tsx
@@ -0,0 +1,182 @@
+import React = require("react");
+
+export namespace InteractionUtils {
+    export const MOUSETYPE = "mouse";
+    export const TOUCHTYPE = "touch";
+    export const PENTYPE = "pen";
+    export const ERASERTYPE = "eraser";
+
+    const POINTER_PEN_BUTTON = -1;
+    const REACT_POINTER_PEN_BUTTON = 0;
+    const ERASER_BUTTON = 5;
+
+    export function GetMyTargetTouches(e: TouchEvent | React.TouchEvent, prevPoints: Map<number, React.Touch>, ignorePen: boolean): React.Touch[] {
+        const myTouches = new Array<React.Touch>();
+        for (let i = 0; i < e.targetTouches.length; i++) {
+            const pt: any = e.targetTouches.item(i);
+            if (pt && prevPoints.has(pt.identifier)) {
+                if (ignorePen || (pt.radiusX > 1 && pt.radiusY > 1)) {
+                    myTouches.push(pt);
+                }
+            }
+        }
+        return myTouches;
+    }
+
+    // TODO: find a way to reference this function from InkingStroke instead of copy pastign here. copied bc of weird error when on mobile view
+    export function CreatePolyline(points: { X: number, Y: number }[], left: number, top: number, color: string, width: number) {
+        const pts = points.reduce((acc: string, pt: { X: number, Y: number }) => acc + `${pt.X - left},${pt.Y - top} `, "");
+        return (
+            <polyline
+                points={pts}
+                style={{
+                    fill: "none",
+                    stroke: color,
+                    strokeWidth: width
+                }}
+            />
+        );
+    }
+
+    export function IsType(e: PointerEvent | React.PointerEvent, type: string): boolean {
+        switch (type) {
+            // pen and eraser are both pointer type 'pen', but pen is button 0 and eraser is button 5. -syip2
+            case PENTYPE:
+                return e.pointerType === PENTYPE && (e.button === -1 || e.button === 0);
+            case ERASERTYPE:
+                return e.pointerType === PENTYPE && e.button === (e instanceof PointerEvent ? ERASER_BUTTON : ERASER_BUTTON);
+            default:
+                return e.pointerType === type;
+        }
+    }
+
+    export function TwoPointEuclidist(pt1: React.Touch, pt2: React.Touch): number {
+        return Math.sqrt(Math.pow(pt1.clientX - pt2.clientX, 2) + Math.pow(pt1.clientY - pt2.clientY, 2));
+    }
+
+    /**
+     * Returns the centroid of an n-arbitrary long list of points (takes the average the x and y components of each point)
+     * @param pts - n-arbitrary long list of points
+     */
+    export function CenterPoint(pts: React.Touch[]): { X: number, Y: number } {
+        const centerX = pts.map(pt => pt.clientX).reduce((a, b) => a + b, 0) / pts.length;
+        const centerY = pts.map(pt => pt.clientY).reduce((a, b) => a + b, 0) / pts.length;
+        return { X: centerX, Y: centerY };
+    }
+
+    /**
+     * Returns -1 if pinching out, 0 if not pinching, and 1 if pinching in
+     * @param pt1 - new point that corresponds to oldPoint1
+     * @param pt2 - new point that corresponds to oldPoint2
+     * @param oldPoint1 - previous point 1
+     * @param oldPoint2 - previous point 2
+     */
+    export function Pinching(pt1: React.Touch, pt2: React.Touch, oldPoint1: React.Touch, oldPoint2: React.Touch): number {
+        const threshold = 4;
+        const oldDist = TwoPointEuclidist(oldPoint1, oldPoint2);
+        const newDist = TwoPointEuclidist(pt1, pt2);
+
+        /** if they have the same sign, then we are either pinching in or out.
+          * threshold it by 10 (it has to be pinching by at least threshold to be a valid pinch)
+          * so that it can still pan without freaking out
+          */
+        if (Math.sign(oldDist) === Math.sign(newDist) && Math.abs(oldDist - newDist) > threshold) {
+            return Math.sign(oldDist - newDist);
+        }
+        return 0;
+    }
+
+    /**
+     * Returns -1 if pinning and pinching out, 0 if not pinning, and 1 if pinching in
+     * @param pt1 - new point that corresponds to oldPoint1
+     * @param pt2 - new point that corresponds to oldPoint2
+     * @param oldPoint1 - previous point 1
+     * @param oldPoint2 - previous point 2
+     */
+    export function Pinning(pt1: React.Touch, pt2: React.Touch, oldPoint1: React.Touch, oldPoint2: React.Touch): number {
+        const threshold = 4;
+
+        const pt1Dist = TwoPointEuclidist(oldPoint1, pt1);
+        const pt2Dist = TwoPointEuclidist(oldPoint2, pt2);
+
+        const pinching = Pinching(pt1, pt2, oldPoint1, oldPoint2);
+
+        if (pinching !== 0) {
+            if ((pt1Dist < threshold && pt2Dist > threshold) || (pt1Dist > threshold && pt2Dist < threshold)) {
+                return pinching;
+            }
+        }
+        return 0;
+    }
+
+    export function IsDragging(oldTouches: Map<number, React.Touch>, newTouches: React.Touch[], leniency: number): boolean {
+        for (const touch of newTouches) {
+            if (touch) {
+                const oldTouch = oldTouches.get(touch.identifier);
+                if (oldTouch) {
+                    if (TwoPointEuclidist(touch, oldTouch) >= leniency) {
+                        return true;
+                    }
+                }
+            }
+        }
+        return false;
+    }
+
+    // These might not be very useful anymore, but I'll leave them here for now -syip2
+    {
+
+
+        /**
+         * Returns the type of Touch Interaction from a list of points.
+         * Also returns any data that is associated with a Touch Interaction
+         * @param pts - List of points
+         */
+        // export function InterpretPointers(pts: React.Touch[]): { type: Opt<TouchInteraction>, data?: any } {
+        //     const leniency = 200;
+        //     switch (pts.length) {
+        //         case 1:
+        //             return { type: OneFinger };
+        //         case 2:
+        //             return { type: TwoSeperateFingers };
+        //         case 3:
+        //             let pt1 = pts[0];
+        //             let pt2 = pts[1];
+        //             let pt3 = pts[2];
+        //             if (pt1 && pt2 && pt3) {
+        //                 let dist12 = TwoPointEuclidist(pt1, pt2);
+        //                 let dist23 = TwoPointEuclidist(pt2, pt3);
+        //                 let dist13 = TwoPointEuclidist(pt1, pt3);
+        //                 console.log(`distances: ${dist12}, ${dist23}, ${dist13}`);
+        //                 let dist12close = dist12 < leniency;
+        //                 let dist23close = dist23 < leniency;
+        //                 let dist13close = dist13 < leniency;
+        //                 let xor2313 = dist23close ? !dist13close : dist13close;
+        //                 let xor = dist12close ? !xor2313 : xor2313;
+        //                 // three input xor because javascript doesn't have logical xor's
+        //                 if (xor) {
+        //                     let points: number[] = [];
+        //                     let min = Math.min(dist12, dist23, dist13);
+        //                     switch (min) {
+        //                         case dist12:
+        //                             points = [0, 1, 2];
+        //                             break;
+        //                         case dist23:
+        //                             points = [1, 2, 0];
+        //                             break;
+        //                         case dist13:
+        //                             points = [0, 2, 1];
+        //                             break;
+        //                     }
+        //                     return { type: TwoToOneFingers, data: points };
+        //                 }
+        //                 else {
+        //                     return { type: ThreeSeperateFingers, data: null };
+        //                 }
+        //             }
+        //         default:
+        //             return { type: undefined };
+        //     }
+        // }
+    }
+}
\ No newline at end of file
-- 
cgit v1.2.3-70-g09d2


From 26c973ecb47de86d94c39d71ad00ece38a85d623 Mon Sep 17 00:00:00 2001
From: bob <bcz@cs.brown.edu>
Date: Wed, 4 Mar 2020 18:20:46 -0500
Subject: changed  text sampling to marking every second to get audio to
 synchronize better

---
 src/client/util/InteractionUtils.tsx        |  1 -
 src/client/util/RichTextSchema.tsx          |  4 +-
 src/client/views/nodes/AudioBox.scss        |  2 +
 src/client/views/nodes/AudioBox.tsx         | 66 +++++++++++++++-------
 src/client/views/nodes/FormattedTextBox.tsx | 85 ++++++++++++++++++++---------
 src/client/views/nodes/PresBox.tsx          |  3 +-
 src/new_fields/Doc.ts                       |  1 +
 7 files changed, 112 insertions(+), 50 deletions(-)

(limited to 'src/client/util/InteractionUtils.tsx')

diff --git a/src/client/util/InteractionUtils.tsx b/src/client/util/InteractionUtils.tsx
index cf51b8e89..2eec02a42 100644
--- a/src/client/util/InteractionUtils.tsx
+++ b/src/client/util/InteractionUtils.tsx
@@ -108,7 +108,6 @@ export namespace InteractionUtils {
     }
 
     export function IsType(e: PointerEvent | React.PointerEvent, type: string): boolean {
-        console.log(e.button);
         switch (type) {
             // pen and eraser are both pointer type 'pen', but pen is button 0 and eraser is button 5. -syip2
             case PENTYPE:
diff --git a/src/client/util/RichTextSchema.tsx b/src/client/util/RichTextSchema.tsx
index a854406f4..0adf060ec 100644
--- a/src/client/util/RichTextSchema.tsx
+++ b/src/client/util/RichTextSchema.tsx
@@ -511,7 +511,7 @@ export const marks: { [index: string]: MarkSpec } = {
     user_mark: {
         attrs: {
             userid: { default: "" },
-            modified: { default: "when?" }, // 5 second intervals since 1970
+            modified: { default: "when?" }, // 1 second intervals since 1970
         },
         group: "inline",
         toDOM(node: any) {
@@ -527,7 +527,7 @@ export const marks: { [index: string]: MarkSpec } = {
     user_tag: {
         attrs: {
             userid: { default: "" },
-            modified: { default: "when?" }, // 5 second intervals since 1970
+            modified: { default: "when?" }, // 1 second intervals since 1970
             tag: { default: "" }
         },
         group: "inline",
diff --git a/src/client/views/nodes/AudioBox.scss b/src/client/views/nodes/AudioBox.scss
index 4516418a7..83cdf3574 100644
--- a/src/client/views/nodes/AudioBox.scss
+++ b/src/client/views/nodes/AudioBox.scss
@@ -29,6 +29,8 @@
     }
     .audiobox-record-interactive {
         pointer-events: all;
+        width:100%;
+        height:100%;
     }
     .audiobox-controls {
         width:100%;
diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx
index c4c6365e3..e2002a596 100644
--- a/src/client/views/nodes/AudioBox.tsx
+++ b/src/client/views/nodes/AudioBox.tsx
@@ -17,6 +17,8 @@ import { ContextMenu } from "../ContextMenu";
 import { Id } from "../../../new_fields/FieldSymbols";
 import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
 import { DocumentView } from "./DocumentView";
+import { Docs } from "../../documents/Documents";
+import { ComputedField } from "../../../new_fields/ScriptField";
 
 interface Window {
     MediaRecorder: MediaRecorder;
@@ -45,12 +47,15 @@ export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocume
     _recorder: any;
     _recordStart = 0;
 
+    public static START = 0;
     @observable private static _scrubTime = 0;
-    @observable private _audioState: "unrecorded" | "recording" | "recorded" = "unrecorded";
-    public static SetScrubTime = action((timeInMillisFrom1970: number) => AudioBox._scrubTime = timeInMillisFrom1970);
+    @computed get audioState(): undefined | "recording" | "paused" | "playing" { return this.dataDoc.audioState as (undefined | "recording" | "paused" | "playing"); }
+    set audioState(value) { this.dataDoc.audioState = value; }
+    public static SetScrubTime = (timeInMillisFrom1970: number) => { runInAction(() => AudioBox._scrubTime = 0); runInAction(() => AudioBox._scrubTime = timeInMillisFrom1970); };
     public static ActiveRecordings: Doc[] = [];
 
     @computed get recordingStart() { return Cast(this.dataDoc[this.props.fieldKey + "-recordingStart"], DateField)?.date.getTime(); }
+    async slideTemplate() { return (await Cast((await Cast(Doc.UserDoc().slidesBtn, Doc) as Doc).dragFactory, Doc) as Doc); }
 
     componentWillUnmount() {
         this._reactionDisposer?.();
@@ -58,7 +63,7 @@ export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocume
         this._scrubbingDisposer?.();
     }
     componentDidMount() {
-        runInAction(() => this._audioState = this.path ? "recorded" : "unrecorded");
+        runInAction(() => this.audioState = this.path ? "paused" : undefined);
         this._linkPlayDisposer = reaction(() => this.layoutDoc.scrollToLinkID,
             scrollLinkId => {
                 if (scrollLinkId) {
@@ -73,13 +78,14 @@ export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocume
             selected => {
                 const sel = selected.length ? selected[0].props.Document : undefined;
                 this.Document.playOnSelect && this.recordingStart && sel && !Doc.AreProtosEqual(sel, this.props.Document) && this.playFromTime(DateCast(sel.creationDate).date.getTime());
+                this.Document.playOnSelect && this.recordingStart && !sel && this.pause();
             });
         this._scrubbingDisposer = reaction(() => AudioBox._scrubTime, (time) => this.Document.playOnSelect && this.playFromTime(AudioBox._scrubTime));
     }
 
     timecodeChanged = () => {
         const htmlEle = this._ele;
-        if (this._audioState === "recorded" && htmlEle) {
+        if (this.audioState !== "recording" && htmlEle) {
             htmlEle.duration && htmlEle.duration !== Infinity && runInAction(() => this.dataDoc.duration = htmlEle.duration);
             DocListCast(this.dataDoc.links).map(l => {
                 let la1 = l.anchor1 as Doc;
@@ -98,7 +104,7 @@ export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocume
 
     pause = action(() => {
         this._ele!.pause();
-        this.props.Document._audioState = "paused";
+        this.audioState = "paused";
     });
 
     playFromTime = (absoluteTime: number) => {
@@ -107,11 +113,15 @@ export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocume
     playFrom = (seekTimeInSeconds: number) => {
         if (this._ele && AudioBox.Enabled) {
             if (seekTimeInSeconds < 0) {
-                this.pause();
+                if (seekTimeInSeconds > -1) {
+                    setTimeout(() => this.playFrom(0), -seekTimeInSeconds * 1000);
+                } else {
+                    this.pause();
+                }
             } else if (seekTimeInSeconds <= this._ele.duration) {
                 this._ele.currentTime = seekTimeInSeconds;
                 this._ele.play();
-                runInAction(() => this.props.Document._audioState = "playing");
+                runInAction(() => this.audioState = "playing");
             } else {
                 this.pause();
             }
@@ -120,7 +130,7 @@ export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocume
 
 
     updateRecordTime = () => {
-        if (this._audioState === "recording") {
+        if (this.audioState === "recording") {
             setTimeout(this.updateRecordTime, 30);
             this.Document.currentTimecode = (new Date().getTime() - this._recordStart) / 1000;
         }
@@ -135,6 +145,7 @@ export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocume
             gumStream = stream;
             self._recorder = new MediaRecorder(stream);
             self.dataDoc[self.props.fieldKey + "-recordingStart"] = new DateField(new Date());
+            AudioBox.START = new DateField(new Date()).date.getTime();
             AudioBox.ActiveRecordings.push(self.props.Document);
             self._recorder.ondataavailable = async function (e: any) {
                 const formData = new FormData();
@@ -151,8 +162,9 @@ export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocume
                     self.props.Document[self.props.fieldKey] = new AudioField(url);
                 });
             };
-            runInAction(() => self._audioState = "recording");
             self._recordStart = new Date().getTime();
+            console.log("RECORD START = " + self._recordStart);
+            runInAction(() => self.audioState = "recording");
             setTimeout(self.updateRecordTime, 0);
             self._recorder.start();
             setTimeout(() => {
@@ -172,7 +184,7 @@ export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocume
     stopRecording = action(() => {
         this._recorder.stop();
         this.dataDoc.duration = (new Date().getTime() - this._recordStart) / 1000;
-        this._audioState = "recorded";
+        this.audioState = "paused";
         const ind = AudioBox.ActiveRecordings.indexOf(this.props.Document);
         ind !== -1 && (AudioBox.ActiveRecordings.splice(ind, 1));
     });
@@ -189,8 +201,19 @@ export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocume
         e.stopPropagation();
     }
     onStop = (e: any) => {
-        this.pause();
-        this._ele!.currentTime = 0;
+        this.Document.playOnSelect = !this.Document.playOnSelect;
+        e.stopPropagation();
+    }
+    onFile = (e: any) => {
+        const newDoc = Docs.Create.TextDocument("", {
+            title: "", _chromeStatus: "disabled",
+            x: NumCast(this.props.Document.x), y: NumCast(this.props.Document.y) + NumCast(this.props.Document._height) + 10,
+            _width: NumCast(this.props.Document._width), _height: 3 * NumCast(this.props.Document._height)
+        });
+        Doc.GetProto(newDoc).recordingSource = this.dataDoc;
+        Doc.GetProto(newDoc).recordingStart = 0;
+        Doc.GetProto(newDoc).audioState = ComputedField.MakeFunction("this.recordingSource.audioState");
+        this.props.addDocument?.(newDoc);
         e.stopPropagation();
     }
 
@@ -218,21 +241,26 @@ export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocume
         const interactive = this.active() ? "-interactive" : "";
         return <div className={`audiobox-container`} onContextMenu={this.specificContextMenu}
             onClick={!this.path ? this.recordClick : undefined}>
-            <div className="audiobox-handle" style={{ backgroundColor: this._audioState === "recording" ? "green" : undefined }} />
             {!this.path ?
-                <button className={`audiobox-record${interactive}`} style={{ backgroundColor: this._audioState === "recording" ? "red" : "black" }}>
-                    {this._audioState === "recording" ? "STOP" : "RECORD"}
-                </button> :
+                <div style={{ display: "flex", width: "100%", alignItems: "center" }}>
+                    <div className="audiobox-playhead" style={{ alignItems: "center", display: "inherit", background: "dimgray" }} onClick={this.onFile}>
+                        <FontAwesomeIcon style={{ width: "30px", background: this.Document.playOnSelect ? "yellow" : "dimGray" }} icon="file-alt" size={this.props.PanelHeight() < 36 ? "1x" : "2x"} />
+                    </div>
+                    <button className={`audiobox-record${interactive}`} style={{ position: "relative", backgroundColor: this.audioState === "recording" ? "red" : "black" }}>
+                        {this.audioState === "recording" ? "STOP" : "RECORD"}
+                    </button>
+                </div> :
                 <div className="audiobox-controls">
                     <div className="audiobox-player" onClick={this.onPlay}>
-                        <div className="audiobox-playhead"> <FontAwesomeIcon style={{ width: "100%" }} icon={this.props.Document._audioState === "paused" ? "play" : "pause"} size={this.props.PanelHeight() < 36 ? "1x" : "2x"} /></div>
-                        <div className="audiobox-playhead" onClick={this.onStop}><FontAwesomeIcon style={{ width: "100%" }} icon="stop" size={this.props.PanelHeight() < 36 ? "1x" : "2x"} /></div>
+                        <div className="audiobox-playhead"> <FontAwesomeIcon style={{ width: "100%" }} icon={this.audioState === "paused" ? "play" : "pause"} size={this.props.PanelHeight() < 36 ? "1x" : "2x"} /></div>
+                        <div className="audiobox-playhead" onClick={this.onStop}><FontAwesomeIcon style={{ width: "100%", background: this.Document.playOnSelect ? "yellow" : "dimGray" }} icon="hand-point-left" size={this.props.PanelHeight() < 36 ? "1x" : "2x"} /></div>
                         <div className="audiobox-timeline" onClick={e => e.stopPropagation()}
                             onPointerDown={e => {
                                 if (e.button === 0 && !e.ctrlKey) {
                                     const rect = (e.target as any).getBoundingClientRect();
+                                    const wasPaused = this.audioState === "paused";
                                     this._ele!.currentTime = this.Document.currentTimecode = (e.clientX - rect.x) / rect.width * NumCast(this.dataDoc.duration);
-                                    this.pause();
+                                    wasPaused && this.pause();
                                     e.stopPropagation();
                                 }
                             }} >
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index 8f4cefbf4..f18183600 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -20,7 +20,7 @@ import { InkTool } from '../../../new_fields/InkField';
 import { RichTextField } from "../../../new_fields/RichTextField";
 import { RichTextUtils } from '../../../new_fields/RichTextUtils';
 import { createSchema, makeInterface } from "../../../new_fields/Schema";
-import { Cast, NumCast, StrCast } from "../../../new_fields/Types";
+import { Cast, NumCast, StrCast, BoolCast } from "../../../new_fields/Types";
 import { TraceMobx } from '../../../new_fields/util';
 import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, emptyFunction, numberRange, returnOne, Utils } from '../../../Utils';
 import { GoogleApiClientUtils, Pulls, Pushes } from '../../apis/google_docs/GoogleApiClientUtils';
@@ -82,6 +82,7 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
     private _lastY = 0;
     private _undoTyping?: UndoManager.Batch;
     private _searchReactionDisposer?: Lambda;
+    private _recordReactionDisposer: Opt<IReactionDisposer>;
     private _scrollToRegionReactionDisposer: Opt<IReactionDisposer>;
     private _reactionDisposer: Opt<IReactionDisposer>;
     private _heightReactionDisposer: Opt<IReactionDisposer>;
@@ -92,6 +93,9 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
     private _scrollDisposer: Opt<IReactionDisposer>;
     private dropDisposer?: DragManager.DragDropDisposer;
 
+    @computed get _recording() { return this.dataDoc.audioState === "recording"; }
+    set _recording(value) { this.dataDoc.audioState = value ? "recording" : undefined; }
+
     @observable private _entered = false;
 
     public static FocusedBox: FormattedTextBox | undefined;
@@ -186,7 +190,7 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
             (tx.storedMarks && !this._editorView.state.storedMarks) && (this._editorView.state.storedMarks = tx.storedMarks);
 
             const tsel = this._editorView.state.selection.$from;
-            tsel.marks().filter(m => m.type === this._editorView!.state.schema.marks.user_mark).map(m => AudioBox.SetScrubTime(Math.max(0, m.attrs.modified * 5000 - 1000)));
+            tsel.marks().filter(m => m.type === this._editorView!.state.schema.marks.user_mark).map(m => AudioBox.SetScrubTime(Math.max(0, m.attrs.modified * 1000)));
             const curText = state.doc.textBetween(0, state.doc.content.size, "\n\n");
             const curTemp = Cast(this.props.Document[this.props.fieldKey + "-textTemplate"], RichTextField);
             if (!this._applyingChange) {
@@ -387,7 +391,7 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
         !this.props.Document.expandedTemplate && funcs.push({ description: "Make Template", event: () => { this.props.Document.isTemplateDoc = true; Doc.AddDocToList(Cast(Doc.UserDoc().noteTypes, Doc, null), "data", this.props.Document); }, icon: "eye" });
         funcs.push({ description: "Toggle Single Line", event: () => this.props.Document._singleLine = !this.props.Document._singleLine, icon: "expand-arrows-alt" });
         funcs.push({ description: "Toggle Sidebar", event: () => this.props.Document._showSidebar = !this.props.Document._showSidebar, icon: "expand-arrows-alt" });
-        funcs.push({ description: "Record Bullet", event: () => this.recordBullet(), icon: "expand-arrows-alt" });
+        funcs.push({ description: "Toggle Audio", event: () => this.props.Document._showAudio = !this.props.Document._showAudio, icon: "expand-arrows-alt" });
         funcs.push({ description: "Toggle Menubar", event: () => this.toggleMenubar(), icon: "expand-arrows-alt" });
         ["My Text", "Text from Others", "Todo Items", "Important Items", "Ignore Items", "Disagree Items", "By Recent Minute", "By Recent Hour"].forEach(option =>
             funcs.push({
@@ -405,12 +409,7 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
         ContextMenu.Instance.addItem({ description: "Text Funcs...", subitems: funcs, icon: "asterisk" });
     }
 
-    @observable _recording = false;
-
     recordDictation = () => {
-        //this._editorView!.focus();
-        if (this._recording) return;
-        runInAction(() => this._recording = true);
         DictationManager.Controls.listen({
             interimHandler: this.setCurrentBulletContent,
             continuous: { indefinite: false },
@@ -418,13 +417,10 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
             if (results && [DictationManager.Controls.Infringed].includes(results)) {
                 DictationManager.Controls.stop();
             }
-            this._editorView!.focus();
+            //this._editorView!.focus();
         });
     }
-    stopDictation = (abort: boolean) => {
-        runInAction(() => this._recording = false);
-        DictationManager.Controls.stop(!abort);
-    }
+    stopDictation = (abort: boolean) => { DictationManager.Controls.stop(!abort); }
 
     @action
     toggleMenubar = () => {
@@ -449,12 +445,28 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
     setCurrentBulletContent = (value: string) => {
         if (this._editorView) {
             let state = this._editorView.state;
+            let now = Date.now();
+            if (NumCast(this.props.Document.recordingStart, -1) === 0) {
+                this.props.Document.recordingStart = now = AudioBox.START;
+            }
+            console.log("NOW = " + (now - AudioBox.START) / 1000);
+            let mark = schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.floor(now / 1000) });
+            if (!this._break && state.selection.to !== state.selection.from) {
+                for (let i = state.selection.from; i <= state.selection.to; i++) {
+                    const pos = state.doc.resolve(i);
+                    const um = Array.from(pos.marks()).find(m => m.type === schema.marks.user_mark);
+                    if (um) {
+                        mark = um;
+                        break;
+                    }
+                }
+            }
+            this._break = false;
+            console.log("start = " + (mark.attrs.modified * 1000 - AudioBox.START) / 1000);
+            value = "" + (mark.attrs.modified * 1000 - AudioBox.START) / 1000 + value;
             const from = state.selection.from;
-            const to = state.selection.to;
-            this._editorView.dispatch(state.tr.insertText(value, from, to));
-            state = this._editorView.state;
-            const updated = TextSelection.create(state.doc, from, from + value.length);
-            this._editorView.dispatch(state.tr.setSelection(updated));
+            const inserted = state.tr.insertText(value).addMark(from, from + value.length + 1, mark);
+            this._editorView.dispatch(inserted.setSelection(TextSelection.create(inserted.doc, from, from + value.length + 1)));
         }
     }
 
@@ -558,6 +570,17 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
             search => search ? this.highlightSearchTerms([Doc.SearchQuery()]) : this.unhighlightSearchTerms(),
             { fireImmediately: true });
 
+        this._recordReactionDisposer = reaction(() => this._recording,
+            () => {
+                if (this._recording) {
+                    setTimeout(action(() => {
+                        this.stopDictation(true);
+                        setTimeout(() => this.recordDictation(), 500);
+                    }), 500);
+                } else setTimeout(() => this.stopDictation(true), 0);
+            }
+        );
+
         this._scrollToRegionReactionDisposer = reaction(
             () => StrCast(this.layoutDoc.scrollToLinkID),
             async (scrollToLinkID) => {
@@ -807,7 +830,7 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
         }
         (selectOnLoad /* || !rtfField?.Text*/) && this._editorView!.focus();
         // add user mark for any first character that was typed since the user mark that gets set in KeyPress won't have been called yet.
-        this._editorView!.state.storedMarks = [...(this._editorView!.state.storedMarks ? this._editorView!.state.storedMarks : []), schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.round(Date.now() / 1000 / 5) })];
+        this._editorView!.state.storedMarks = [...(this._editorView!.state.storedMarks ? this._editorView!.state.storedMarks : []), schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.floor(Date.now() / 1000) })];
     }
     getFont(font: string) {
         switch (font) {
@@ -831,6 +854,7 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
         this._pullReactionDisposer?.();
         this._heightReactionDisposer?.();
         this._searchReactionDisposer?.();
+        this._recordReactionDisposer?.();
         this._buttonBarReactionDisposer?.();
         this._editorView?.destroy();
     }
@@ -838,7 +862,19 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
     static _downEvent: any;
     _downX = 0;
     _downY = 0;
+    _break = false;
     onPointerDown = (e: React.PointerEvent): void => {
+        if (this._recording && !e.ctrlKey && e.button === 0) {
+            this.stopDictation(true);
+            this._break = true;
+            let state = this._editorView!.state;
+            const to = state.selection.to;
+            const updated = TextSelection.create(state.doc, to, to);
+            this._editorView!.dispatch(this._editorView!.state.tr.setSelection(updated).insertText("\n", to));
+            e.preventDefault();
+            e.stopPropagation();
+            if (this._recording) setTimeout(() => this.recordDictation(), 500);
+        }
         this._downX = e.clientX;
         this._downY = e.clientY;
         this.doLinkOnDeselect();
@@ -955,7 +991,6 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
             this.props.select(e.ctrlKey);
             this.hitBulletTargets(e.clientX, e.clientY, e.shiftKey, false);
         }
-        if (this._recording) setTimeout(() => { this.stopDictation(true); setTimeout(() => this.recordDictation(), 500); }, 500);
     }
 
     // this hackiness handles clicking on the list item bullets to do expand/collapse.  the bullets are ::before pseudo elements so there's no real way to hit test against them.
@@ -1062,17 +1097,13 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
         if (e.key === "Tab" || e.key === "Enter") {
             e.preventDefault();
         }
-        const mark = e.key !== " " && this._lastTimedMark ? this._lastTimedMark : schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.round(Date.now() / 1000 / 5) });
+        const mark = e.key !== " " && this._lastTimedMark ? this._lastTimedMark : schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.floor(Date.now() / 1000) });
         this._lastTimedMark = mark;
         this._editorView!.dispatch(this._editorView!.state.tr.removeStoredMark(schema.marks.user_mark.create({})).addStoredMark(mark));
 
         if (!this._undoTyping) {
             this._undoTyping = UndoManager.StartBatch("undoTyping");
         }
-        if (this._recording) {
-            this.stopDictation(true);
-            setTimeout(() => this.recordDictation(), 250);
-        }
     }
 
     onscrolled = (ev: React.UIEvent) => {
@@ -1169,8 +1200,8 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
                     </div>}
                 {!this.props.Document._showAudio ? (null) :
                     <div className="formattedTextBox-dictation"
-                        onClick={e => {
-                            this._recording ? this.stopDictation(true) : this.recordDictation();
+                        onPointerDown={e => {
+                            runInAction(() => this._recording = !this._recording);
                             setTimeout(() => this._editorView!.focus(), 500);
                             e.stopPropagation();
                         }} >
diff --git a/src/client/views/nodes/PresBox.tsx b/src/client/views/nodes/PresBox.tsx
index 27c2d6957..d43df0bfb 100644
--- a/src/client/views/nodes/PresBox.tsx
+++ b/src/client/views/nodes/PresBox.tsx
@@ -1,6 +1,6 @@
 import React = require("react");
 import { library } from '@fortawesome/fontawesome-svg-core';
-import { faArrowLeft, faArrowRight, faEdit, faMinus, faPlay, faPlus, faStop, faTimes } from '@fortawesome/free-solid-svg-icons';
+import { faArrowLeft, faArrowRight, faEdit, faMinus, faPlay, faPlus, faStop, faHandPointLeft, faTimes } from '@fortawesome/free-solid-svg-icons';
 import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
 import { action, computed, IReactionDisposer, observable, reaction, runInAction } from "mobx";
 import { observer } from "mobx-react";
@@ -24,6 +24,7 @@ library.add(faArrowLeft);
 library.add(faArrowRight);
 library.add(faPlay);
 library.add(faStop);
+library.add(faHandPointLeft);
 library.add(faPlus);
 library.add(faTimes);
 library.add(faMinus);
diff --git a/src/new_fields/Doc.ts b/src/new_fields/Doc.ts
index e9fdf42b9..141a01ed2 100644
--- a/src/new_fields/Doc.ts
+++ b/src/new_fields/Doc.ts
@@ -859,6 +859,7 @@ export namespace Doc {
         const targetDoc = doc && Doc.GetProto(Cast(doc.expandedTemplate, Doc, null) || doc);
         targetDoc && (targetDoc.backgroundColor = ComputedField.MakeFunction(`options.data.find(doc => doc.title === (this.expandedTemplate||this)["${enumeratedFieldKey}"])?._backgroundColor || "white"`, undefined, { options }));
         targetDoc && (targetDoc.color = ComputedField.MakeFunction(`options.data.find(doc => doc.title === (this.expandedTemplate||this)["${enumeratedFieldKey}"]).color || "black"`, undefined, { options }));
+        targetDoc && (targetDoc.color = ComputedField.MakeFunction(`options.data.find(doc => doc.title === (this.expandedTemplate||this)["${enumeratedFieldKey}"]).borderRounding`, undefined, { options }));
         enumerations.map(enumeration => {
             const found = DocListCast(options.data).find(d => d.title === enumeration.title);
             if (found) {
-- 
cgit v1.2.3-70-g09d2

