aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authoranika-ahluwalia <anika.ahluwalia@gmail.com>2020-08-09 18:42:04 -0500
committeranika-ahluwalia <anika.ahluwalia@gmail.com>2020-08-09 18:42:04 -0500
commitb3b760cecc3fb2f455b63c7ccdd438ab66ac63fa (patch)
tree81142fcd22649a191f2f6186165c8ec5f5d56933 /src
parentfa01f623ef7db8e22f7fd055d4e339feba6c34df (diff)
parentbe7011f5ba6b45b4bff21e73e6c4226c909d446a (diff)
Merge branch 'master' of https://github.com/browngraphicslab/Dash-Web into menu_restructure
Diffstat (limited to 'src')
-rw-r--r--src/Utils.ts8
-rw-r--r--src/client/apis/HypothesisAuthenticationManager.tsx160
-rw-r--r--src/client/documents/Documents.ts3
-rw-r--r--src/client/util/CurrentUserUtils.ts3
-rw-r--r--src/client/util/HypothesisUtils.ts170
-rw-r--r--src/client/util/SettingsManager.tsx3
-rw-r--r--src/client/views/GlobalKeyHandler.ts2
-rw-r--r--src/client/views/MainView.tsx93
-rw-r--r--src/client/views/collections/CollectionLinearView.tsx2
-rw-r--r--src/client/views/collections/CollectionSubView.tsx32
-rw-r--r--src/client/views/collections/collectionFreeForm/MarqueeView.tsx2
-rw-r--r--src/client/views/linking/LinkEditor.tsx6
-rw-r--r--src/client/views/linking/LinkMenuItem.tsx33
-rw-r--r--src/client/views/nodes/AudioBox.scss26
-rw-r--r--src/client/views/nodes/AudioBox.tsx342
-rw-r--r--src/client/views/nodes/DocumentLinksButton.tsx122
-rw-r--r--src/server/ApiManagers/HypothesisManager.ts44
-rw-r--r--src/server/ApiManagers/UploadManager.ts1
-rw-r--r--src/server/apis/google/GoogleApiServerUtils.ts1
-rw-r--r--src/server/database.ts2
-rw-r--r--src/server/index.ts3
21 files changed, 549 insertions, 509 deletions
diff --git a/src/Utils.ts b/src/Utils.ts
index 6608bb176..d9a5353e8 100644
--- a/src/Utils.ts
+++ b/src/Utils.ts
@@ -514,7 +514,7 @@ export function clearStyleSheetRules(sheet: any) {
return false;
}
-export function simulateMouseClick(element: Element, x: number, y: number, sx: number, sy: number) {
+export function simulateMouseClick(element: Element, x: number, y: number, sx: number, sy: number, rightClick = true) {
["pointerdown", "pointerup"].map(event => element.dispatchEvent(
new PointerEvent(event, {
view: window,
@@ -528,7 +528,7 @@ export function simulateMouseClick(element: Element, x: number, y: number, sx: n
screenY: sy,
})));
- element.dispatchEvent(
+ rightClick && element.dispatchEvent(
new MouseEvent("contextmenu", {
view: window,
bubbles: true,
@@ -547,7 +547,7 @@ export function setupMoveUpEvents(
target: object,
e: React.PointerEvent,
moveEvent: (e: PointerEvent, down: number[], delta: number[]) => boolean,
- upEvent: (e: PointerEvent) => void,
+ upEvent: (e: PointerEvent, movement: number[]) => void,
clickEvent: (e: PointerEvent, doubleTap?: boolean) => void,
stopPropagation: boolean = true,
stopMovePropagation: boolean = true
@@ -571,7 +571,7 @@ export function setupMoveUpEvents(
const _upEvent = (e: PointerEvent): void => {
(target as any)._doubleTap = (Date.now() - (target as any)._lastTap < 300);
(target as any)._lastTap = Date.now();
- upEvent(e);
+ upEvent(e, [e.clientX - (target as any)._downX, e.clientY - (target as any)._downY]);
if (Math.abs(e.clientX - (target as any)._downX) < 4 && Math.abs(e.clientY - (target as any)._downY) < 4) {
clickEvent(e, (target as any)._doubleTap);
}
diff --git a/src/client/apis/HypothesisAuthenticationManager.tsx b/src/client/apis/HypothesisAuthenticationManager.tsx
deleted file mode 100644
index bc95b5f9a..000000000
--- a/src/client/apis/HypothesisAuthenticationManager.tsx
+++ /dev/null
@@ -1,160 +0,0 @@
-import { observable, action, reaction, runInAction, IReactionDisposer } from "mobx";
-import { observer } from "mobx-react";
-import * as React from "react";
-import MainViewModal from "../views/MainViewModal";
-import { Opt } from "../../fields/Doc";
-import { Networking } from "../Network";
-import "./HypothesisAuthenticationManager.scss";
-import { Scripting } from "../util/Scripting";
-
-const prompt = "Paste authorization code here...";
-
-@observer
-export default class HypothesisAuthenticationManager extends React.Component<{}> {
- public static Instance: HypothesisAuthenticationManager;
- private authenticationLink: Opt<string> = undefined;
- @observable private openState = false;
- @observable private authenticationCode: Opt<string> = undefined;
- @observable private showPasteTargetState = false;
- @observable private success: Opt<boolean> = undefined;
- @observable private displayLauncher = true;
- @observable private credentials: string = "";
- private disposer: Opt<IReactionDisposer>;
-
- private set isOpen(value: boolean) {
- runInAction(() => this.openState = value);
- }
-
- private set shouldShowPasteTarget(value: boolean) {
- runInAction(() => this.showPasteTargetState = value);
- }
-
- public cancel() {
- this.openState && this.resetState(0, 0);
- }
-
- public fetchAccessToken = async (displayIfFound = false) => {
- const response: any = await Networking.FetchFromServer("/readHypothesisAccessToken");
- // if this is an authentication url, activate the UI to register the new access token
- if (!response) { // new RegExp(AuthenticationUrl).test(response)) {
- this.isOpen = true;
- this.authenticationLink = response;
- return new Promise<string>(async resolve => {
- this.disposer?.();
- this.disposer = reaction(
- () => this.authenticationCode,
- async authenticationCode => {
- if (authenticationCode) {
- this.disposer?.();
- Networking.PostToServer("/writeHypothesisAccessToken", { authenticationCode });
- runInAction(() => {
- this.success = true;
- this.credentials = response;
- });
- this.resetState();
- resolve(authenticationCode);
- }
- }
- );
- });
- }
-
- if (displayIfFound) {
- runInAction(() => {
- this.success = true;
- this.credentials = response;
- });
- this.resetState(-1, -1);
- this.isOpen = true;
- }
- return response.access_token;
- }
-
- resetState = action((visibleForMS: number = 3000, fadesOutInMS: number = 500) => {
- if (!visibleForMS && !fadesOutInMS) {
- runInAction(() => {
- this.isOpen = false;
- this.success = undefined;
- this.displayLauncher = true;
- this.credentials = "";
- this.shouldShowPasteTarget = false;
- this.authenticationCode = undefined;
- });
- return;
- }
- this.authenticationCode = undefined;
- this.displayLauncher = false;
- this.shouldShowPasteTarget = false;
- if (visibleForMS > 0 && fadesOutInMS > 0) {
- setTimeout(action(() => {
- this.isOpen = false;
- setTimeout(action(() => {
- this.success = undefined;
- this.displayLauncher = true;
- this.credentials = "";
- }), fadesOutInMS);
- }), visibleForMS);
- }
- });
-
- constructor(props: {}) {
- super(props);
- HypothesisAuthenticationManager.Instance = this;
- }
-
- private get renderPrompt() {
- return (
- <div className={'authorize-container'}>
-
- {this.displayLauncher ? <button
- className={"dispatch"}
- onClick={() => {
- this.shouldShowPasteTarget = true;
- }}
- style={{ marginBottom: this.showPasteTargetState ? 15 : 0 }}
- >Authorize a Hypothesis account...</button> : (null)}
- {this.showPasteTargetState ? <input
- className={'paste-target'}
- onChange={action(e => this.authenticationCode = e.currentTarget.value)}
- placeholder={prompt}
- /> : (null)}
- {this.credentials ?
- <>
- <span
- className={'welcome'}
- >Welcome to Dash, {this.credentials}
- </span>
- <div
- className={'disconnect'}
- onClick={async () => {
- await Networking.FetchFromServer("/revokeHypothesisAccessToken");
- this.resetState(0, 0);
- }}
- >Disconnect Account</div>
- </> : (null)}
- </div>
- );
- }
-
- private get dialogueBoxStyle() {
- const borderColor = this.success === undefined ? "black" : this.success ? "green" : "red";
- return { borderColor, transition: "0.2s borderColor ease", zIndex: 1002 };
- }
-
- render() {
- return (
- <MainViewModal
- isDisplayed={this.openState}
- interactive={true}
- contents={this.renderPrompt}
- // overlayDisplayedOpacity={0.9}
- dialogueBoxStyle={this.dialogueBoxStyle}
- overlayStyle={{ zIndex: 1001 }}
- closeOnExternalClick={action(() => this.isOpen = false)}
- />
- );
- }
-
-}
-
-Scripting.addGlobal("HypothesisAuthenticationManager", HypothesisAuthenticationManager); \ No newline at end of file
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index f902da0a2..070068401 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -722,7 +722,7 @@ export namespace Docs {
}
export function WebDocument(url: string, options: DocumentOptions = {}) {
- return InstanceFromProto(Prototypes.get(DocumentType.WEB), url ? new WebField(new URL(url)) : undefined, { _fitWidth: true, _chromeStatus: url ? "disabled" : "enabled", isAnnotating: true, _lockedTransform: true, ...options });
+ return InstanceFromProto(Prototypes.get(DocumentType.WEB), url ? new WebField(new URL(url)) : undefined, { _fitWidth: true, _chromeStatus: url ? "disabled" : "enabled", isAnnotating: false, _lockedTransform: true, ...options });
}
export function HtmlDocument(html: string, options: DocumentOptions = {}) {
@@ -943,7 +943,6 @@ export namespace DocUtils {
return linkDoc;
}
-
export function DocumentFromField(target: Doc, fieldKey: string, proto?: Doc, options?: DocumentOptions): Doc | undefined {
let created: Doc | undefined;
let layout: ((fieldKey: string) => string) | undefined;
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index 45d8da911..b2fb1e33a 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -447,8 +447,9 @@ export class CurrentUserUtils {
// { title: "use stamp", icon: "stamp", click: 'activateStamp(this.activeInkPen = sameDocs(this.activeInkPen, this) ? undefined : this)', backgroundColor: "orange", ischecked: `sameDocs(this.activeInkPen, this)`, activeInkPen: doc },
// { title: "use eraser", icon: "eraser", click: 'activateEraser(this.activeInkPen = sameDocs(this.activeInkPen, this) ? undefined : this);', ischecked: `sameDocs(this.activeInkPen, this)`, backgroundColor: "pink", activeInkPen: doc },
// { title: "use drag", icon: "mouse-pointer", click: 'deactivateInk();this.activeInkPen = this;', ischecked: `sameDocs(this.activeInkPen, this)`, backgroundColor: "white", activeInkPen: doc },
- { toolTip: "Drag a document previewer", title: "Prev", icon: "expand", click: 'openOnRight(getCopy(this.dragFactory, true))', drag: 'getCopy(this.dragFactory, true)', dragFactory: doc.emptyDocHolder as Doc },
+ { toolTip: "Drag a document previewer", title: "Prev", icon: "expand", click: 'openOnRight(getCopy(this.dragFactory, true))', drag: 'getCopy(this.dragFactory,true)', dragFactory: doc.emptyDocHolder as Doc },
{ toolTip: "Toggle a Calculator REPL", title: "repl", icon: "calculator", click: 'addOverlayWindow("ScriptingRepl", { x: 300, y: 100, width: 200, height: 200, title: "Scripting REPL" })' },
+ { toolTip: "Connect a Google Account", title: "Google Account", icon: "external-link-alt", click: 'GoogleAuthenticationManager.Instance.fetchOrGenerateAccessToken(true)' },
];
}
diff --git a/src/client/util/HypothesisUtils.ts b/src/client/util/HypothesisUtils.ts
new file mode 100644
index 000000000..9ede94e4b
--- /dev/null
+++ b/src/client/util/HypothesisUtils.ts
@@ -0,0 +1,170 @@
+import { StrCast, Cast } from "../../fields/Types";
+import { SearchUtil } from "./SearchUtil";
+import { action, runInAction } from "mobx";
+import { Doc, Opt } from "../../fields/Doc";
+import { DocumentType } from "../documents/DocumentTypes";
+import { Docs } from "../documents/Documents";
+import { SelectionManager } from "./SelectionManager";
+import { WebField } from "../../fields/URLField";
+import { DocumentManager } from "./DocumentManager";
+import { DocumentLinksButton } from "../views/nodes/DocumentLinksButton";
+import { simulateMouseClick, Utils } from "../../Utils";
+import { DocumentView } from "../views/nodes/DocumentView";
+import { Id } from "../../fields/FieldSymbols";
+
+export namespace Hypothesis {
+
+ /**
+ * Retrieve a WebDocument with the given url, prioritizing results that are on screen.
+ * If none exist, create and return a new WebDocument.
+ */
+ export const getSourceWebDoc = async (uri: string) => {
+ const result = await findWebDoc(uri);
+ console.log(result ? "existing doc found" : "existing doc NOT found");
+ return result || Docs.Create.WebDocument(uri, { title: uri, _nativeWidth: 850, _nativeHeight: 962, _width: 400, UseCors: true }); // create and return a new Web doc with given uri if no matching docs are found
+ };
+
+
+ /**
+ * Search for a WebDocument whose url field matches the given uri, return undefined if not found
+ */
+ export const findWebDoc = async (uri: string) => {
+ const currentDoc = SelectionManager.SelectedDocuments().length && SelectionManager.SelectedDocuments()[0].props.Document;
+ if (currentDoc && Cast(currentDoc.data, WebField)?.url.href === uri) return currentDoc; // always check first whether the currently selected doc is the annotation's source, only use Search otherwise
+
+ const results: Doc[] = [];
+ await SearchUtil.Search("web", true).then(action(async (res: SearchUtil.DocSearchResult) => {
+ const docs = await Promise.all(res.docs.map(async doc => (await Cast(doc.extendsDoc, Doc)) || doc));
+ const filteredDocs = docs.filter(doc =>
+ doc.author === Doc.CurrentUserEmail && doc.type === DocumentType.WEB && doc.data
+ );
+ filteredDocs.forEach(doc => {
+ uri === Cast(doc.data, WebField)?.url.href && results.push(doc); // TODO check visited sites history?
+ });
+ }));
+
+ const onScreenResults = results.filter(doc => DocumentManager.Instance.getFirstDocumentView(doc));
+ return onScreenResults.length ? onScreenResults[0] : (results.length ? results[0] : undefined); // prioritize results that are currently on the screen
+ };
+
+ /**
+ * listen for event from Hypothes.is plugin to link an annotation to Dash
+ */
+ export const linkListener = async (e: any) => {
+ const annotationId: string = e.detail.id;
+ const annotationUri: string = StrCast(e.detail.uri).split("#annotations:")[0]; // clean hypothes.is URLs that reference a specific annotation
+ const sourceDoc: Doc = await getSourceWebDoc(annotationUri);
+
+ if (!DocumentLinksButton.StartLink || sourceDoc === DocumentLinksButton.StartLink) { // start new link if there were none already started, or if the old startLink came from the same web document (prevent links to itself)
+ runInAction(() => {
+ DocumentLinksButton.AnnotationId = annotationId;
+ DocumentLinksButton.AnnotationUri = annotationUri;
+ DocumentLinksButton.StartLink = sourceDoc;
+ });
+ } else { // if a link has already been started, complete the link to sourceDoc
+ runInAction(() => {
+ DocumentLinksButton.AnnotationId = annotationId;
+ DocumentLinksButton.AnnotationUri = annotationUri;
+ });
+ const endLinkView = DocumentManager.Instance.getFirstDocumentView(sourceDoc);
+ const rect = document.body.getBoundingClientRect();
+ const x = rect.x + rect.width / 2;
+ const y = 250;
+ DocumentLinksButton.finishLinkClick(x, y, DocumentLinksButton.StartLink, sourceDoc, false, endLinkView);
+ }
+ };
+
+ /**
+ * Send message to Hypothes.is client to edit an annotation to add a Dash hyperlink
+ */
+ export const makeLink = async (title: string, url: string, annotationId: string, annotationSourceDoc: Doc) => {
+ // if the annotation's source webpage isn't currently loaded in Dash, we're not able to access and edit the annotation from the client
+ // so we're loading the webpage and its annotations invisibly in a WebBox in MainView.tsx, until the editing is done
+ !DocumentManager.Instance.getFirstDocumentView(annotationSourceDoc) && runInAction(() => DocumentLinksButton.invisibleWebDoc = annotationSourceDoc);
+
+ var success = false;
+ const onSuccess = action(() => {
+ console.log("Edit success!!");
+ success = true;
+ clearTimeout(interval);
+ DocumentLinksButton.invisibleWebDoc = undefined;
+ document.removeEventListener("editSuccess", onSuccess);
+ });
+
+ const newHyperlink = `[${title}\n](${url})`;
+ const interval = setInterval(() => // keep trying to edit until annotations have loaded and editing is successful
+ !success && document.dispatchEvent(new CustomEvent<{ newHyperlink: string, id: string }>("addLink", {
+ detail: { newHyperlink: newHyperlink, id: annotationId },
+ bubbles: true
+ })), 300);
+
+ setTimeout(action(() => {
+ if (!success) {
+ clearInterval(interval);
+ DocumentLinksButton.invisibleWebDoc = undefined;
+ }
+ }), 10000); // give up if no success after 10s
+ document.addEventListener("editSuccess", onSuccess);
+ };
+
+ /**
+ * Send message Hypothes.is client request to edit an annotation to find and delete the target Dash hyperlink
+ */
+ export const deleteLink = async (linkDoc: Doc, sourceDoc: Doc, destinationDoc: Doc) => {
+ if (Cast(destinationDoc.data, WebField)?.url.href !== StrCast(linkDoc.annotationUri)) return; // check that the destinationDoc is a WebDocument containing the target annotation
+
+ !DocumentManager.Instance.getFirstDocumentView(destinationDoc) && runInAction(() => DocumentLinksButton.invisibleWebDoc = destinationDoc); // see note in makeLink
+
+ var success = false;
+ const onSuccess = action(() => {
+ console.log("Edit success!");
+ success = true;
+ clearTimeout(interval);
+ DocumentLinksButton.invisibleWebDoc = undefined;
+ document.removeEventListener("editSuccess", onSuccess);
+ });
+
+ const annotationId = StrCast(linkDoc.annotationId);
+ const linkUrl = Utils.prepend("/doc/" + sourceDoc[Id]);
+ const interval = setInterval(() => {// keep trying to edit until annotations have loaded and editing is successful
+ !success && document.dispatchEvent(new CustomEvent<{ targetUrl: string, id: string }>("deleteLink", {
+ detail: { targetUrl: linkUrl, id: annotationId },
+ bubbles: true
+ }));
+ }, 300);
+
+ setTimeout(action(() => {
+ if (!success) {
+ clearInterval(interval);
+ DocumentLinksButton.invisibleWebDoc = undefined;
+ }
+ }), 10000); // give up if no success after 10s
+ document.addEventListener("editSuccess", onSuccess);
+ };
+
+ /**
+ * Send message to Hypothes.is client to scroll to an annotation when it loads
+ */
+ export const scrollToAnnotation = (annotationId: string, target: Doc) => {
+ var success = false;
+ const onSuccess = () => {
+ console.log("Scroll success!!");
+ document.removeEventListener('scrollSuccess', onSuccess);
+ clearInterval(interval);
+ success = true;
+ };
+
+ const interval = setInterval(() => { // keep trying to scroll every 250ms until annotations have loaded and scrolling is successful
+ document.dispatchEvent(new CustomEvent('scrollToAnnotation', {
+ detail: annotationId,
+ bubbles: true
+ }));
+ const targetView: Opt<DocumentView> = DocumentManager.Instance.getFirstDocumentView(target);
+ const position = targetView?.props.ScreenToLocalTransform().inverse().transformPoint(0, 0);
+ targetView && position && simulateMouseClick(targetView.ContentDiv!, position[0], position[1], position[0], position[1], false);
+ }, 300);
+
+ document.addEventListener('scrollSuccess', onSuccess); // listen for success message from client
+ setTimeout(() => !success && clearInterval(interval), 10000); // give up if no success after 10s
+ };
+} \ No newline at end of file
diff --git a/src/client/util/SettingsManager.tsx b/src/client/util/SettingsManager.tsx
index 9ffd4ff20..8b58880d4 100644
--- a/src/client/util/SettingsManager.tsx
+++ b/src/client/util/SettingsManager.tsx
@@ -11,7 +11,6 @@ import { CurrentUserUtils } from "./CurrentUserUtils";
import { Utils, addStyleSheet, addStyleSheetRule, removeStyleSheetRule } from "../../Utils";
import { Doc } from "../../fields/Doc";
import GroupManager from "./GroupManager";
-import HypothesisAuthenticationManager from "../apis/HypothesisAuthenticationManager";
import GoogleAuthenticationManager from "../apis/GoogleAuthenticationManager";
import { DocServer } from "../DocServer";
import { BoolCast, StrCast, NumCast } from "../../fields/Types";
@@ -44,7 +43,6 @@ export default class SettingsManager extends React.Component<{}> {
public open = action(() => (this.isOpen = true) && SelectionManager.DeselectAll());
private googleAuthorize = action(() => GoogleAuthenticationManager.Instance.fetchOrGenerateAccessToken(true));
- private hypothesisAuthorize = action(() => HypothesisAuthenticationManager.Instance.fetchAccessToken(true));
private changePassword = async () => {
if (!(this.curr_password && this.new_password && this.new_confirm)) {
runInAction(() => this.passwordResultText = "Error: Hey, we're missing some fields!");
@@ -144,7 +142,6 @@ export default class SettingsManager extends React.Component<{}> {
@computed get accountsContent() {
return <div className="accounts-content">
<button onClick={this.googleAuthorize} value="data">Link to Google</button>
- <button onClick={this.hypothesisAuthorize} value="data">Link to Hypothes.is</button>
<button onClick={GroupManager.Instance?.open}>Manage groups</button>
</div>;
}
diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts
index 3a61e89ce..0ea02e3cb 100644
--- a/src/client/views/GlobalKeyHandler.ts
+++ b/src/client/views/GlobalKeyHandler.ts
@@ -7,7 +7,6 @@ import { List } from "../../fields/List";
import { ScriptField } from "../../fields/ScriptField";
import { Cast, PromiseValue } from "../../fields/Types";
import GoogleAuthenticationManager from "../apis/GoogleAuthenticationManager";
-import HypothesisAuthenticationManager from "../apis/HypothesisAuthenticationManager";
import { DocServer } from "../DocServer";
import { DocumentType } from "../documents/DocumentTypes";
import { DictationManager } from "../util/DictationManager";
@@ -107,7 +106,6 @@ export default class KeyManager {
doDeselect && SelectionManager.DeselectAll();
DictationManager.Controls.stop();
GoogleAuthenticationManager.Instance.cancel();
- HypothesisAuthenticationManager.Instance.cancel();
SharingManager.Instance.close();
GroupManager.Instance.close();
CollectionFreeFormViewChrome.Instance.clearKeep();
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index b6058db7a..f5dccd567 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -14,9 +14,8 @@ import { listSpec } from '../../fields/Schema';
import { ScriptField } from '../../fields/ScriptField';
import { BoolCast, Cast, FieldValue, StrCast } from '../../fields/Types';
import { TraceMobx } from '../../fields/util';
-import { emptyFunction, emptyPath, returnEmptyFilter, returnFalse, returnOne, returnTrue, returnZero, setupMoveUpEvents, Utils } from '../../Utils';
+import { emptyFunction, emptyPath, returnEmptyFilter, returnFalse, returnOne, returnTrue, returnZero, setupMoveUpEvents, Utils, simulateMouseClick } from '../../Utils';
import GoogleAuthenticationManager from '../apis/GoogleAuthenticationManager';
-import HypothesisAuthenticationManager from '../apis/HypothesisAuthenticationManager';
import { DocServer } from '../DocServer';
import { Docs, DocumentOptions } from '../documents/Documents';
import { DocumentType } from '../documents/DocumentTypes';
@@ -59,7 +58,10 @@ import { TaskCompletionBox } from './nodes/TaskCompletedBox';
import { OverlayView } from './OverlayView';
import PDFMenu from './pdf/PDFMenu';
import { PreviewCursor } from './PreviewCursor';
+import { Hypothesis } from '../util/HypothesisUtils';
import { undoBatch } from '../util/UndoManager';
+import { WebBox } from './nodes/WebBox';
+import * as ReactDOM from 'react-dom';
import { SearchBox } from './search/SearchBox';
@observer
@@ -127,6 +129,7 @@ export class MainView extends React.Component {
}
});
});
+ document.addEventListener("linkAnnotationToDash", Hypothesis.linkListener);
}
componentWillUnMount() {
@@ -134,6 +137,7 @@ export class MainView extends React.Component {
window.removeEventListener("pointerdown", this.globalPointerDown);
window.removeEventListener("pointerup", this.globalPointerUp);
window.removeEventListener("paste", KeyManager.Instance.paste as any);
+ document.removeEventListener("linkAnnotationToDash", Hypothesis.linkListener);
}
constructor(props: Readonly<{}>) {
@@ -772,6 +776,37 @@ export class MainView extends React.Component {
</div>;
}
+ @computed get invisibleWebBox() { // see note under the makeLink method in HypothesisUtils.ts
+ return !DocumentLinksButton.invisibleWebDoc ? null :
+ <div style={{ position: 'absolute', left: 50, top: 50, display: 'block', width: '500px', height: '1000px' }} ref={DocumentLinksButton.invisibleWebRef}>
+ <WebBox
+ fieldKey={"data"}
+ ContainingCollectionView={undefined}
+ ContainingCollectionDoc={undefined}
+ Document={DocumentLinksButton.invisibleWebDoc}
+ LibraryPath={emptyPath}
+ dropAction={"move"}
+ isSelected={returnFalse}
+ select={returnFalse}
+ rootSelected={returnFalse}
+ renderDepth={0}
+ addDocTab={returnFalse}
+ pinToPres={returnFalse}
+ ScreenToLocalTransform={Transform.Identity}
+ bringToFront={returnFalse}
+ active={returnFalse}
+ whenActiveChanged={returnFalse}
+ focus={returnFalse}
+ PanelWidth={() => 500}
+ PanelHeight={() => 800}
+ NativeHeight={() => 500}
+ NativeWidth={() => 800}
+ ContentScaling={returnOne}
+ docFilters={returnEmptyFilter}
+ />
+ </div>;
+ }
+
render() {
return (<div className={"mainView-container" + (this.darkScheme ? "-dark" : "")} ref={this._mainViewRef}>
@@ -781,7 +816,6 @@ export class MainView extends React.Component {
<SettingsManager />
<GroupManager />
<GoogleAuthenticationManager />
- <HypothesisAuthenticationManager />
<DocumentDecorations />
{this.search}
<CollectionMenu />
@@ -806,8 +840,61 @@ export class MainView extends React.Component {
<OverlayView />
<TimelineMenu />
{this.snapLines}
+ <div ref={this.makeWebRef} style={{ position: 'absolute', left: -1000, top: -1000, display: 'block', width: '200px', height: '800px' }} />
</div >);
}
+
+ makeWebRef = (ele: HTMLDivElement) => {
+ reaction(() => DocumentLinksButton.invisibleWebDoc,
+ invisibleDoc => {
+ ReactDOM.unmountComponentAtNode(ele);
+ invisibleDoc && ReactDOM.render(<span title="Drag as document" className="invisible-webbox" >
+ <div style={{ position: 'absolute', left: -1000, top: -1000, display: 'block', width: '200px', height: '800px' }} ref={DocumentLinksButton.invisibleWebRef}>
+ <WebBox
+ fieldKey={"data"}
+ ContainingCollectionView={undefined}
+ ContainingCollectionDoc={undefined}
+ Document={invisibleDoc}
+ LibraryPath={emptyPath}
+ dropAction={"move"}
+ isSelected={returnFalse}
+ select={returnFalse}
+ rootSelected={returnFalse}
+ renderDepth={0}
+ addDocTab={returnFalse}
+ pinToPres={returnFalse}
+ ScreenToLocalTransform={Transform.Identity}
+ bringToFront={returnFalse}
+ active={returnFalse}
+ whenActiveChanged={returnFalse}
+ focus={returnFalse}
+ PanelWidth={() => 500}
+ PanelHeight={() => 800}
+ NativeHeight={() => 500}
+ NativeWidth={() => 800}
+ ContentScaling={returnOne}
+ docFilters={returnEmptyFilter}
+ />
+ </div>;
+ </span>, ele);
+
+ var success = false;
+ const onSuccess = () => {
+ success = true;
+ clearTimeout(interval);
+ document.removeEventListener("editSuccess", onSuccess);
+ };
+
+ // For some reason, Hypothes.is annotations don't load until a click is registered on the page,
+ // so we keep simulating clicks until annotations have loaded and editing is successful
+ const interval = setInterval(() => {
+ !success && simulateMouseClick(ele, 50, 50, 50, 50);
+ }, 500);
+
+ setTimeout(() => !success && clearInterval(interval), 10000); // give up if no success after 10s
+ document.addEventListener("editSuccess", onSuccess);
+ });
+ }
}
Scripting.addGlobal(function freezeSidebar() { MainView.expandFlyout(); });
Scripting.addGlobal(function toggleComicMode() { Doc.UserDoc().fontFamily = "Comic Sans MS"; Doc.UserDoc().renderStyle = Doc.UserDoc().renderStyle === "comic" ? undefined : "comic"; });
diff --git a/src/client/views/collections/CollectionLinearView.tsx b/src/client/views/collections/CollectionLinearView.tsx
index a9e812ad3..3cf46dbed 100644
--- a/src/client/views/collections/CollectionLinearView.tsx
+++ b/src/client/views/collections/CollectionLinearView.tsx
@@ -176,7 +176,7 @@ export class CollectionLinearView extends CollectionSubView(LinearDocument) {
}}
onPointerDown={e => e.stopPropagation()} >
<span className="bottomPopup-text" >
- Creating link from: {DocumentLinksButton.StartLink.props.Document.title}
+ Creating link from: {DocumentLinksButton.AnnotationId ? "Annotation in " : " "} {StrCast(DocumentLinksButton.StartLink.title).length < 51 ? DocumentLinksButton.StartLink.title : StrCast(DocumentLinksButton.StartLink.title).slice(0, 50) + '...'}
</span>
<Tooltip title={<><div className="dash-tooltip">{LinkDescriptionPopup.showDescriptions ? "Turn off description pop-up" :
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index 0e40cd21c..72aece284 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -402,14 +402,28 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?:
// }
}
if (uriList) {
- this.addDocument(Docs.Create.WebDocument(uriList, {
- ...options,
- title: uriList,
- _width: 400,
- _height: 315,
- _nativeWidth: 850,
- _nativeHeight: 962
- }));
+ const existingWebDoc = await Hypothesis.findWebDoc(uriList);
+ if (existingWebDoc) {
+ const alias = Doc.MakeAlias(existingWebDoc);
+ alias.x = options.x;
+ alias.y = options.y;
+ alias._nativeWidth = 850;
+ alias._nativeHeight = 962;
+ alias._width = 400;
+ this.addDocument(alias);
+ } else {
+ const newDoc = Docs.Create.WebDocument(uriList, {
+ ...options,
+ title: uriList.split("#annotations:")[0],
+ _width: 400,
+ _height: 315,
+ _nativeWidth: 850,
+ _nativeHeight: 962,
+ UseCors: true
+ });
+ newDoc.data = new WebField(uriList.split("#annotations:")[0]); // clean hypothes.is URLs that reference a specific annotation (eg. https://en.wikipedia.org/wiki/Cartoon#annotations:t7qAeNbCEeqfG5972KR2Ig)
+ this.addDocument(newDoc);
+ }
return;
}
@@ -492,3 +506,5 @@ import { CollectionView, CollectionViewType } from "./CollectionView";
import { SelectionManager } from "../../util/SelectionManager";
import { OverlayView } from "../OverlayView";
import { setTimeout } from "timers";
+import { Hypothesis } from "../../util/HypothesisUtils";
+
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index 7ca06f0f9..1a708d67d 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -74,7 +74,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
if (e.key === "?") {
ContextMenu.Instance.setDefaultItem("?", (str: string) => {
const textDoc = Docs.Create.WebDocument(`https://bing.com/search?q=${str}`, {
- _width: 200, x, y, _nativeHeight: 962, _nativeWidth: 800, isAnnotating: false,
+ _width: 200, x, y, _nativeHeight: 962, _nativeWidth: 850, isAnnotating: false,
title: "bing", UseCors: true
});
this.props.addDocTab(textDoc, "onRight");
diff --git a/src/client/views/linking/LinkEditor.tsx b/src/client/views/linking/LinkEditor.tsx
index 75fc8bf85..5832a2181 100644
--- a/src/client/views/linking/LinkEditor.tsx
+++ b/src/client/views/linking/LinkEditor.tsx
@@ -388,6 +388,12 @@ export class LinkEditor extends React.Component<LinkEditorProps> {
onPointerDown={() => this.changeFollowBehavior("inTab")}>
Always open in new tab
</div>
+ {this.props.linkDoc.linksToAnnotation ?
+ <div className="linkEditor-followingDropdown-option"
+ onPointerDown={() => this.changeFollowBehavior("openExternal")}>
+ Always open in external page
+ </div>
+ : null}
</div>
</div>
</div>;
diff --git a/src/client/views/linking/LinkMenuItem.tsx b/src/client/views/linking/LinkMenuItem.tsx
index f4aed94e7..b95fccf2a 100644
--- a/src/client/views/linking/LinkMenuItem.tsx
+++ b/src/client/views/linking/LinkMenuItem.tsx
@@ -1,9 +1,9 @@
import { library } from '@fortawesome/fontawesome-svg-core';
import { faArrowRight, faChevronDown, faChevronUp, faEdit, faEye, faTimes, faPencilAlt, faEyeSlash } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon, FontAwesomeIconProps } from '@fortawesome/react-fontawesome';
-import { action, observable } from 'mobx';
+import { action, observable, runInAction } from 'mobx';
import { observer } from "mobx-react";
-import { Doc, DocListCast } from '../../../fields/Doc';
+import { Doc, DocListCast, Opt } from '../../../fields/Doc';
import { Cast, StrCast } from '../../../fields/Types';
import { DragManager } from '../../util/DragManager';
import { LinkManager } from '../../util/LinkManager';
@@ -11,13 +11,16 @@ import { ContextMenu } from '../ContextMenu';
import './LinkMenuItem.scss';
import React = require("react");
import { DocumentManager } from '../../util/DocumentManager';
-import { setupMoveUpEvents, emptyFunction } from '../../../Utils';
+import { setupMoveUpEvents, emptyFunction, Utils, simulateMouseClick } from '../../../Utils';
import { DocumentView } from '../nodes/DocumentView';
import { DocumentLinksButton } from '../nodes/DocumentLinksButton';
import { LinkDocPreview } from '../nodes/LinkDocPreview';
+import { Hypothesis } from '../../util/HypothesisUtils';
+import { Id } from '../../../fields/FieldSymbols';
import { Tooltip } from '@material-ui/core';
import { DocumentType } from '../../documents/DocumentTypes';
import { undoBatch } from '../../util/UndoManager';
+import { WebField } from '../../../fields/URLField';
library.add(faEye, faEdit, faTimes, faArrowRight, faChevronDown, faChevronUp, faPencilAlt, faEyeSlash);
@@ -148,23 +151,35 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> {
}
@action.bound
- async followDefault() {
+ followDefault() {
DocumentLinksButton.EditLink = undefined;
LinkDocPreview.LinkInfo = undefined;
+ const linkDoc = this.props.linkDoc;
- if (this.props.linkDoc.followLinkLocation && this.props.linkDoc.followLinkLocation !== "Default") {
- this.props.addDocTab(this.props.destinationDoc, StrCast(this.props.linkDoc.followLinkLocation));
+ if (linkDoc.followLinkLocation === "openExternal" && this.props.destinationDoc.type === DocumentType.WEB) {
+ window.open(`${StrCast(linkDoc.annotationUri)}#annotations:${StrCast(linkDoc.annotationId)}`, '_blank');
+ return;
+ }
+
+ if (linkDoc.followLinkLocation && linkDoc.followLinkLocation !== "Default") {
+ this.props.addDocTab(this.props.destinationDoc, StrCast(linkDoc.followLinkLocation));
} else {
DocumentManager.Instance.FollowLink(this.props.linkDoc, this.props.sourceDoc, doc => this.props.addDocTab(doc, "onRight"), false);
}
+
+ linkDoc.linksToAnnotation && Hypothesis.scrollToAnnotation(StrCast(this.props.linkDoc.annotationId), this.props.destinationDoc);
}
@undoBatch
@action
deleteLink = (): void => {
+ this.props.linkDoc.linksToAnnotation && Hypothesis.deleteLink(this.props.linkDoc, this.props.sourceDoc, this.props.destinationDoc);
LinkManager.Instance.deleteLink(this.props.linkDoc);
- LinkDocPreview.LinkInfo = undefined;
- DocumentLinksButton.EditLink = undefined;
+
+ runInAction(() => {
+ LinkDocPreview.LinkInfo = undefined;
+ DocumentLinksButton.EditLink = undefined;
+ });
}
@undoBatch
@@ -233,7 +248,7 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> {
<FontAwesomeIcon className="destination-icon" icon={destinationIcon} size="sm" /></div>
<p className="linkMenu-destination-title"
onPointerDown={this.followDefault}>
- {title}
+ {this.props.linkDoc.linksToAnnotation && Cast(this.props.destinationDoc.data, WebField)?.url.href === this.props.linkDoc.annotationUri ? "Annotation in" : ""} {title}
</p>
</div>
{this.props.linkDoc.description !== "" ? <p className="linkMenu-description">
diff --git a/src/client/views/nodes/AudioBox.scss b/src/client/views/nodes/AudioBox.scss
index 306062ced..0d787d9af 100644
--- a/src/client/views/nodes/AudioBox.scss
+++ b/src/client/views/nodes/AudioBox.scss
@@ -150,6 +150,21 @@
z-index: 1000;
overflow: hidden;
+ .audiobox-container {
+ position: absolute;
+ width: 10px;
+ top: 2.5%;
+ height: 0px;
+ background: lightblue;
+ border-radius: 5px;
+ // box-shadow: black 2px 2px 1px;
+ opacity: 0.3;
+ z-index: 500;
+ border-style: solid;
+ border-color: darkblue;
+ border-width: 1px;
+ }
+
.audiobox-current {
width: 1px;
height: 100%;
@@ -164,7 +179,16 @@
height: 100%;
overflow: hidden;
z-index: -1000;
- bottom: -30%;
+ bottom: 0;
+ pointer-events: none;
+ div {
+ height: 100% !important;
+ width: 100% !important;
+ }
+ canvas {
+ height: 100% !important;
+ width: 100% !important;
+ }
}
.audiobox-linker,
diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx
index eba1046b2..bc89cb6f9 100644
--- a/src/client/views/nodes/AudioBox.tsx
+++ b/src/client/views/nodes/AudioBox.tsx
@@ -7,7 +7,7 @@ import { AudioField, nullAudio } from "../../../fields/URLField";
import { ViewBoxAnnotatableComponent } from "../DocComponent";
import { makeInterface, createSchema } from "../../../fields/Schema";
import { documentSchema } from "../../../fields/documentSchemas";
-import { Utils, returnTrue, emptyFunction, returnOne, returnTransparent, returnFalse, returnZero, formatTime } from "../../../Utils";
+import { Utils, returnTrue, emptyFunction, returnOne, returnTransparent, returnFalse, returnZero, formatTime, setupMoveUpEvents } from "../../../Utils";
import { runInAction, observable, reaction, IReactionDisposer, computed, action, trace, toJS } from "mobx";
import { DateField } from "../../../fields/DateField";
import { SelectionManager } from "../../util/SelectionManager";
@@ -31,9 +31,7 @@ declare class MediaRecorder {
// whatever MediaRecorder has
constructor(e: any);
}
-export const audioSchema = createSchema({
- playOnSelect: "boolean"
-});
+export const audioSchema = createSchema({ playOnSelect: "boolean" });
type AudioDocument = makeInterface<[typeof documentSchema, typeof audioSchema]>;
const AudioDocument = makeInterface(documentSchema, audioSchema);
@@ -60,26 +58,28 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD
_start: number = 0;
_hold: boolean = false;
_left: boolean = false;
- _markers: Array<any> = [];
_first: boolean = false;
_dragging = false;
_count: Array<any> = [];
+ _audioRef = React.createRef<HTMLDivElement>();
_timeline: Opt<HTMLDivElement>;
_duration = 0;
-
- private _isPointerDown = false;
+ _markerStart: number = 0;
private _currMarker: any;
+ @observable _visible: boolean = false;
+ @observable _markerEnd: number = 0;
@observable _position: number = 0;
@observable _buckets: Array<number> = new Array<number>();
- @observable private _height: number = NumCast(this.layoutDoc._height);
+ @observable _waveHeight: Opt<number> = this.layoutDoc._height;
@observable private _paused: boolean = false;
@observable private static _scrubTime = 0;
@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 SetScrubTime = action((timeInMillisFrom1970: number) => { AudioBox._scrubTime = 0; AudioBox._scrubTime = timeInMillisFrom1970; });
@computed get recordingStart() { return Cast(this.dataDoc[this.props.fieldKey + "-recordingStart"], DateField)?.date.getTime(); }
+ @computed get audioDuration() { return NumCast(this.dataDoc.duration); }
async slideTemplate() { return (await Cast((await Cast(Doc.UserDoc().slidesBtn, Doc) as Doc).dragFactory, Doc) as Doc); }
constructor(props: Readonly<FieldViewProps>) {
@@ -135,21 +135,11 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD
la2 = l.anchor1 as Doc;
linkTime = NumCast(l.anchor1_timecode);
}
- if (la2.audioStart) {
- linkTime = NumCast(la2.audioStart);
- }
-
- if (la1.audioStart) {
- linkTime = NumCast(la1.audioStart);
- }
-
- if (la1.audioEnd) {
- endTime = NumCast(la1.audioEnd);
- }
+ if (la2.audioStart) linkTime = NumCast(la2.audioStart);
+ if (la1.audioStart) linkTime = NumCast(la1.audioStart);
- if (la2.audioEnd) {
- endTime = NumCast(la2.audioEnd);
- }
+ if (la1.audioEnd) endTime = NumCast(la1.audioEnd);
+ if (la2.audioEnd) endTime = NumCast(la2.audioEnd);
if (linkTime) {
link = true;
@@ -201,7 +191,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD
// play back the audio from time
@action
- playFrom = (seekTimeInSeconds: number, endTime: number = this.dataDoc.duration) => {
+ playFrom = (seekTimeInSeconds: number, endTime: number = this.audioDuration) => {
let play;
clearTimeout(play);
this._duration = endTime - seekTimeInSeconds;
@@ -216,7 +206,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD
this._ele.currentTime = seekTimeInSeconds;
this._ele.play();
runInAction(() => this.audioState = "playing");
- if (endTime !== this.dataDoc.duration) {
+ if (endTime !== this.audioDuration) {
play = setTimeout(() => this.pause(), (this._duration) * 1000); // use setTimeout to play a specific duration
}
} else {
@@ -228,11 +218,10 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD
// update the recording time
updateRecordTime = () => {
if (this.audioState === "recording") {
+ setTimeout(this.updateRecordTime, 30);
if (this._paused) {
- setTimeout(this.updateRecordTime, 30);
this._pausedTime += (new Date().getTime() - this._recordStart) / 1000;
} else {
- setTimeout(this.updateRecordTime, 30);
this.layoutDoc.currentTimecode = (new Date().getTime() - this._recordStart - this.pauseTime) / 1000;
}
}
@@ -351,115 +340,96 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD
// return the total time paused to update the correct time
@computed get pauseTime() {
- return (this._pauseEnd - this._pauseStart);
+ return this._pauseEnd - this._pauseStart;
}
- // creates a new label
+ // starting the drag event for marker resizing
@action
- newMarker(marker: Doc) {
- marker.data = "";
- if (this.dataDoc[this.annotationKey]) {
- this.dataDoc[this.annotationKey].push(marker);
- } else {
- this.dataDoc[this.annotationKey] = new List<Doc>([marker]);
- }
+ onPointerDownTimeline = (e: React.PointerEvent): void => {
+ const rect = (e.target as any).getBoundingClientRect();
+ const toTimeline = (screen_delta: number) => screen_delta / rect.width * this.audioDuration;
+ this._markerStart = this._markerEnd = toTimeline(e.clientX - rect.x);
+ setupMoveUpEvents(this, e, action((e: PointerEvent) => {
+ this._visible = true;
+ this._markerEnd = toTimeline(e.clientX - rect.x);
+ if (this._markerEnd < this._markerStart) {
+ const tmp = this._markerStart;
+ this._markerStart = this._markerEnd;
+ this._markerEnd = tmp;
+ }
+ return false;
+ }),
+ action((e: PointerEvent, movement: number[]) => {
+ if (Math.abs(movement[0]) > 15) {
+ this.createNewMarker(this._markerStart, toTimeline(e.clientX - rect.x));
+ }
+ this._visible = false;
+ }),
+ emptyFunction);
}
-
- // the starting time of the marker
- start(startingPoint: number) {
- this._hold = true;
- this._start = startingPoint;
+ // returns the selection container
+ @computed get container() {
+ return <div className="audiobox-container" style={{
+ left: `${NumCast(this._markerStart) / this.audioDuration * 100}%`,
+ width: `${Math.abs(this._markerStart - this._markerEnd) / this.audioDuration * 100}%`, height: "100%", top: "0%"
+ }}></div>;
}
// creates a new marker
@action
- end(marker: number) {
- this._hold = false;
- const newMarker = Docs.Create.LabelDocument({ title: ComputedField.MakeFunction(`formatToTime(self.audioStart) + "-" + formatToTime(self.audioEnd)`) as any, isLabel: false, useLinkSmallAnchor: true, hideLinkButton: true, audioStart: this._start, audioEnd: marker, _showSidebar: false, _autoHeight: true, annotationOn: this.props.Document });
- newMarker.data = "";
+ createNewMarker(audioStart: number, audioEnd: number) {
+ const newMarker = Docs.Create.LabelDocument({
+ title: ComputedField.MakeFunction(`formatToTime(self.audioStart) + "-" + formatToTime(self.audioEnd)`) as any, isLabel: false,
+ useLinkSmallAnchor: true, hideLinkButton: true, audioStart, audioEnd, _showSidebar: false,
+ _autoHeight: true, annotationOn: this.props.Document
+ });
+ this.addMark(newMarker);
+ }
+
+ // adds an annotation marker or label
+ @action
+ addMark(marker: Doc) {
+ marker.data = "";
if (this.dataDoc[this.annotationKey]) {
- this.dataDoc[this.annotationKey].push(newMarker);
+ this.dataDoc[this.annotationKey].push(marker);
} else {
- this.dataDoc[this.annotationKey] = new List<Doc>([newMarker]);
+ this.dataDoc[this.annotationKey] = new List<Doc>([marker]);
}
-
- this._start = 0;
}
// starting the drag event for marker resizing
onPointerDown = (e: React.PointerEvent, m: any, left: boolean): void => {
- e.stopPropagation();
- e.preventDefault();
- this._isPointerDown = true;
this._currMarker = m;
- this._timeline?.setPointerCapture(e.pointerId);
this._left = left;
-
- document.removeEventListener("pointermove", this.onPointerMove);
- document.addEventListener("pointermove", this.onPointerMove);
- document.removeEventListener("pointerup", this.onPointerUp);
- document.addEventListener("pointerup", this.onPointerUp);
- }
-
- // ending the drag event for marker resizing
- @action
- onPointerUp = (e: PointerEvent): void => {
- e.stopPropagation();
- e.preventDefault();
- this._isPointerDown = false;
- this._dragging = false;
-
const rect = (e.target as any).getBoundingClientRect();
- this._ele!.currentTime = this.layoutDoc.currentTimecode = (e.clientX - rect.x) / rect.width * NumCast(this.dataDoc.duration);
-
- this._timeline?.releasePointerCapture(e.pointerId);
-
- document.removeEventListener("pointermove", this.onPointerMove);
- document.removeEventListener("pointerup", this.onPointerUp);
- }
-
- // resizes the marker while dragging
- onPointerMove = async (e: PointerEvent) => {
- e.stopPropagation();
- e.preventDefault();
-
- if (!this._isPointerDown) {
- return;
- }
-
- const rect = await (e.target as any).getBoundingClientRect();
-
- const newTime = (e.clientX - rect.x) / rect.width * NumCast(this.dataDoc.duration);
-
- this.changeMarker(this._currMarker, newTime);
+ const toTimeline = (screen_delta: number) => screen_delta / rect.width * this.audioDuration;
+ setupMoveUpEvents(this, e, () => {
+ this.changeMarker(this._currMarker, toTimeline(e.clientX - rect.x));
+ return false;
+ },
+ () => this._ele!.currentTime = this.layoutDoc.currentTimecode = toTimeline(e.clientX - rect.x),
+ emptyFunction);
}
// updates the marker with the new time
@action
changeMarker = (m: any, time: any) => {
- DocListCast(this.dataDoc[this.annotationKey]).forEach((marker: Doc) => {
- if (this.isSame(marker, m)) {
- this._left ? marker.audioStart = time : marker.audioEnd = time;
- }
- });
+ DocListCast(this.dataDoc[this.annotationKey]).filter(marker => this.isSame(marker, m)).forEach(marker =>
+ this._left ? marker.audioStart = time : marker.audioEnd = time);
}
// checks if the two markers are the same with start and end time
isSame = (m1: any, m2: any) => {
- if (m1.audioStart === m2.audioStart && m1.audioEnd === m2.audioEnd) {
- return true;
- }
- return false;
+ return m1.audioStart === m2.audioStart && m1.audioEnd === m2.audioEnd;
}
// instantiates a new array of size 500 for marker layout
markers = () => {
- const increment = NumCast(this.layoutDoc.duration) / 500;
+ const increment = this.audioDuration / 500;
this._count = [];
for (let i = 0; i < 500; i++) {
this._count.push([increment * i, 0]);
}
-
}
// makes sure no markers overlaps each other by setting the correct position and width
@@ -484,7 +454,6 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD
if (this._count[i][0] >= m.audioStart && this._count[i][0] <= m.audioEnd) {
this._count[i][1] = max;
}
-
}
if (this.dataDoc.markerAmount < max) {
@@ -497,11 +466,11 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD
@computed get waveform() {
return <Waveform
color={"darkblue"}
- height={this._height}
+ height={this._waveHeight}
barWidth={0.1}
- // pos={this.layoutDoc.currentTimecode}
- pos={this.dataDoc.duration}
- duration={this.dataDoc.duration}
+ // pos={this.layoutDoc.currentTimecode} need to correctly resize parent to make this work (not very necessary for function)
+ pos={this.audioDuration}
+ duration={this.audioDuration}
peaks={this._buckets.length === 100 ? this._buckets : undefined}
progressColor={"blue"} />;
}
@@ -542,74 +511,31 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD
return this.buckets();
}
- // for updating the width and height of the waveform with timeline ref
- timelineRef = (timeline: HTMLDivElement) => {
- const observer = new _global.ResizeObserver(action((entries: any) => {
- for (const entry of entries) {
- this.update(entry.contentRect.width, entry.contentRect.height);
- this._position = entry.contentRect.width;
- }
- }));
- timeline && observer.observe(timeline);
-
- this._timeline = timeline;
- }
-
- // update the width and height of the audio waveform
- @action
- update = (width: number, height: number) => {
- if (height) {
- this._height = 0.8 * NumCast(this.layoutDoc._height);
- const canvas2 = document.getElementsByTagName("canvas")[0];
- if (canvas2) {
- const oldWidth = canvas2.width;
- const oldHeight = canvas2.height;
- canvas2.style.height = `${this._height}`;
- canvas2.style.width = `${width}`;
-
- const ratio1 = oldWidth / window.innerWidth;
- const ratio2 = oldHeight / window.innerHeight;
- const context = canvas2.getContext('2d');
- if (context) {
- context.scale(ratio1, ratio2);
- }
- }
-
- const canvas1 = document.getElementsByTagName("canvas")[1];
- if (canvas1) {
- const oldWidth = canvas1.width;
- const oldHeight = canvas1.height;
- canvas1.style.height = `${this._height}`;
- canvas1.style.width = `${width}`;
-
- const ratio1 = oldWidth / window.innerWidth;
- const ratio2 = oldHeight / window.innerHeight;
- const context = canvas1.getContext('2d');
- if (context) {
- context.scale(ratio1, ratio2);
- }
-
- const parent = canvas1.parentElement;
- if (parent) {
- parent.style.width = `${width}`;
- parent.style.height = `${this._height}`;
- }
- }
- }
- }
-
rangeScript = () => AudioBox.RangeScript;
-
labelScript = () => AudioBox.LabelScript;
- // for indicating the first marker that is rendered
- reset = () => this._first = true;
-
render() {
const interactive = this.active() ? "-interactive" : "";
- this.reset();
+ this._first = true; // for indicating the first marker that is rendered
this.path && this._buckets.length !== 100 ? this.peaks : null; // render waveform if audio is done recording
- return <div className={`audiobox-container`} onContextMenu={this.specificContextMenu} onClick={!this.path ? this.recordClick : undefined}>
+ const markerDoc = (mark: Doc, script: undefined | (() => ScriptField)) => {
+ return <DocumentView {...this.props}
+ Document={mark}
+ pointerEvents={true}
+ NativeHeight={returnZero}
+ NativeWidth={returnZero}
+ rootSelected={returnFalse}
+ LayoutTemplate={undefined}
+ ContainingCollectionDoc={this.props.Document}
+ removeDocument={this.removeDocument}
+ parentActive={returnTrue}
+ onClick={this.layoutDoc.playOnClick ? script : undefined}
+ ignoreAutoHeight={false}
+ bringToFront={emptyFunction}
+ scriptContext={this} />;
+ };
+ return <div className={`audiobox-container`} onContextMenu={this.specificContextMenu} onClick={!this.path ? this.recordClick : undefined}
+ style={{ pointerEvents: !interactive ? "none" : undefined }}>
{!this.path ?
<div className="audiobox-buttons">
<div className="audiobox-dictation" onClick={this.onFile}>
@@ -634,85 +560,51 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD
<div className="audiobox-dictation"></div>
<div className="audiobox-player" >
<div className="audiobox-playhead" title={this.audioState === "paused" ? "play" : "pause"} onClick={this.onPlay}> <FontAwesomeIcon style={{ width: "100%", position: "absolute", left: "0px", top: "5px", borderWidth: "thin", borderColor: "white" }} icon={this.audioState === "paused" ? "play" : "pause"} size={"1x"} /></div>
- <div className="audiobox-timeline" ref={this.timelineRef} onClick={e => { e.stopPropagation(); e.preventDefault(); }}
+ <div className="audiobox-timeline" onClick={e => { e.stopPropagation(); e.preventDefault(); }}
onPointerDown={e => {
e.stopPropagation();
e.preventDefault();
if (e.button === 0 && !e.ctrlKey) {
const rect = (e.target as any).getBoundingClientRect();
- if (e.target as HTMLElement !== document.getElementById("current")) {
+ if (e.target !== this._audioRef.current) {
const wasPaused = this.audioState === "paused";
- this._ele!.currentTime = this.layoutDoc.currentTimecode = (e.clientX - rect.x) / rect.width * NumCast(this.dataDoc.duration);
+ this._ele!.currentTime = this.layoutDoc.currentTimecode = (e.clientX - rect.x) / rect.width * this.audioDuration;
wasPaused && this.pause();
}
- }
- if (e.button === 0 && e.altKey) {
- this.newMarker(Docs.Create.LabelDocument({ title: ComputedField.MakeFunction(`formatToTime(self.audioStart)`) as any, useLinkSmallAnchor: true, hideLinkButton: true, isLabel: true, audioStart: this._ele!.currentTime, _showSidebar: false, _autoHeight: true, annotationOn: this.props.Document }));
- }
+ this.onPointerDownTimeline(e);
+ }
if (e.button === 0 && e.shiftKey) {
- const rect = (e.target as any).getBoundingClientRect();
- this._ele!.currentTime = this.layoutDoc.currentTimecode = (e.clientX - rect.x) / rect.width * NumCast(this.dataDoc.duration);
- this._hold ? this.end(this._ele!.currentTime) : this.start(this._ele!.currentTime);
+ this.addMark(Docs.Create.LabelDocument({ title: ComputedField.MakeFunction(`formatToTime(self.audioStart)`) as any, useLinkSmallAnchor: true, hideLinkButton: true, isLabel: true, audioStart: this._ele!.currentTime, _showSidebar: false, _autoHeight: true, annotationOn: this.props.Document }));
}
}}>
- <div className="waveform" id="waveform" style={{ height: `${100}%`, width: "100%", bottom: "0px" }}>
+ <div className="waveform">
{this.waveform}
</div>
- {DocListCast(this.dataDoc[this.annotationKey]).map((m, i) => {
- let rect;
+ {DocListCast(this.dataDoc[this.annotationKey]).map((m, i) =>
(!m.isLabel) ?
(this.layoutDoc.hideMarkers) ? (null) :
- rect =
- <div key={i} id={"audiobox-marker-container1"} className={this.props.PanelHeight() < 32 ? "audiobox-marker-minicontainer" : "audiobox-marker-container1"}
+ <div className={`audiobox-marker-${this.props.PanelHeight() < 32 ? "mini" : ""}container1`} key={i}
title={`${formatTime(Math.round(NumCast(m.audioStart)))}` + " - " + `${formatTime(Math.round(NumCast(m.audioEnd)))}`}
style={{
- left: `${NumCast(m.audioStart) / NumCast(this.dataDoc.duration, 1) * 100}%`,
- width: `${(NumCast(m.audioEnd) - NumCast(m.audioStart)) / NumCast(this.dataDoc.duration, 1) * 100}%`, height: `${1 / (this.dataDoc.markerAmount + 1) * 100}%`,
- top: `${this.isOverlap(m) * 1 / (this.dataDoc.markerAmount + 1) * 100}%`
+ left: `${NumCast(m.audioStart) / this.audioDuration * 100}%`,
+ top: `${this.isOverlap(m) * 1 / (this.dataDoc.markerAmount + 1) * 100}%`,
+ width: `${(NumCast(m.audioEnd) - NumCast(m.audioStart)) / this.audioDuration * 100}%`, height: `${1 / (this.dataDoc.markerAmount + 1) * 100}%`
}}
onClick={e => { this.playFrom(NumCast(m.audioStart), NumCast(m.audioEnd)); e.stopPropagation(); }} >
<div className="left-resizer" onPointerDown={e => this.onPointerDown(e, m, true)}></div>
- <DocumentView {...this.props}
- Document={m}
- pointerEvents={true}
- NativeHeight={returnZero}
- NativeWidth={returnZero}
- rootSelected={returnFalse}
- LayoutTemplate={undefined}
- ContainingCollectionDoc={this.props.Document}
- removeDocument={this.removeDocument}
- parentActive={returnTrue}
- onClick={this.layoutDoc.playOnClick ? this.rangeScript : undefined}
- ignoreAutoHeight={false}
- bringToFront={emptyFunction}
- scriptContext={this} />
+ {markerDoc(m, this.rangeScript)}
<div className="resizer" onPointerDown={e => this.onPointerDown(e, m, false)}></div>
</div>
:
(this.layoutDoc.hideLabels) ? (null) :
- rect =
- <div className={this.props.PanelHeight() < 32 ? "audiobox-marker-minicontainer" : "audiobox-marker-container"} key={i} style={{ left: `${NumCast(m.audioStart) / NumCast(this.dataDoc.duration, 1) * 100}%` }}>
- <DocumentView {...this.props}
- Document={m}
- pointerEvents={true}
- NativeHeight={returnZero}
- NativeWidth={returnZero}
- rootSelected={returnFalse}
- LayoutTemplate={undefined}
- ContainingCollectionDoc={this.props.Document}
- removeDocument={this.removeDocument}
- parentActive={returnTrue}
- onClick={this.layoutDoc.playOnClick ? this.labelScript : undefined}
- ignoreAutoHeight={false}
- bringToFront={emptyFunction}
- scriptContext={this} />
- </div>;
- return rect;
- })}
+ <div className={`audiobox-marker-${this.props.PanelHeight() < 32 ? "mini" : ""}container`} key={i}
+ style={{ left: `${NumCast(m.audioStart) / this.audioDuration * 100}%` }}>
+ {markerDoc(m, this.labelScript)}
+ </div>
+ )}
{DocListCast(this.dataDoc.links).map((l, i) => {
-
let la1 = l.anchor1 as Doc;
let la2 = l.anchor2 as Doc;
let linkTime = NumCast(l.anchor2_timecode);
@@ -727,7 +619,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD
}
return !linkTime ? (null) :
- <div className={this.props.PanelHeight() < 32 ? "audiobox-marker-minicontainer" : "audiobox-marker-container"} key={l[Id]} style={{ left: `${linkTime / NumCast(this.dataDoc.duration, 1) * 100}%` }} onClick={e => e.stopPropagation()}>
+ <div className={`audiobox-marker-${this.props.PanelHeight() < 32 ? "mini" : ""}container`} key={l[Id]} style={{ left: `${linkTime / this.audioDuration * 100}%` }} onClick={e => e.stopPropagation()}>
<DocumentView {...this.props}
Document={l}
NativeHeight={returnZero}
@@ -744,17 +636,19 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD
LayoutTemplateString={LinkAnchorBox.LayoutString(`anchor${Doc.LinkEndpoint(l, la2)}`)}
/>
<div key={i} className={`audiobox-marker`} onPointerEnter={() => Doc.linkFollowHighlight(la1)}
- onPointerDown={e => { if (e.button === 0 && !e.ctrlKey) { const wasPaused = this.audioState === "paused"; this.playFrom(linkTime); e.stopPropagation(); e.preventDefault(); } }} />
+ onPointerDown={e => { if (e.button === 0 && !e.ctrlKey) { this.playFrom(linkTime); e.stopPropagation(); e.preventDefault(); } }} />
</div>;
})}
- <div className="audiobox-current" id="current" onClick={e => { e.stopPropagation(); e.preventDefault(); }} style={{ left: `${NumCast(this.layoutDoc.currentTimecode) / NumCast(this.dataDoc.duration, 1) * 100}%`, pointerEvents: "none" }} />
+ {this._visible ? this.container : null}
+
+ <div className="audiobox-current" ref={this._audioRef} onClick={e => { e.stopPropagation(); e.preventDefault(); }} style={{ left: `${NumCast(this.layoutDoc.currentTimecode) / this.audioDuration * 100}%`, pointerEvents: "none" }} />
{this.audio}
</div>
<div className="current-time">
{formatTime(Math.round(NumCast(this.layoutDoc.currentTimecode)))}
</div>
<div className="total-time">
- {formatTime(Math.round(NumCast(this.dataDoc.duration)))}
+ {formatTime(Math.round(this.audioDuration))}
</div>
</div>
</div>
diff --git a/src/client/views/nodes/DocumentLinksButton.tsx b/src/client/views/nodes/DocumentLinksButton.tsx
index c2f27c85a..31dd33fc1 100644
--- a/src/client/views/nodes/DocumentLinksButton.tsx
+++ b/src/client/views/nodes/DocumentLinksButton.tsx
@@ -2,18 +2,23 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Tooltip } from "@material-ui/core";
import { action, computed, observable, runInAction } from "mobx";
import { observer } from "mobx-react";
-import { Doc } from "../../../fields/Doc";
+import { Doc, DocListCast, Opt } from "../../../fields/Doc";
+import { DocumentType } from "../../documents/DocumentTypes";
+import { emptyFunction, setupMoveUpEvents, returnFalse, Utils, emptyPath } from "../../../Utils";
import { TraceMobx } from "../../../fields/util";
-import { emptyFunction, returnFalse, setupMoveUpEvents, emptyPath } from "../../../Utils";
-import { DocUtils } from "../../documents/Documents";
+import { DocUtils, Docs } from "../../documents/Documents";
import { DragManager } from "../../util/DragManager";
import { LinkManager } from "../../util/LinkManager";
import { undoBatch, UndoManager } from "../../util/UndoManager";
-import './DocumentLinksButton.scss';
import { DocumentView } from "./DocumentView";
+import { StrCast, Cast } from "../../../fields/Types";
import { LinkDescriptionPopup } from "./LinkDescriptionPopup";
+import { Hypothesis } from "../../util/HypothesisUtils";
+import { Id } from "../../../fields/FieldSymbols";
import { TaskCompletionBox } from "./TaskCompletedBox";
import React = require("react");
+import './DocumentLinksButton.scss';
+
const higflyout = require("@hig/flyout");
export const { anchorPoints } = higflyout;
export const Flyout = higflyout.default;
@@ -30,7 +35,12 @@ interface DocumentLinksButtonProps {
export class DocumentLinksButton extends React.Component<DocumentLinksButtonProps, {}> {
private _linkButton = React.createRef<HTMLDivElement>();
- @observable public static StartLink: DocumentView | undefined;
+ @observable public static StartLink: Doc | undefined;
+ @observable public static AnnotationId: string | undefined;
+ @observable public static AnnotationUri: string | undefined;
+
+ @observable public static invisibleWebDoc: Opt<Doc>;
+ public static invisibleWebRef = React.createRef<HTMLDivElement>();
@action @undoBatch
onLinkButtonMoved = (e: PointerEvent) => {
@@ -67,10 +77,10 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp
setupMoveUpEvents(this, e, this.onLinkButtonMoved, emptyFunction, action((e, doubleTap) => {
if (doubleTap && this.props.InMenu && this.props.StartLink) {
//action(() => Doc.BrushDoc(this.props.View.Document));
- if (DocumentLinksButton.StartLink === this.props.View) {
+ if (DocumentLinksButton.StartLink === this.props.View.props.Document) {
DocumentLinksButton.StartLink = undefined;
} else {
- DocumentLinksButton.StartLink = this.props.View;
+ DocumentLinksButton.StartLink = this.props.View.props.Document;
}
} else if (!this.props.InMenu) {
DocumentLinksButton.EditLink = this.props.View;
@@ -81,10 +91,12 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp
@action @undoBatch
onLinkClick = (e: React.MouseEvent): void => {
if (this.props.InMenu && this.props.StartLink) {
- if (DocumentLinksButton.StartLink === this.props.View) {
+ DocumentLinksButton.AnnotationId = undefined;
+ DocumentLinksButton.AnnotationUri = undefined;
+ if (DocumentLinksButton.StartLink === this.props.View.props.Document) {
DocumentLinksButton.StartLink = undefined;
} else {
- DocumentLinksButton.StartLink = this.props.View;
+ DocumentLinksButton.StartLink = this.props.View.props.Document;
}
//action(() => Doc.BrushDoc(this.props.View.Document));
@@ -95,37 +107,64 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp
completeLink = (e: React.PointerEvent): void => {
setupMoveUpEvents(this, e, returnFalse, emptyFunction, undoBatch(action((e, doubleTap) => {
- if (doubleTap && this.props.InMenu && !this.props.StartLink) {
- if (DocumentLinksButton.StartLink === this.props.View) {
+ if (doubleTap && !this.props.StartLink) {
+ if (DocumentLinksButton.StartLink === this.props.View.props.Document) {
DocumentLinksButton.StartLink = undefined;
- } else if (DocumentLinksButton.StartLink && DocumentLinksButton.StartLink !== this.props.View) {
- const linkDoc = DocUtils.MakeLink({ doc: DocumentLinksButton.StartLink.props.Document }, { doc: this.props.View.props.Document }, "long drag");
+ DocumentLinksButton.AnnotationId = undefined;
+ } else if (DocumentLinksButton.StartLink && DocumentLinksButton.StartLink !== this.props.View.props.Document) {
+ const sourceDoc = DocumentLinksButton.StartLink;
+ const targetDoc = this.props.View.props.Document;
+ const linkDoc = DocUtils.MakeLink({ doc: sourceDoc }, { doc: targetDoc }, "long drag");
+
LinkManager.currentLink = linkDoc;
- if (linkDoc) {
- TaskCompletionBox.textDisplayed = "Link Created";
- TaskCompletionBox.popupX = e.screenX;
- TaskCompletionBox.popupY = e.screenY - 133;
- TaskCompletionBox.taskCompleted = true;
-
- LinkDescriptionPopup.popupX = e.screenX;
- LinkDescriptionPopup.popupY = e.screenY - 100;
- LinkDescriptionPopup.descriptionPopup = true;
-
- setTimeout(action(() => TaskCompletionBox.taskCompleted = false), 2500);
- }
+
+ runInAction(() => {
+ if (linkDoc) {
+ TaskCompletionBox.textDisplayed = "Link Created";
+ TaskCompletionBox.popupX = e.screenX;
+ TaskCompletionBox.popupY = e.screenY - 133;
+ TaskCompletionBox.taskCompleted = true;
+
+ LinkDescriptionPopup.popupX = e.screenX;
+ LinkDescriptionPopup.popupY = e.screenY - 100;
+ LinkDescriptionPopup.descriptionPopup = true;
+
+ LinkDescriptionPopup.popupX = e.screenX;
+ LinkDescriptionPopup.popupY = e.screenY - 100;
+ LinkDescriptionPopup.descriptionPopup = true;
+
+ setTimeout(action(() => TaskCompletionBox.taskCompleted = false), 2500);
+ }
+ });
}
}
})));
}
- finishLinkClick = undoBatch(action((screenX: number, screenY: number) => {
- if (DocumentLinksButton.StartLink === this.props.View) {
+
+ public static finishLinkClick = undoBatch(action((screenX: number, screenY: number, startLink: Doc, endLink: Doc, startIsAnnotation: boolean, endLinkView?: DocumentView,) => {
+ if (startLink === endLink) {
DocumentLinksButton.StartLink = undefined;
- } else if (this.props.InMenu && !this.props.StartLink && DocumentLinksButton.StartLink && DocumentLinksButton.StartLink !== this.props.View) {
- const linkDoc = DocUtils.MakeLink({ doc: DocumentLinksButton.StartLink.props.Document }, { doc: this.props.View.props.Document }, "long drag");
+ DocumentLinksButton.AnnotationId = undefined;
+ DocumentLinksButton.AnnotationUri = undefined;
+ //!this.props.StartLink
+ } else if (startLink !== endLink) {
+ const linkDoc = DocUtils.MakeLink({ doc: startLink }, { doc: endLink }, DocumentLinksButton.AnnotationId ? "hypothes.is annotation" : "long drag");
// this notifies any of the subviews that a document is made so that they can make finer-grained hyperlinks (). see note above in onLInkButtonMoved
- DocumentLinksButton.StartLink._link = this.props.View._link = linkDoc;
- setTimeout(action(() => DocumentLinksButton.StartLink!._link = this.props.View._link = undefined), 0);
+ if (endLinkView) {
+ startLink._link = endLinkView._link = linkDoc;
+ setTimeout(action(() => startLink._link = endLinkView._link = undefined), 0);
+ }
LinkManager.currentLink = linkDoc;
+
+ if (DocumentLinksButton.AnnotationId && DocumentLinksButton.AnnotationUri) { // if linking from a Hypothes.is annotation
+ Doc.GetProto(linkDoc as Doc).linksToAnnotation = true;
+ Doc.GetProto(linkDoc as Doc).annotationId = DocumentLinksButton.AnnotationId;
+ Doc.GetProto(linkDoc as Doc).annotationUri = DocumentLinksButton.AnnotationUri;
+ const dashHyperlink = Utils.prepend("/doc/" + (startIsAnnotation ? endLink[Id] : startLink[Id]));
+ Hypothesis.makeLink(StrCast(startIsAnnotation ? endLink.title : startLink.title), dashHyperlink, DocumentLinksButton.AnnotationId,
+ (startIsAnnotation ? startLink : endLink)); // edit annotation to add a Dash hyperlink to the linked doc
+ }
+
if (linkDoc) {
TaskCompletionBox.textDisplayed = "Link Created";
TaskCompletionBox.popupX = screenX;
@@ -137,8 +176,7 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp
LinkDescriptionPopup.popupY = screenY - 100;
LinkDescriptionPopup.descriptionPopup = true;
}
-
- setTimeout(action(() => TaskCompletionBox.taskCompleted = false), 2500);
+ setTimeout(action(() => { TaskCompletionBox.taskCompleted = false; }), 2500);
}
}
}));
@@ -198,7 +236,7 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp
link : links.length}
</div>
- {this.props.InMenu && !this.props.StartLink && DocumentLinksButton.StartLink !== this.props.View ?
+ {this.props.InMenu && !this.props.StartLink && DocumentLinksButton.StartLink !== this.props.View.props.Document ?
<div className={"documentLinksButton-endLink"}
style={{
width: this.props.InMenu ? "20px" : "30px", height: this.props.InMenu ? "20px" : "30px",
@@ -206,12 +244,15 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp
border: DocumentLinksButton.StartLink ? "" : "none"
}}
onPointerDown={DocumentLinksButton.StartLink ? this.completeLink : emptyFunction}
- onClick={e => DocumentLinksButton.StartLink ? this.finishLinkClick(e.screenX, e.screenY) : emptyFunction} /> : (null)}
- {DocumentLinksButton.StartLink === this.props.View && this.props.InMenu && this.props.StartLink ? <div className={"documentLinksButton-startLink"}
- style={{ width: this.props.InMenu ? "20px" : "30px", height: this.props.InMenu ? "20px" : "30px" }}
- onPointerDown={this.clearLinks} onClick={this.clearLinks}
- /> : (null)}
- </div>;
+ onClick={e => DocumentLinksButton.StartLink ? DocumentLinksButton.finishLinkClick(e.screenX, e.screenY, DocumentLinksButton.StartLink, this.props.View.props.Document, true, this.props.View) : emptyFunction} /> : (null)
+ }
+ {
+ DocumentLinksButton.StartLink === this.props.View.props.Document && this.props.InMenu && this.props.StartLink ? <div className={"documentLinksButton-startLink"}
+ style={{ width: this.props.InMenu ? "20px" : "30px", height: this.props.InMenu ? "20px" : "30px" }}
+ onPointerDown={this.clearLinks} onClick={this.clearLinks}
+ /> : (null)
+ }
+ </div >;
return (!links.length) && !this.props.AlwaysOn ? (null) :
this.props.InMenu && (DocumentLinksButton.StartLink || this.props.StartLink) ?
@@ -223,6 +264,7 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp
</Tooltip> :
linkButton;
}
+
render() {
return this.linkButton;
}
diff --git a/src/server/ApiManagers/HypothesisManager.ts b/src/server/ApiManagers/HypothesisManager.ts
deleted file mode 100644
index 33badbc42..000000000
--- a/src/server/ApiManagers/HypothesisManager.ts
+++ /dev/null
@@ -1,44 +0,0 @@
-import ApiManager, { Registration } from "./ApiManager";
-import { Method, _permission_denied } from "../RouteManager";
-import { GoogleApiServerUtils } from "../apis/google/GoogleApiServerUtils";
-import { Database } from "../database";
-import { writeFile, readFile, readFileSync, existsSync } from "fs";
-import { serverPathToFile, Directory } from "./UploadManager";
-
-export default class HypothesisManager extends ApiManager {
-
- protected initialize(register: Registration): void {
-
- register({
- method: Method.GET,
- subscription: "/readHypothesisAccessToken",
- secureHandler: async ({ user, res }) => {
- if (existsSync(serverPathToFile(Directory.hypothesis, user.id))) {
- const read = readFileSync(serverPathToFile(Directory.hypothesis, user.id), "base64") || "";
- console.log("READ = " + read);
- res.send(read);
- } else res.send("");
- }
- });
-
- register({
- method: Method.POST,
- subscription: "/writeHypothesisAccessToken",
- secureHandler: async ({ user, req, res }) => {
- const write = req.body.authenticationCode;
- console.log("WRITE = " + write);
- res.send(await writeFile(serverPathToFile(Directory.hypothesis, user.id), write, "base64", () => { }));
- }
- });
-
- register({
- method: Method.GET,
- subscription: "/revokeHypothesisAccessToken",
- secureHandler: async ({ user, res }) => {
- await Database.Auxiliary.GoogleAccessToken.Revoke("dash-hyp-" + user.id);
- res.send();
- }
- });
-
- }
-} \ No newline at end of file
diff --git a/src/server/ApiManagers/UploadManager.ts b/src/server/ApiManagers/UploadManager.ts
index 515fbe4ff..bd8fe97eb 100644
--- a/src/server/ApiManagers/UploadManager.ts
+++ b/src/server/ApiManagers/UploadManager.ts
@@ -25,7 +25,6 @@ export enum Directory {
text = "text",
pdf_thumbnails = "pdf_thumbnails",
audio = "audio",
- hypothesis = "hypothesis"
}
export function serverPathToFile(directory: Directory, filename: string) {
diff --git a/src/server/apis/google/GoogleApiServerUtils.ts b/src/server/apis/google/GoogleApiServerUtils.ts
index b0157a85f..64bafe7fb 100644
--- a/src/server/apis/google/GoogleApiServerUtils.ts
+++ b/src/server/apis/google/GoogleApiServerUtils.ts
@@ -40,7 +40,6 @@ export namespace GoogleApiServerUtils {
export enum Service {
Documents = "Documents",
Slides = "Slides",
- Hypothesis = "Hypothesis"
}
/**
diff --git a/src/server/database.ts b/src/server/database.ts
index b7aa77f5d..41bf8b3da 100644
--- a/src/server/database.ts
+++ b/src/server/database.ts
@@ -304,7 +304,7 @@ export namespace Database {
*/
export enum AuxiliaryCollections {
GooglePhotosUploadHistory = "uploadedFromGooglePhotos",
- GoogleAccess = "googleAuthentication"
+ GoogleAccess = "googleAuthentication",
}
/**
diff --git a/src/server/index.ts b/src/server/index.ts
index 9185e3c5e..c4e6be8a2 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -8,7 +8,6 @@ import DeleteManager from "./ApiManagers/DeleteManager";
import DownloadManager from './ApiManagers/DownloadManager';
import GeneralGoogleManager from "./ApiManagers/GeneralGoogleManager";
import GooglePhotosManager from "./ApiManagers/GooglePhotosManager";
-import HypothesisManager from "./ApiManagers/HypothesisManager";
import PDFManager from "./ApiManagers/PDFManager";
import { SearchManager } from './ApiManagers/SearchManager';
import SessionManager from "./ApiManagers/SessionManager";
@@ -72,7 +71,6 @@ function routeSetter({ isRelease, addSupervisedRoute, logRegistrationOutcome }:
new DeleteManager(),
new UtilManager(),
new GeneralGoogleManager(),
- new HypothesisManager(),
new GooglePhotosManager(),
];
@@ -105,7 +103,6 @@ function routeSetter({ isRelease, addSupervisedRoute, logRegistrationOutcome }:
const serve: PublicHandler = ({ req, res }) => {
const detector = new mobileDetect(req.headers['user-agent'] || "");
const filename = detector.mobile() !== null ? 'mobile/image.html' : 'index.html';
- console.log(detector.is("iPhone"));
res.sendFile(path.join(__dirname, '../../deploy/' + filename));
};