diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/client/apis/HypothesisAuthenticationManager.tsx | 6 | ||||
-rw-r--r-- | src/client/apis/hypothesis/HypothesisApiUtils.ts | 75 | ||||
-rw-r--r-- | src/client/documents/Documents.ts | 2 | ||||
-rw-r--r-- | src/client/util/CurrentUserUtils.ts | 2 | ||||
-rw-r--r-- | src/client/views/MainView.tsx | 4 | ||||
-rw-r--r-- | src/client/views/collections/CollectionLinearView.tsx | 2 | ||||
-rw-r--r-- | src/client/views/linking/LinkMenuItem.tsx | 5 | ||||
-rw-r--r-- | src/client/views/nodes/DocumentLinksButton.tsx | 73 | ||||
-rw-r--r-- | src/client/views/nodes/DocumentView.tsx | 10 | ||||
-rw-r--r-- | src/server/ApiManagers/HypothesisManager.ts | 14 | ||||
-rw-r--r-- | src/server/database.ts | 39 |
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); + } + }; + } } } |