diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/client/apis/HypothesisAuthenticationManager.tsx | 160 | ||||
-rw-r--r-- | src/client/apis/hypothesis/HypothesisUtils.ts | 154 | ||||
-rw-r--r-- | src/client/documents/Documents.ts | 1 | ||||
-rw-r--r-- | src/client/util/CurrentUserUtils.ts | 3 | ||||
-rw-r--r-- | src/client/util/SettingsManager.tsx | 6 | ||||
-rw-r--r-- | src/client/views/GlobalKeyHandler.ts | 2 | ||||
-rw-r--r-- | src/client/views/MainView.tsx | 5 | ||||
-rw-r--r-- | src/client/views/collections/CollectionLinearView.tsx | 2 | ||||
-rw-r--r-- | src/client/views/collections/CollectionSubView.tsx | 5 | ||||
-rw-r--r-- | src/client/views/collections/collectionFreeForm/MarqueeView.tsx | 2 | ||||
-rw-r--r-- | src/client/views/linking/LinkEditor.tsx | 6 | ||||
-rw-r--r-- | src/client/views/linking/LinkMenuItem.tsx | 21 | ||||
-rw-r--r-- | src/client/views/nodes/DocumentLinksButton.tsx | 127 | ||||
-rw-r--r-- | src/server/ApiManagers/HypothesisManager.ts | 44 | ||||
-rw-r--r-- | src/server/ApiManagers/UploadManager.ts | 1 | ||||
-rw-r--r-- | src/server/apis/google/GoogleApiServerUtils.ts | 1 | ||||
-rw-r--r-- | src/server/database.ts | 2 | ||||
-rw-r--r-- | src/server/index.ts | 1 |
18 files changed, 271 insertions, 272 deletions
diff --git a/src/client/apis/HypothesisAuthenticationManager.tsx b/src/client/apis/HypothesisAuthenticationManager.tsx deleted file mode 100644 index c3e8d2fff..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/apis/hypothesis/HypothesisUtils.ts b/src/client/apis/hypothesis/HypothesisUtils.ts new file mode 100644 index 000000000..5c6e4d31d --- /dev/null +++ b/src/client/apis/hypothesis/HypothesisUtils.ts @@ -0,0 +1,154 @@ +import { StrCast, Cast } from "../../../fields/Types"; +import { SearchUtil } from "../../util/SearchUtil"; +import { action, runInAction } from "mobx"; +import { Doc } from "../../../fields/Doc"; +import { DocumentType } from "../../documents/DocumentTypes"; +import { Docs, DocUtils } from "../../documents/Documents"; +import { SelectionManager } from "../../util/SelectionManager"; +import { WebField } from "../../../fields/URLField"; +import { DocumentManager } from "../../util/DocumentManager"; +import { DocumentLinksButton } from "../../views/nodes/DocumentLinksButton"; +import { LinkManager } from "../../util/LinkManager"; +import { TaskCompletionBox } from "../../views/nodes/TaskCompletedBox"; +import { Utils } from "../../../Utils"; +import { LinkDescriptionPopup } from "../../views/nodes/LinkDescriptionPopup"; +import { Id } from "../../../fields/FieldSymbols"; + +export namespace Hypothesis { + + // Return web doc with the given uri, or create and create a new doc with the given uri + export const getSourceWebDoc = async (uri: string) => { + const currentDoc = SelectionManager.SelectedDocuments()[0].props.Document; + console.log(Cast(currentDoc.data, WebField)?.url.href === uri, uri, Cast(currentDoc.data, WebField)?.url.href); + if (Cast(currentDoc.data, WebField)?.url.href === uri) return currentDoc; // always check first whether the current doc is the source, only resort to 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 => console.log("web docs:", doc.title, Cast(doc.data, WebField)?.url.href)); + filteredDocs.forEach(doc => { uri === Cast(doc.data, WebField)?.url.href && results.push(doc); }); // TODO check history? imperfect matches? + })); + + results.forEach(doc => console.log(doc.title, StrCast(doc.data))); + + return results.length ? results[0] : Docs.Create.WebDocument(uri, { _nativeWidth: 850, _nativeHeight: 962, _width: 600, UseCors: true }); // create and return a new Web doc with given uri if no matching docs are found + }; + + // Send Hypothes.is client request to edit an annotation to add a Dash hyperlink + export const makeLink = async (title: string, url: string, annotationId: string) => { + console.log("SEND addLink"); + const newHyperlink = `[${title}\n](${url})`; + document.dispatchEvent(new CustomEvent<{ newHyperlink: string, id: string }>("addLink", { + detail: { newHyperlink: newHyperlink, id: annotationId }, + bubbles: true + })); + }; + + // Send Hypothes.is client request to edit an annotation to find and remove a dash hyperlink + export const deleteLink = async (annotationId: string, linkUrl: string) => { + document.dispatchEvent(new CustomEvent<{ targetUrl: string, id: string }>("deleteLink", { + detail: { targetUrl: linkUrl, id: annotationId }, + bubbles: true + })); + }; + + // 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 = e.detail.uri; + const sourceDoc: Doc = await getSourceWebDoc(annotationUri); + + if (!DocumentLinksButton.StartLink) { // start link if there were none already started + runInAction(() => { + DocumentLinksButton.AnnotationId = annotationId; + DocumentLinksButton.AnnotationUri = annotationUri; + DocumentLinksButton.StartLink = sourceDoc; + }); + } else if (!Doc.AreProtosEqual(sourceDoc, DocumentLinksButton.StartLink)) { // if a link has already been started, complete the link to the sourceDoc + console.log("completing link", sourceDoc.title); + runInAction(() => { + DocumentLinksButton.AnnotationId = annotationId; + DocumentLinksButton.AnnotationUri = annotationUri; + }); + + const linkDoc = DocUtils.MakeLink({ doc: DocumentLinksButton.StartLink }, { doc: sourceDoc }, DocumentLinksButton.AnnotationId ? "hypothes.is annotation" : "long drag"); + LinkManager.currentLink = linkDoc; + + Doc.GetProto(linkDoc as Doc).linksToAnnotation = true; + Doc.GetProto(linkDoc as Doc).annotationId = DocumentLinksButton.AnnotationId; + Doc.GetProto(linkDoc as Doc).annotationUri = DocumentLinksButton.AnnotationUri; + makeLink(StrCast(DocumentLinksButton.StartLink.title), Utils.prepend("/doc/" + DocumentLinksButton.StartLink[Id]), StrCast(DocumentLinksButton.AnnotationId)); // update and link placeholder annotation + + runInAction(() => { + if (linkDoc) { + TaskCompletionBox.textDisplayed = "Link Created"; + TaskCompletionBox.popupX = screenX; + TaskCompletionBox.popupY = screenY - 133; + TaskCompletionBox.taskCompleted = true; + + if (LinkDescriptionPopup.showDescriptions === "ON" || !LinkDescriptionPopup.showDescriptions) { + LinkDescriptionPopup.popupX = screenX; + LinkDescriptionPopup.popupY = screenY - 100; + LinkDescriptionPopup.descriptionPopup = true; + } + setTimeout(action(() => { TaskCompletionBox.taskCompleted = false; }), 2500); + } + }); + } + }; + + // Return web doc with the given uri, or create and create a new doc with the given uri + export const getSourceWebDocView = async (uri: string) => { + const currentDoc = SelectionManager.SelectedDocuments()[0].props.Document; + console.log(Cast(currentDoc.data, WebField)?.url.href === uri, uri, Cast(currentDoc.data, WebField)?.url.href); + if (Cast(currentDoc.data, WebField)?.url.href === uri) return currentDoc; // always check first whether the current doc is the source, only resort to 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 => console.log("web docs:", doc.title, Cast(doc.data, WebField)?.url.href)); + filteredDocs.forEach(doc => { uri === Cast(doc.data, WebField)?.url.href && results.push(doc); }); // TODO check history? imperfect matches? + })); + + results.forEach(doc => { + const docView = DocumentManager.Instance.getFirstDocumentView(doc); + if (docView) { + console.log(doc.title, StrCast(doc.data)); + return docView; + } + }); + + return undefined; + }; + + export const createInvisibleDoc = (uri: string) => { + const newDoc = Docs.Create.WebDocument(uri, { _nativeWidth: 0, _nativeHeight: 0, _width: 0, UseCors: true }); + }; + + export const scrollToAnnotation = (annotationId: string) => { + var success = false; + const onSuccess = () => { + console.log("scroll success!!"); + document.removeEventListener('scrollSuccess', onSuccess); + clearTimeout(interval); + success = true; + }; + + const interval = setInterval(() => { // keep trying to scroll every 200ms until annotations have loaded and scrolling is successful + console.log("send scroll"); + document.dispatchEvent(new CustomEvent('scrollToAnnotation', { + detail: annotationId, + bubbles: true + })); + }, 250); + + document.addEventListener('scrollSuccess', onSuccess); // listen for success message from client + setTimeout(() => !success && clearTimeout(interval), 10000); // give up if no success after 10s + }; +}
\ No newline at end of file diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index dbff6a46c..3ee7ed87a 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -927,7 +927,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 8023df8b4..674980b32 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -450,8 +450,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/SettingsManager.tsx b/src/client/util/SettingsManager.tsx index 6276fae96..d438ec971 100644 --- a/src/client/util/SettingsManager.tsx +++ b/src/client/util/SettingsManager.tsx @@ -12,7 +12,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"; @@ -93,10 +92,6 @@ export default class SettingsManager extends React.Component<{}> { googleAuthorize = (event: any) => { GoogleAuthenticationManager.Instance.fetchOrGenerateAccessToken(true); } - @action - hypothesisAuthorize = (event: any) => { - HypothesisAuthenticationManager.Instance.fetchAccessToken(true); - } @action togglePlaygroundMode = () => { @@ -122,7 +117,6 @@ export default class SettingsManager extends React.Component<{}> { <button onClick={this.noviceToggle} value="data">{`Set ${Doc.UserDoc().noviceMode ? "developer" : "novice"} mode`}</button> <button onClick={this.togglePlaygroundMode}>{`${this.playgroundMode ? "Disable" : "Enable"} playground mode`}</button> <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> <button onClick={() => window.location.assign(Utils.prepend("/logout"))}> {CurrentUserUtils.GuestWorkspace ? "Exit" : "Log Out"} diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts index c9f95a538..b4a518326 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"; @@ -106,7 +105,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 66545ea1f..fccfe325a 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -16,7 +16,6 @@ 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 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,6 +58,7 @@ import { TaskCompletionBox } from './nodes/TaskCompletedBox'; import { OverlayView } from './OverlayView'; import PDFMenu from './pdf/PDFMenu'; import { PreviewCursor } from './PreviewCursor'; +import { Hypothesis } from '../apis/hypothesis/HypothesisUtils'; import { undoBatch } from '../util/UndoManager'; @observer @@ -125,6 +125,7 @@ export class MainView extends React.Component { } }); }); + document.addEventListener("linkAnnotationToDash", Hypothesis.linkListener); } componentWillUnMount() { @@ -132,6 +133,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<{}>) { @@ -727,7 +729,6 @@ export class MainView extends React.Component { <SettingsManager /> <GroupManager /> <GoogleAuthenticationManager /> - <HypothesisAuthenticationManager /> <DocumentDecorations /> {/* {this.search} */} <CollectionMenu /> diff --git a/src/client/views/collections/CollectionLinearView.tsx b/src/client/views/collections/CollectionLinearView.tsx index a9e812ad3..19d1ffa7b 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" : ""} {DocumentLinksButton.StartLink.title} </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 3ebc6baca..5906282f1 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -351,8 +351,9 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?: title: uriList, _width: 400, _height: 315, - _nativeWidth: 600, - _nativeHeight: 472.5 + _nativeWidth: 850, + _nativeHeight: 962, + UseCors: true })); return; } diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx index a32c8b363..4334a339a 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 660afd4b9..fb014af51 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 6feb0a398..b29754d45 100644 --- a/src/client/views/linking/LinkMenuItem.tsx +++ b/src/client/views/linking/LinkMenuItem.tsx @@ -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 '../../apis/hypothesis/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); @@ -151,17 +154,27 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> { async 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)); } @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; @@ -234,7 +247,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/DocumentLinksButton.tsx b/src/client/views/nodes/DocumentLinksButton.tsx index 31efaaaa6..00477874b 100644 --- a/src/client/views/nodes/DocumentLinksButton.tsx +++ b/src/client/views/nodes/DocumentLinksButton.tsx @@ -2,18 +2,24 @@ 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 } 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 { 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 "../../apis/hypothesis/HypothesisUtils"; +import { Id } from "../../../fields/FieldSymbols"; import { TaskCompletionBox } from "./TaskCompletedBox"; import React = require("react"); +import './DocumentLinksButton.scss'; +import { WebField } from "../../../fields/URLField"; + const higflyout = require("@hig/flyout"); export const { anchorPoints } = higflyout; export const Flyout = higflyout.default; @@ -30,7 +36,9 @@ 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; @action @undoBatch onLinkButtonMoved = (e: PointerEvent) => { @@ -67,10 +75,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; @@ -82,10 +90,10 @@ 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) { + 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)); @@ -97,50 +105,80 @@ 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"); - 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; + 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 }, DocumentLinksButton.AnnotationId ? "hypothes.is annotation" : "long drag"); - setTimeout(action(() => TaskCompletionBox.taskCompleted = false), 2500); + // TODO: Not currently possible to drag to complete links to annotations + if (DocumentLinksButton.AnnotationId && DocumentLinksButton.AnnotationUri) { + Doc.GetProto(linkDoc as Doc).linksToAnnotation = true; + Doc.GetProto(linkDoc as Doc).annotationId = DocumentLinksButton.AnnotationId; + Doc.GetProto(linkDoc as Doc).annotationUri = DocumentLinksButton.AnnotationUri; + Hypothesis.makeLink(StrCast(targetDoc.title), Utils.prepend("/doc/" + targetDoc[Id]), DocumentLinksButton.AnnotationId); // update and link placeholder annotation } + + LinkManager.currentLink = linkDoc; + + 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) { + if (DocumentLinksButton.StartLink === this.props.View.props.Document) { 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"); - // 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); - LinkManager.currentLink = linkDoc; - if (linkDoc) { - TaskCompletionBox.textDisplayed = "Link Created"; - TaskCompletionBox.popupX = screenX; - TaskCompletionBox.popupY = screenY - 133; - TaskCompletionBox.taskCompleted = true; - - if (LinkDescriptionPopup.showDescriptions === "ON" || !LinkDescriptionPopup.showDescriptions) { - LinkDescriptionPopup.popupX = screenX; - LinkDescriptionPopup.popupY = screenY - 100; - LinkDescriptionPopup.descriptionPopup = true; + DocumentLinksButton.AnnotationId = undefined; + } else { + if (!this.props.StartLink && DocumentLinksButton.StartLink && DocumentLinksButton.StartLink !== this.props.View.props.Document) { + const linkDoc = DocUtils.MakeLink({ doc: DocumentLinksButton.StartLink }, { 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 + DocumentLinksButton.StartLink._link = this.props.View._link = linkDoc; + setTimeout(action(() => DocumentLinksButton.StartLink!._link = this.props.View._link = undefined), 0); + LinkManager.currentLink = linkDoc; + + if (DocumentLinksButton.AnnotationId && DocumentLinksButton.AnnotationUri) { // if linking from a Hypothes.is annotation + 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).annotationUri = DocumentLinksButton.AnnotationUri; + Hypothesis.makeLink(StrCast(targetDoc.title), Utils.prepend("/doc/" + targetDoc[Id]), DocumentLinksButton.AnnotationId); // edit annotation to add a Dash hyperlink to the linked doc } - setTimeout(action(() => TaskCompletionBox.taskCompleted = false), 2500); + if (linkDoc) { + TaskCompletionBox.textDisplayed = "Link Created"; + TaskCompletionBox.popupX = screenX; + TaskCompletionBox.popupY = screenY - 133; + TaskCompletionBox.taskCompleted = true; + + if (LinkDescriptionPopup.showDescriptions === "ON" || !LinkDescriptionPopup.showDescriptions) { + LinkDescriptionPopup.popupX = screenX; + LinkDescriptionPopup.popupY = screenY - 100; + LinkDescriptionPopup.descriptionPopup = true; + } + setTimeout(action(() => { TaskCompletionBox.taskCompleted = false; }), 2500); + } } } })); @@ -203,7 +241,7 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp </div> {this.props.InMenu && !this.props.StartLink && - DocumentLinksButton.StartLink !== this.props.View ? <div className={"documentLinksButton-endLink"} + DocumentLinksButton.StartLink !== this.props.View.props.Document ? <div className={"documentLinksButton-endLink"} style={{ width: this.props.InMenu ? "20px" : "30px", height: this.props.InMenu ? "20px" : "30px", backgroundColor: DocumentLinksButton.StartLink ? "" : "grey", @@ -211,11 +249,11 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp }} 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"} + {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>; + </div >; return (!links.length) && !this.props.AlwaysOn ? (null) : this.props.InMenu && (this.props.StartLink || DocumentLinksButton.StartLink) ? @@ -227,6 +265,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 4455d11eb..bd242db5e 100644 --- a/src/server/ApiManagers/UploadManager.ts +++ b/src/server/ApiManagers/UploadManager.ts @@ -26,7 +26,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..40fc5222f 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -72,7 +72,6 @@ function routeSetter({ isRelease, addSupervisedRoute, logRegistrationOutcome }: new DeleteManager(), new UtilManager(), new GeneralGoogleManager(), - new HypothesisManager(), new GooglePhotosManager(), ]; |