aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/client/apis/HypothesisAuthenticationManager.tsx24
-rw-r--r--src/client/apis/hypothesis/HypothesisApiUtils.ts117
-rw-r--r--src/client/documents/Documents.ts2
-rw-r--r--src/client/util/CurrentUserUtils.ts4
-rw-r--r--src/client/views/collections/CollectionLinearView.tsx2
-rw-r--r--src/client/views/linking/LinkMenuItem.tsx11
-rw-r--r--src/client/views/nodes/DocumentLinksButton.tsx80
-rw-r--r--src/client/views/nodes/DocumentView.tsx1
-rw-r--r--src/server/ApiManagers/HypothesisManager.ts14
-rw-r--r--src/server/database.ts40
10 files changed, 265 insertions, 30 deletions
diff --git a/src/client/apis/HypothesisAuthenticationManager.tsx b/src/client/apis/HypothesisAuthenticationManager.tsx
index c3e8d2fff..d4a81b3eb 100644
--- a/src/client/apis/HypothesisAuthenticationManager.tsx
+++ b/src/client/apis/HypothesisAuthenticationManager.tsx
@@ -6,6 +6,7 @@ import { Opt } from "../../fields/Doc";
import { Networking } from "../Network";
import "./HypothesisAuthenticationManager.scss";
import { Scripting } from "../util/Scripting";
+import { Hypothesis } from "./hypothesis/HypothesisApiUtils";
const prompt = "Paste authorization code here...";
@@ -18,7 +19,7 @@ export default class HypothesisAuthenticationManager extends React.Component<{}>
@observable private showPasteTargetState = false;
@observable private success: Opt<boolean> = undefined;
@observable private displayLauncher = true;
- @observable private credentials: string;
+ @observable private credentials: { username: string, apiKey: string } | undefined;
private disposer: Opt<IReactionDisposer>;
private set isOpen(value: boolean) {
@@ -34,9 +35,10 @@ export default class HypothesisAuthenticationManager extends React.Component<{}>
}
public fetchAccessToken = async (displayIfFound = false) => {
- const response: any = await Networking.FetchFromServer("/readHypothesisAccessToken");
+ const jsonResponse = await Networking.FetchFromServer("/readHypothesisAccessToken");
+ const response = jsonResponse !== "" ? JSON.parse(jsonResponse) : undefined;
// if this is an authentication url, activate the UI to register the new access token
- if (!response) { // new RegExp(AuthenticationUrl).test(response)) {
+ if (!response) {
this.isOpen = true;
this.authenticationLink = response;
return new Promise<string>(async resolve => {
@@ -44,12 +46,14 @@ export default class HypothesisAuthenticationManager extends React.Component<{}>
this.disposer = reaction(
() => this.authenticationCode,
async authenticationCode => {
- if (authenticationCode) {
+ const userProfile = authenticationCode && await Hypothesis.fetchUser(authenticationCode);
+ if (userProfile && userProfile.userid !== null) {
this.disposer?.();
- Networking.PostToServer("/writeHypothesisAccessToken", { authenticationCode });
+ const hypothesisUsername = Hypothesis.extractUsername(userProfile.userid); // extract username from profile
+ Networking.PostToServer("/writeHypothesisAccessToken", { authenticationCode, hypothesisUsername });
runInAction(() => {
this.success = true;
- this.credentials = response;
+ this.credentials = { username: hypothesisUsername, apiKey: authenticationCode! };
});
this.resetState();
resolve(authenticationCode);
@@ -67,7 +71,7 @@ export default class HypothesisAuthenticationManager extends React.Component<{}>
this.resetState(-1, -1);
this.isOpen = true;
}
- return response.access_token;
+ return response;
}
resetState = action((visibleForMS: number = 3000, fadesOutInMS: number = 500) => {
@@ -76,7 +80,7 @@ export default class HypothesisAuthenticationManager extends React.Component<{}>
this.isOpen = false;
this.success = undefined;
this.displayLauncher = true;
- this.credentials = "";
+ this.credentials = undefined;
this.shouldShowPasteTarget = false;
this.authenticationCode = undefined;
});
@@ -91,7 +95,7 @@ export default class HypothesisAuthenticationManager extends React.Component<{}>
setTimeout(action(() => {
this.success = undefined;
this.displayLauncher = true;
- this.credentials = "";
+ this.credentials = undefined;
}), fadesOutInMS);
}), visibleForMS);
}
@@ -122,7 +126,7 @@ export default class HypothesisAuthenticationManager extends React.Component<{}>
<>
<span
className={'welcome'}
- >Welcome to Dash, {this.credentials}
+ >Welcome to Dash, {this.credentials.username}
</span>
<div
className={'disconnect'}
diff --git a/src/client/apis/hypothesis/HypothesisApiUtils.ts b/src/client/apis/hypothesis/HypothesisApiUtils.ts
new file mode 100644
index 000000000..4c9fef45c
--- /dev/null
+++ b/src/client/apis/hypothesis/HypothesisApiUtils.ts
@@ -0,0 +1,117 @@
+import { StrCast, Cast } from "../../../fields/Types";
+import HypothesisAuthenticationManager from "../HypothesisAuthenticationManager";
+import { SearchUtil } from "../../util/SearchUtil";
+import { action } from "mobx";
+import { Doc } from "../../../fields/Doc";
+import { DocumentType } from "../../documents/DocumentTypes";
+
+export namespace Hypothesis {
+
+ const getCredentials = async () => HypothesisAuthenticationManager.Instance.fetchAccessToken();
+
+ export const fetchAnnotation = async (annotationId: string) => {
+ const response = await fetch(`https://api.hypothes.is/api/annotations/${annotationId}`);
+ if (response.ok) {
+ return response.json();
+ } else {
+ throw new Error('DASH: Error in fetchAnnotation GET request');
+ }
+ };
+
+ /**
+ * Searches for annotations authored by the current user that contain @param searchKeyWord
+ */
+ export const searchAnnotation = async (searchKeyWord: string) => {
+ const credentials = await getCredentials();
+ const base = 'https://api.hypothes.is/api/search';
+ const request = base + `?user=acct:${credentials.username}@hypothes.is&text=${searchKeyWord}`;
+ console.log("DASH Querying " + request);
+ const response = await fetch(request);
+ if (response.ok) {
+ return response.json();
+ } else {
+ throw new Error('DASH: Error in searchAnnotation GET request');
+ }
+ };
+
+ export const fetchUser = async (apiKey: string) => {
+ const response = await fetch('https://api.hypothes.is/api/profile', {
+ headers: {
+ 'Authorization': `Bearer ${apiKey}`,
+ },
+ });
+ if (response.ok) {
+ return response.json();
+ } else {
+ throw new Error('DASH: Error in fetchUser GET request');
+ }
+ };
+
+ export const editAnnotation = async (annotationId: string, newText: string) => {
+ console.log("DASH dispatching editRequest");
+ const credentials = await getCredentials();
+ document.dispatchEvent(new CustomEvent<{ newText: string, id: string, apiKey: string }>("editRequest", {
+ detail: { newText: newText, id: annotationId, apiKey: credentials.apiKey },
+ bubbles: true
+ }));
+ };
+
+ /**
+ * Edit an annotation with ID @param annotationId to add a hyperlink to a Dash document, which needs to be
+ * written in the format [@param title](@param url)
+ */
+ export const makeLink = async (title: string, url: string, annotationId: string) => {
+ const oldAnnotation = await fetchAnnotation(annotationId);
+ const oldText = StrCast(oldAnnotation.text);
+ const newHyperlink = `[${title}\n](${url})`;
+ const newText = oldText === "placeholder" ? newHyperlink : oldText + '\n\n' + newHyperlink; // if this is not the first link in the annotation, add link on new line
+ await editAnnotation(annotationId, newText);
+ };
+
+ /**
+ * Edit an annotation with ID @param annotationId to delete a hyperlink to the Dash document with URL @param linkUrl
+ */
+ export const deleteLink = async (annotationId: string, linkUrl: string) => {
+ const annotation = await fetchAnnotation(annotationId);
+ const regex = new RegExp(`(\[[^[]*)\(${linkUrl.replace('/', '\/')}\)`);
+ const target = regex.exec(annotation.text); // use regex to extract the link to be deleted, which is written in [title](hyperlink) format
+ target && console.log(target);
+ // target && editAnnotation(annotationId, annotation.text.replace(target[0], ''));
+ };
+
+ // Finds the most recent placeholder annotation created and returns its ID
+ export const getPlaceholderId = async (searchKeyWord: string) => {
+ const getResponse = await Hypothesis.searchAnnotation(searchKeyWord);
+ const id = getResponse.rows.length > 0 ? getResponse.rows[0].id : undefined;
+ const uri = getResponse.rows.length > 0 ? getResponse.rows[0].uri : undefined;
+ return id ? { id, uri } : undefined;
+ };
+
+ // Construct an URL which will scroll the web page to a specific annotation's position
+ export const makeAnnotationUrl = (annotationId: string, baseUrl: string) => {
+ return `https://hyp.is/${annotationId}/${baseUrl}`; // embeds the generic version of Hypothes.is client, not the Dash version
+ // return baseUrl + '#annotations:' + annotationId;
+ };
+
+ // Extract username from Hypothe.is's userId format
+ export const extractUsername = (userid: string) => {
+ const regex = new RegExp('(?<=\:)(.*?)(?=\@)/');
+ return regex.exec(userid)![0];
+ };
+
+ // Return corres
+ export const getWebDocs = async (uri: string) => {
+ const results: Doc[] = [];
+ await SearchUtil.Search(uri, 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.type === DocumentType.WEB && doc.data);
+
+ console.log("docs", docs);
+ console.log("FILTEREDDOCS: ", filteredDocs);
+ filteredDocs.forEach(doc => {
+ results.push(doc);
+ });
+ }));
+ return results;
+ };
+} \ No newline at end of file
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index cd2792226..f1d4ec46c 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -50,6 +50,7 @@ import { DashWebRTCVideo } from "../views/webcam/DashWebRTCVideo";
import { DocumentType } from "./DocumentTypes";
import { Networking } from "../Network";
import { Upload } from "../../server/SharedMediaTypes";
+import { Hypothesis } from "../apis/hypothesis/HypothesisApiUtils";
const path = require('path');
export interface DocumentOptions {
@@ -932,7 +933,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 b47cfb33a..ff33d35e0 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -438,8 +438,10 @@ 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)' },
+ { toolTip: "Connect a Hypothesis Account", title: "Hypothesis Account", icon: "heading", click: 'HypothesisAuthenticationManager.Instance.fetchAccessToken(true)' },
];
}
diff --git a/src/client/views/collections/CollectionLinearView.tsx b/src/client/views/collections/CollectionLinearView.tsx
index 407524353..cd6e60de6 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.title}
+ Creating link from: {DocumentLinksButton.AnnotationId ? "Annotation in" : ""} {DocumentLinksButton.StartLink.title}
</span>
<Tooltip title={<><div className="dash-tooltip">{LinkDescriptionPopup.showDescriptions ? "Turn off description pop-up" :
diff --git a/src/client/views/linking/LinkMenuItem.tsx b/src/client/views/linking/LinkMenuItem.tsx
index d1c839c3b..cae90cd0c 100644
--- a/src/client/views/linking/LinkMenuItem.tsx
+++ b/src/client/views/linking/LinkMenuItem.tsx
@@ -11,10 +11,12 @@ 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 } from '../../../Utils';
import { DocumentView } from '../nodes/DocumentView';
import { DocumentLinksButton } from '../nodes/DocumentLinksButton';
import { LinkDocPreview } from '../nodes/LinkDocPreview';
+import { Hypothesis } from '../../apis/hypothesis/HypothesisApiUtils';
+import { Id } from '../../../fields/FieldSymbols';
import { Tooltip } from '@material-ui/core';
import { DocumentType } from '../../documents/DocumentTypes';
import { undoBatch } from '../../util/UndoManager';
@@ -152,16 +154,21 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> {
DocumentLinksButton.EditLink = undefined;
LinkDocPreview.LinkInfo = undefined;
+ // this.props.linkDoc.linksToAnnotation && (Doc.GetProto(this.props.destinationDoc).data = new WebField(StrCast(this.props.linkDoc.annotationUrl))); // if destination is a Hypothes.is annotation, redirect website to the annotation's URL to scroll to the annotation
if (this.props.linkDoc.followLinkLocation && this.props.linkDoc.followLinkLocation !== "Default") {
this.props.addDocTab(this.props.destinationDoc, StrCast(this.props.linkDoc.followLinkLocation));
} else {
DocumentManager.Instance.FollowLink(this.props.linkDoc, this.props.sourceDoc, doc => this.props.addDocTab(doc, "onRight"), false);
}
+
+ this.props.linkDoc.linksToAnnotation && setTimeout(() => window.open(StrCast(this.props.linkDoc.annotationUrl), '_blank'), 1000); // open external page if hypothes.is annotation
}
@undoBatch
@action
deleteLink = (): void => {
+ this.props.linkDoc.linksToAnnotation && Hypothesis.deleteLink(StrCast(this.props.linkDoc.annotationId), Utils.prepend("/doc/" + this.props.sourceDoc[Id])); // delete hyperlink in annotation
+ this.props.linkDoc.linksToAnnotation && console.log("annotationId", this.props.linkDoc.annotationId);
LinkManager.Instance.deleteLink(this.props.linkDoc);
LinkDocPreview.LinkInfo = undefined;
DocumentLinksButton.EditLink = undefined;
@@ -232,7 +239,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 ? "Annotation in" : ""} {title}
</p>
</div>
{this.props.linkDoc.description !== "" ? <p className="linkMenu-description">
diff --git a/src/client/views/nodes/DocumentLinksButton.tsx b/src/client/views/nodes/DocumentLinksButton.tsx
index b17accfd6..57fa26b80 100644
--- a/src/client/views/nodes/DocumentLinksButton.tsx
+++ b/src/client/views/nodes/DocumentLinksButton.tsx
@@ -1,18 +1,24 @@
import { action, computed, observable, runInAction } from "mobx";
import { observer } from "mobx-react";
import { Doc, DocListCast } from "../../../fields/Doc";
-import { emptyFunction, setupMoveUpEvents, returnFalse } from "../../../Utils";
+import { emptyFunction, setupMoveUpEvents, returnFalse, Utils } from "../../../Utils";
import { DragManager } from "../../util/DragManager";
import { UndoManager, undoBatch } from "../../util/UndoManager";
import './DocumentLinksButton.scss';
import { DocumentView } from "./DocumentView";
import React = require("react");
-import { DocUtils } from "../../documents/Documents";
+import { DocUtils, Docs } from "../../documents/Documents";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { LinkDocPreview } from "./LinkDocPreview";
import { LinkCreatedBox } from "./LinkCreatedBox";
+import { SelectionManager } from "../../util/SelectionManager";
+import { Document } from "../../../fields/documentSchemas";
+import { StrCast } from "../../../fields/Types";
+
import { LinkDescriptionPopup } from "./LinkDescriptionPopup";
import { LinkManager } from "../../util/LinkManager";
+import { Hypothesis } from "../../apis/hypothesis/HypothesisApiUtils";
+import { Id } from "../../../fields/FieldSymbols";
import { Tooltip } from "@material-ui/core";
const higflyout = require("@hig/flyout");
export const { anchorPoints } = higflyout;
@@ -30,6 +36,45 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp
private _linkButton = React.createRef<HTMLDivElement>();
@observable public static StartLink: DocumentView | undefined;
+ @observable public static AnnotationId: string | undefined;
+ @observable public static AnnotationUri: string | undefined;
+
+ componentDidMount() {
+ window.addEventListener("message", this.newAnnotation); // listen for a new Hypothes.is annotation from an iframe inside Dash
+ document.addEventListener("linkAnnotationToDash", this.linkAnnotation); // listen for event from Hypothes.is extension to link an existing annotation
+ }
+
+ // start link from new Hypothes.is annotation
+ // TODO: pass in placeholderId directly from client
+ @action
+ newAnnotation = async (e: any) => {
+ if (e.origin === "http://localhost:1050" && e.data.message === "annotation created") {
+ console.log("DASH received message: annotation created");
+ window.removeEventListener("message", this.newAnnotation);
+ const response = await Hypothesis.getPlaceholderId("placeholder");
+ const source = SelectionManager.SelectedDocuments()[0];
+ response && runInAction(() => {
+ DocumentLinksButton.AnnotationId = response.id;
+ DocumentLinksButton.AnnotationUri = response.uri;
+ DocumentLinksButton.StartLink = source;
+ });
+ }
+ }
+
+ @action
+ linkAnnotation = async (e: any) => { // event sent by hypothes.is plugin to tell Dash which annotation we're linking from
+ const annotationId = e.detail.id;
+ const annotationUri = e.detail.uri;
+ console.log(annotationId, annotationUri);
+ // const source = await Hypothesis.getWebDoc(annotationUri);
+
+ const source = SelectionManager.SelectedDocuments()[0]; // TO BE FIXED, currently link just starts from whichever doc is selected
+ runInAction(() => {
+ DocumentLinksButton.AnnotationId = annotationId;
+ DocumentLinksButton.AnnotationUri = annotationUri;
+ DocumentLinksButton.StartLink = source;
+ });
+ }
@action @undoBatch
onLinkButtonMoved = (e: PointerEvent) => {
@@ -91,10 +136,22 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp
if (doubleTap && this.props.InMenu && !!!this.props.StartLink) {
if (DocumentLinksButton.StartLink === this.props.View) {
DocumentLinksButton.StartLink = undefined;
+ DocumentLinksButton.AnnotationId = 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");
+ const sourceDoc = DocumentLinksButton.StartLink.props.Document;
+ const targetDoc = this.props.View.props.Document;
+ const linkDoc = DocUtils.MakeLink({ doc: sourceDoc }, { doc: targetDoc }, DocumentLinksButton.AnnotationId ? "hypothes.is annotation" : "long drag");
+
+ // TODO: Not currently possible to drag to complete links to annotations
+ if (DocumentLinksButton.AnnotationId && DocumentLinksButton.AnnotationUri) {
+ const sourceUrl = DocumentLinksButton.AnnotationUri;
+ Doc.GetProto(linkDoc as Doc).linksToAnnotation = true;
+ Doc.GetProto(linkDoc as Doc).annotationId = DocumentLinksButton.AnnotationId;
+ Doc.GetProto(linkDoc as Doc).annotationUrl = Hypothesis.makeAnnotationUrl(DocumentLinksButton.AnnotationId, sourceUrl); // redirect web doc to this URL when following link
+ Hypothesis.makeLink(StrCast(targetDoc.title), Utils.prepend("/doc/" + targetDoc[Id]), DocumentLinksButton.AnnotationId); // update and link placeholder annotation
+ }
+
LinkManager.currentLink = linkDoc;
runInAction(() => {
@@ -121,15 +178,28 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp
finishLinkClick = (e: React.MouseEvent) => {
if (DocumentLinksButton.StartLink === this.props.View) {
DocumentLinksButton.StartLink = undefined;
+ DocumentLinksButton.AnnotationId = undefined;
+ window.removeEventListener("message", this.newAnnotation);
+ window.addEventListener("message", this.newAnnotation);
} else {
if (this.props.InMenu && !!!this.props.StartLink) {
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");
+ const linkDoc = DocUtils.MakeLink({ doc: DocumentLinksButton.StartLink.props.Document }, { doc: this.props.View.props.Document }, 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
runInAction(() => DocumentLinksButton.StartLink!._link = this.props.View._link = linkDoc);
setTimeout(action(() => DocumentLinksButton.StartLink!._link = this.props.View._link = undefined), 0);
LinkManager.currentLink = linkDoc;
+ // if the link is to a Hypothes.is annotation
+ if (DocumentLinksButton.AnnotationId && DocumentLinksButton.AnnotationUri) {
+ const sourceUrl = DocumentLinksButton.AnnotationUri; // the URL of the annotation's source web page
+ const targetDoc = this.props.View.props.Document;
+ Doc.GetProto(linkDoc as Doc).linksToAnnotation = true;
+ Doc.GetProto(linkDoc as Doc).annotationId = DocumentLinksButton.AnnotationId;
+ Doc.GetProto(linkDoc as Doc).annotationUrl = Hypothesis.makeAnnotationUrl(DocumentLinksButton.AnnotationId, sourceUrl); // redirect web doc to this URL when following link
+ Hypothesis.makeLink(StrCast(targetDoc.title), Utils.prepend("/doc/" + targetDoc[Id]), DocumentLinksButton.AnnotationId); // update and link placeholder annotation
+ }
+
runInAction(() => {
if (linkDoc) {
LinkCreatedBox.popupX = e.screenX;
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 998c6798e..555e58bb0 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -42,6 +42,7 @@ import React = require("react");
import { DocumentLinksButton } from './DocumentLinksButton';
import { MobileInterface } from '../../../mobile/MobileInterface';
import { LinkCreatedBox } from './LinkCreatedBox';
+import { Hypothesis } from '../../apis/hypothesis/HypothesisApiUtils';
import { LinkDescriptionPopup } from './LinkDescriptionPopup';
import { LinkManager } from '../../util/LinkManager';
diff --git a/src/server/ApiManagers/HypothesisManager.ts b/src/server/ApiManagers/HypothesisManager.ts
index 33badbc42..370d02a49 100644
--- a/src/server/ApiManagers/HypothesisManager.ts
+++ b/src/server/ApiManagers/HypothesisManager.ts
@@ -13,11 +13,8 @@ export default class HypothesisManager extends ApiManager {
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("");
+ const credentials = await Database.Auxiliary.HypothesisAccessToken.Fetch(user.id);
+ res.send(credentials ? { username: credentials.hypothesisUsername, apiKey: credentials.hypothesisApiKey } : "");
}
});
@@ -25,9 +22,8 @@ export default class HypothesisManager extends ApiManager {
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", () => { }));
+ await Database.Auxiliary.HypothesisAccessToken.Write(user.id, req.body.authenticationCode, req.body.hypothesisUsername);
+ res.send();
}
});
@@ -35,7 +31,7 @@ export default class HypothesisManager extends ApiManager {
method: Method.GET,
subscription: "/revokeHypothesisAccessToken",
secureHandler: async ({ user, res }) => {
- await Database.Auxiliary.GoogleAccessToken.Revoke("dash-hyp-" + user.id);
+ await Database.Auxiliary.HypothesisAccessToken.Revoke(user.id);
res.send();
}
});
diff --git a/src/server/database.ts b/src/server/database.ts
index 2372cbcf2..456c1c254 100644
--- a/src/server/database.ts
+++ b/src/server/database.ts
@@ -304,7 +304,8 @@ export namespace Database {
*/
export enum AuxiliaryCollections {
GooglePhotosUploadHistory = "uploadedFromGooglePhotos",
- GoogleAccess = "googleAuthentication"
+ GoogleAccess = "googleAuthentication",
+ HypothesisAccess = "hypothesisAuthentication"
}
/**
@@ -405,6 +406,43 @@ export namespace Database {
}
+ export namespace HypothesisAccessToken {
+ /**
+ * Format stored in database.
+ */
+ interface StoredCredentials {
+ userId: string;
+ hypothesisApiKey: string;
+ hypothesisUsername: string;
+ _id?: string;
+ }
+
+ /**
+ * Writes the @param hypothesisApiKey to the database, associated
+ * with @param userId for later retrieval and updating.
+ */
+ export const Write = async (userId: string, hypothesisApiKey: string, hypothesisUsername: string) => {
+ return Instance.insert({ userId, hypothesisApiKey, hypothesisUsername }, AuxiliaryCollections.HypothesisAccess);
+ };
+
+ /**
+ * Retrieves the credentials associaed with @param userId
+ * and optionally removes their database id according to @param removeId.
+ */
+ export const Fetch = async (userId: string, removeId = true): Promise<Opt<StoredCredentials>> => {
+ return SanitizedSingletonQuery<StoredCredentials>({ userId }, AuxiliaryCollections.HypothesisAccess, removeId);
+ };
+
+ /**
+ * Revokes the credentials associated with @param userId.
+ */
+ export const Revoke = async (userId: string) => {
+ const entry = await Fetch(userId, false);
+ if (entry) {
+ Instance.delete({ _id: entry._id }, AuxiliaryCollections.HypothesisAccess);
+ }
+ };
+ }
}
}