aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/client/apis/HypothesisAuthenticationManager.tsx6
-rw-r--r--src/client/apis/hypothesis/HypothesisApiUtils.ts75
-rw-r--r--src/client/documents/Documents.ts2
-rw-r--r--src/client/util/CurrentUserUtils.ts2
-rw-r--r--src/client/views/MainView.tsx4
-rw-r--r--src/client/views/collections/CollectionLinearView.tsx2
-rw-r--r--src/client/views/linking/LinkMenuItem.tsx5
-rw-r--r--src/client/views/nodes/DocumentLinksButton.tsx73
-rw-r--r--src/client/views/nodes/DocumentView.tsx10
-rw-r--r--src/server/ApiManagers/HypothesisManager.ts14
-rw-r--r--src/server/database.ts39
11 files changed, 206 insertions, 26 deletions
diff --git a/src/client/apis/HypothesisAuthenticationManager.tsx b/src/client/apis/HypothesisAuthenticationManager.tsx
index cffb87227..b299f233e 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...";
@@ -44,12 +45,13 @@ 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 });
runInAction(() => {
this.success = true;
- this.credentials = response;
+ this.credentials = Hypothesis.extractUsername(userProfile.userid); // extract username from profile
});
this.resetState();
resolve(authenticationCode);
diff --git a/src/client/apis/hypothesis/HypothesisApiUtils.ts b/src/client/apis/hypothesis/HypothesisApiUtils.ts
new file mode 100644
index 000000000..f31fa60a1
--- /dev/null
+++ b/src/client/apis/hypothesis/HypothesisApiUtils.ts
@@ -0,0 +1,75 @@
+import { StrCast } from "../../../fields/Types";
+
+export namespace Hypothesis {
+ 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 made by @param username that
+ * contain @param searchKeyWord
+ */
+ export const searchAnnotation = async (username: string, searchKeyWord: string) => {
+ const base = 'https://api.hypothes.is/api/search';
+ const request = base + `?user=acct:${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');
+ }
+ };
+
+ // Find the most recent placeholder annotation created, and return its ID
+ export const getPlaceholderId = async (username: string, searchKeyWord: string) => {
+ const getResponse = await Hypothesis.searchAnnotation(username, searchKeyWord);
+ const id = getResponse.rows.length > 0 ? getResponse.rows[0].id : undefined;
+ return StrCast(id);
+ };
+
+ // Send request to Hypothes.is client to modify a placeholder annotation into a hyperlink to Dash
+ export const dispatchLinkRequest = async (title: string, url: string, annotationId: string) => {
+ const apiKey = "6879-GHmtDG_P2kmWNKM3hcHptEUZX3VMOUePkamCaOrJbSw";
+
+ const oldAnnotation = await fetchAnnotation(annotationId);
+ const oldText = StrCast(oldAnnotation.text);
+ const newHyperlink = `[${title}\n](${url})`;
+ const newText = oldText === "placeholder" ? newHyperlink : oldText + '\n\n' + newHyperlink;
+
+ console.log("DASH dispatching linkRequest");
+ document.dispatchEvent(new CustomEvent<{ newText: string, id: string, apiKey: string }>("linkRequest", {
+ detail: { newText: newText, id: annotationId, apiKey: apiKey },
+ bubbles: true
+ }));
+ };
+
+ // 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}`;
+ };
+
+ // Extract username from Hypothe.is's userId format
+ export const extractUsername = (userid: string) => {
+ const exp: RegExp = /(?<=\:)(.*?)(?=\@)/;
+ return exp.exec(userid)![0];
+ };
+} \ No newline at end of file
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index fa85d58f0..7b85d2e46 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 {
@@ -924,7 +925,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 8099228c6..cb2a025cb 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -424,7 +424,7 @@ export class CurrentUserUtils {
{ title: "Drag a document previewer", label: "Prev", icon: "expand", click: 'openOnRight(getCopy(this.dragFactory, true))', drag: 'getCopy(this.dragFactory,true)', dragFactory: doc.emptyDocHolder as Doc },
{ title: "Toggle a Calculator REPL", label: "repl", icon: "calculator", click: 'addOverlayWindow("ScriptingRepl", { x: 300, y: 100, width: 200, height: 200, title: "Scripting REPL" })' },
{ title: "Connect a Google Account", label: "Google Account", icon: "external-link-alt", click: 'GoogleAuthenticationManager.Instance.fetchOrGenerateAccessToken(true)' },
- { title: "Connect a Hypothesis Account", label: "Hypothesis Account", icon: "heading", click: 'HypothesisAuthenticationManager.Instance.fetchOrGenerateAccessToken(true)' },
+ { title: "Connect a Hypothesis Account", label: "Hypothesis Account", icon: "heading", click: 'HypothesisAuthenticationManager.Instance.fetchAccessToken(true)' },
];
}
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 81195b550..cf1129895 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -111,6 +111,10 @@ export class MainView extends React.Component {
}
});
});
+ // document.addEventListener("linkComplete", (e: any) => { // event used by Hypothes.is plugin to tell Dash when an annotation has been linked
+ // const annotatedUrl = e.details;
+ // console.log("This website " + annotatedUrl + " has a linked annotation");
+ // });
}
componentWillUnMount() {
diff --git a/src/client/views/collections/CollectionLinearView.tsx b/src/client/views/collections/CollectionLinearView.tsx
index c370415be..0bbe97ec9 100644
--- a/src/client/views/collections/CollectionLinearView.tsx
+++ b/src/client/views/collections/CollectionLinearView.tsx
@@ -170,7 +170,7 @@ export class CollectionLinearView extends CollectionSubView(LinearDocument) {
}}
onPointerDown={e => e.stopPropagation()} >
<span className="bottomPopup-text" >
- Creating link from: {DocumentLinksButton.StartLink.title} </span>
+ Creating link from: {DocumentLinksButton.AnnotationId ? "Annotation in" : ""} {DocumentLinksButton.StartLink.title} </span>
<span className="bottomPopup-descriptions" onClick={this.changeDescriptionSetting}
> Labels: {LinkDescriptionPopup.showDescriptions ? LinkDescriptionPopup.showDescriptions : "ON"}
</span>
diff --git a/src/client/views/linking/LinkMenuItem.tsx b/src/client/views/linking/LinkMenuItem.tsx
index 6af474513..ad3c12122 100644
--- a/src/client/views/linking/LinkMenuItem.tsx
+++ b/src/client/views/linking/LinkMenuItem.tsx
@@ -15,6 +15,7 @@ import { setupMoveUpEvents, emptyFunction } from '../../../Utils';
import { DocumentView } from '../nodes/DocumentView';
import { DocumentLinksButton } from '../nodes/DocumentLinksButton';
import { LinkDocPreview } from '../nodes/LinkDocPreview';
+import { WebField } from '../../../fields/URLField';
library.add(faEye, faEdit, faTimes, faArrowRight, faChevronDown, faChevronUp, faPencilAlt);
@@ -151,6 +152,8 @@ 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.follow) {
if (this.props.linkDoc.follow === "Default") {
DocumentManager.Instance.FollowLink(this.props.linkDoc, this.props.sourceDoc, doc => this.props.addDocTab(doc, "onRight"), false);
@@ -192,7 +195,7 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> {
<div className="linkMenu-text">
<p className="linkMenu-destination-title"
onPointerDown={this.followDefault}>
- {StrCast(this.props.destinationDoc.title)}</p>
+ {(this.props.linkDoc.linksToAnnotation ? "Annotation in " : "") + StrCast(this.props.destinationDoc.title)}</p>
{this.props.linkDoc.description !== "" ? <p className="linkMenu-description">
{StrCast(this.props.linkDoc.description)}</p> : null} </div>
diff --git a/src/client/views/nodes/DocumentLinksButton.tsx b/src/client/views/nodes/DocumentLinksButton.tsx
index 7fb447cab..32c344304 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 } 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";
const higflyout = require("@hig/flyout");
export const { anchorPoints } = higflyout;
export const Flyout = higflyout.default;
@@ -28,6 +34,29 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp
private _linkButton = React.createRef<HTMLDivElement>();
@observable public static StartLink: DocumentView | undefined;
+ @observable public static AnnotationId: string | undefined;
+
+ componentDidMount() {
+ // window.addEventListener("annotationCreated", (e: any) => { // event used by Hypothes.is plugin to tell Dash when an unlinked annotation has been created
+ // const id = e.details;
+ // const source = SelectionManager.SelectedDocuments()[0];
+ // runInAction(() => {
+ // DocumentLinksButton.AnnotationId = id;
+ // DocumentLinksButton.StartLink = source;
+ // });
+ // });
+ console.log("window", window);
+ window.addEventListener("fakeAnnotationCreated", async (e: any) => { // event used by Hypothes.is plugin to tell Dash when an unlinked annotation has been created
+ console.log("Helo fake annotation make");
+ // const id = e.detail;
+ const id = await Hypothesis.getPlaceholderId("melissaz", "placeholder"); // delete once eventListening between client & Dash works
+ const source = SelectionManager.SelectedDocuments()[0];
+ runInAction(() => {
+ DocumentLinksButton.AnnotationId = id;
+ DocumentLinksButton.StartLink = source;
+ });
+ });
+ }
@action
onLinkButtonMoved = (e: PointerEvent) => {
@@ -86,13 +115,25 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp
if (doubleTap) {
if (DocumentLinksButton.StartLink === this.props.View) {
DocumentLinksButton.StartLink = undefined;
+ DocumentLinksButton.AnnotationId = undefined;
+ console.log("reset to undefined (completeLink)");
// action((e: React.PointerEvent<HTMLDivElement>) => {
// Doc.UnBrushDoc(this.props.View.Document);
// });
} 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");
+
+ // if the link's source is a Hypothes.is annotation
+ if (DocumentLinksButton.AnnotationId) {
+ const sourceUrl = StrCast(sourceDoc.data.url); // the URL of the annotation's source web page
+ Doc.GetProto(linkDoc as Doc).linksToAnnotation = true;
+ Doc.GetProto(linkDoc as Doc).annotationUrl = Hypothesis.makeAnnotationUrl(DocumentLinksButton.AnnotationId, sourceUrl); // redirect web doc to this URL when following link
+ Hypothesis.dispatchLinkRequest(StrCast(targetDoc.title), Utils.prepend("/doc/" + targetDoc[Id]), DocumentLinksButton.AnnotationId); // update and link placeholder annotation
+ }
+
LinkManager.currentLink = linkDoc;
runInAction(() => {
LinkCreatedBox.popupX = e.screenX;
@@ -115,23 +156,35 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp
finishLinkClick = (e: React.MouseEvent) => {
if (DocumentLinksButton.StartLink === this.props.View) {
DocumentLinksButton.StartLink = undefined;
+ DocumentLinksButton.AnnotationId = undefined;
+ console.log("reset to undefined (finisheLinkClick)");
// action((e: React.PointerEvent<HTMLDivElement>) => {
// Doc.UnBrushDoc(this.props.View.Document);
// });
} 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");
+
+ // if the link is to a Hypothes.is annotation
+ if (DocumentLinksButton.AnnotationId) {
+ const sourceUrl = StrCast(sourceDoc.data.url); // the URL of the annotation's source web page
+ console.log("sourceAnnotationId, url", DocumentLinksButton.AnnotationId, sourceUrl);
+ Doc.GetProto(linkDoc as Doc).linksToAnnotation = true;
+ Doc.GetProto(linkDoc as Doc).annotationUrl = Hypothesis.makeAnnotationUrl(DocumentLinksButton.AnnotationId, sourceUrl); // redirect web doc to this URL when following link
+ Hypothesis.dispatchLinkRequest(StrCast(targetDoc.title), Utils.prepend("/doc/" + targetDoc[Id]), DocumentLinksButton.AnnotationId); // update and link placeholder annotation
+ }
+
LinkManager.currentLink = linkDoc;
runInAction(() => {
LinkCreatedBox.popupX = e.screenX;
LinkCreatedBox.popupY = e.screenY - 133;
LinkCreatedBox.linkCreated = true;
- if (LinkDescriptionPopup.showDescriptions === "ON" || !LinkDescriptionPopup.showDescriptions) {
- 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(() => { LinkCreatedBox.linkCreated = false; }), 2500);
});
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 97e3bc01c..f766c3867 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -43,6 +43,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';
@@ -773,6 +774,15 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
const cm = ContextMenu.Instance;
if (!cm) return;
+ cm.addItem({
+ description: "pretend we made an annotation", event: () => {
+ document.dispatchEvent(new CustomEvent("fakeAnnotationCreated", {
+ detail: "fakefakefakeid",
+ bubbles: true
+ }));
+ }, icon: "eye"
+ });
+
const customScripts = Cast(this.props.Document.contextMenuScripts, listSpec(ScriptField), []);
Cast(this.props.Document.contextMenuLabels, listSpec("string"), []).forEach((label, i) =>
cm.addItem({ description: label, event: () => customScripts[i]?.script.run({ this: this.layoutDoc, self: this.rootDoc }), icon: "sticky-note" }));
diff --git a/src/server/ApiManagers/HypothesisManager.ts b/src/server/ApiManagers/HypothesisManager.ts
index 33badbc42..73c707a55 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?.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);
+ 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..767d38350 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,42 @@ export namespace Database {
}
+ export namespace HypothesisAccessToken {
+ /**
+ * Format stored in database.
+ */
+ interface StoredCredentials {
+ userId: string;
+ hypothesisApiKey: 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) => {
+ return Instance.insert({ userId, hypothesisApiKey }, 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);
+ }
+ };
+ }
}
}