import { StrCast, Cast } from '../../fields/Types'; import { SearchUtil } from './SearchUtil'; import { action, runInAction } from 'mobx'; import { Doc, Opt } from '../../fields/Doc'; import { DocumentType } from '../documents/DocumentTypes'; import { Docs } from '../documents/Documents'; import { SelectionManager } from './SelectionManager'; import { WebField } from '../../fields/URLField'; import { DocumentManager } from './DocumentManager'; import { DocumentLinksButton } from '../views/nodes/DocumentLinksButton'; import { simulateMouseClick, Utils } from '../../Utils'; import { DocumentView } from '../views/nodes/DocumentView'; import { Id } from '../../fields/FieldSymbols'; export namespace Hypothesis { /** * Retrieve a WebDocument with the given url, prioritizing results that are on screen. * If none exist, create and return a new WebDocument. */ export const getSourceWebDoc = async (uri: string) => { const result = await findWebDoc(uri); console.log(result ? 'existing doc found' : 'existing doc NOT found'); return result || Docs.Create.WebDocument(uri, { title: uri, _nativeWidth: 850, _height: 512, _width: 400, useCors: true }); // create and return a new Web doc with given uri if no matching docs are found }; /** * Search for a WebDocument whose url field matches the given uri, return undefined if not found */ export const findWebDoc = async (uri: string) => { const currentDoc = SelectionManager.Docs().lastElement(); if (currentDoc && Cast(currentDoc.data, WebField)?.url.href === uri) return currentDoc; // always check first whether the currently selected doc is the annotation's source, only use Search otherwise const results: Doc[] = []; await SearchUtil.Search('web', true).then( action(async (res: SearchUtil.DocSearchResult) => { const docs = res.docs; const filteredDocs = docs.filter(doc => doc.author === Doc.CurrentUserEmail && doc.type === DocumentType.WEB && doc.data); filteredDocs.forEach(doc => { uri === Cast(doc.data, WebField)?.url.href && results.push(doc); // TODO check visited sites history? }); }) ); const onScreenResults = results.filter(doc => DocumentManager.Instance.getFirstDocumentView(doc)); return onScreenResults.length ? onScreenResults[0] : results.length ? results[0] : undefined; // prioritize results that are currently on the screen }; /** * listen for event from Hypothes.is plugin to link an annotation to Dash */ export const linkListener = async (e: any) => { const annotationId: string = e.detail.id; const annotationUri: string = StrCast(e.detail.uri).split('#annotations:')[0]; // clean hypothes.is URLs that reference a specific annotation const sourceDoc: Doc = await getSourceWebDoc(annotationUri); if (!DocumentLinksButton.StartLink || sourceDoc === DocumentLinksButton.StartLink) { // start new link if there were none already started, or if the old startLink came from the same web document (prevent links to itself) runInAction(() => { DocumentLinksButton.AnnotationId = annotationId; DocumentLinksButton.AnnotationUri = annotationUri; DocumentLinksButton.StartLink = sourceDoc; DocumentLinksButton.StartLinkView = undefined; }); } else { // if a link has already been started, complete the link to sourceDoc runInAction(() => { DocumentLinksButton.AnnotationId = annotationId; DocumentLinksButton.AnnotationUri = annotationUri; }); const endLinkView = DocumentManager.Instance.getFirstDocumentView(sourceDoc); const rect = document.body.getBoundingClientRect(); const x = rect.x + rect.width / 2; const y = 250; DocumentLinksButton.finishLinkClick(x, y, DocumentLinksButton.StartLink, sourceDoc, false, endLinkView); } }; /** * Send message to Hypothes.is client to edit an annotation to add a Dash hyperlink */ export const makeLink = async (title: string, url: string, annotationId: string, annotationSourceDoc: Doc) => { // if the annotation's source webpage isn't currently loaded in Dash, we're not able to access and edit the annotation from the client // so we're loading the webpage and its annotations invisibly in a WebBox in MainView.tsx, until the editing is done //!DocumentManager.Instance.getFirstDocumentView(annotationSourceDoc) && runInAction(() => DocumentLinksButton.invisibleWebDoc = annotationSourceDoc); var success = false; const onSuccess = action(() => { console.log('Edit success!!'); success = true; clearTimeout(interval); //DocumentLinksButton.invisibleWebDoc = undefined; document.removeEventListener('editSuccess', onSuccess); }); const newHyperlink = `[${title}\n](${url})`; const interval = setInterval( () => // keep trying to edit until annotations have loaded and editing is successful !success && document.dispatchEvent( new CustomEvent<{ newHyperlink: string; id: string }>('addLink', { detail: { newHyperlink: newHyperlink, id: annotationId }, bubbles: true, }) ), 300 ); setTimeout( action(() => { if (!success) { clearInterval(interval); //DocumentLinksButton.invisibleWebDoc = undefined; } }), 10000 ); // give up if no success after 10s document.addEventListener('editSuccess', onSuccess); }; /** * Send message Hypothes.is client request to edit an annotation to find and delete the target Dash hyperlink */ export const deleteLink = async (linkDoc: Doc, sourceDoc: Doc, destinationDoc: Doc) => { if (Cast(destinationDoc.data, WebField)?.url.href !== StrCast(linkDoc.annotationUri)) return; // check that the destinationDoc is a WebDocument containing the target annotation //!DocumentManager.Instance.getFirstDocumentView(destinationDoc) && runInAction(() => DocumentLinksButton.invisibleWebDoc = destinationDoc); // see note in makeLink var success = false; const onSuccess = action(() => { console.log('Edit success!'); success = true; clearTimeout(interval); // DocumentLinksButton.invisibleWebDoc = undefined; document.removeEventListener('editSuccess', onSuccess); }); const annotationId = StrCast(linkDoc.annotationId); const linkUrl = Doc.globalServerPath(sourceDoc); const interval = setInterval(() => { // keep trying to edit until annotations have loaded and editing is successful !success && document.dispatchEvent( new CustomEvent<{ targetUrl: string; id: string }>('deleteLink', { detail: { targetUrl: linkUrl, id: annotationId }, bubbles: true, }) ); }, 300); setTimeout( action(() => { if (!success) { clearInterval(interval); //DocumentLinksButton.invisibleWebDoc = undefined; } }), 10000 ); // give up if no success after 10s document.addEventListener('editSuccess', onSuccess); }; /** * Send message to Hypothes.is client to scroll to an annotation when it loads */ export const scrollToAnnotation = (annotationId: string, target: Doc) => { var success = false; const onSuccess = () => { console.log('Scroll success!!'); document.removeEventListener('scrollSuccess', onSuccess); clearInterval(interval); success = true; }; const interval = setInterval(() => { // keep trying to scroll every 250ms until annotations have loaded and scrolling is successful document.dispatchEvent( new CustomEvent('scrollToAnnotation', { detail: annotationId, bubbles: true, }) ); const targetView: Opt = DocumentManager.Instance.getFirstDocumentView(target); const position = targetView?.props.ScreenToLocalTransform().inverse().transformPoint(0, 0); targetView && position && simulateMouseClick(targetView.ContentDiv!, position[0], position[1], position[0], position[1], false); }, 300); document.addEventListener('scrollSuccess', onSuccess); // listen for success message from client setTimeout(() => !success && clearInterval(interval), 10000); // give up if no success after 10s }; }