diff options
-rw-r--r-- | src/client/apis/HypothesisAuthenticationManager.scss | 26 | ||||
-rw-r--r-- | src/client/apis/HypothesisAuthenticationManager.tsx | 158 | ||||
-rw-r--r-- | src/client/documents/Documents.ts | 41 | ||||
-rw-r--r-- | src/client/util/CurrentUserUtils.ts | 1 | ||||
-rw-r--r-- | src/client/views/GlobalKeyHandler.ts | 2 | ||||
-rw-r--r-- | src/client/views/MainView.tsx | 2 | ||||
-rw-r--r-- | src/client/views/collections/CollectionSubView.tsx | 43 | ||||
-rw-r--r-- | src/client/views/nodes/DocumentView.tsx | 2 | ||||
-rw-r--r-- | src/server/ApiManagers/HypothesisManager.ts | 44 | ||||
-rw-r--r-- | src/server/ApiManagers/UploadManager.ts | 3 | ||||
-rw-r--r-- | src/server/apis/google/GoogleApiServerUtils.ts | 3 | ||||
-rw-r--r-- | src/server/index.ts | 3 |
12 files changed, 285 insertions, 43 deletions
diff --git a/src/client/apis/HypothesisAuthenticationManager.scss b/src/client/apis/HypothesisAuthenticationManager.scss new file mode 100644 index 000000000..bd30dd94f --- /dev/null +++ b/src/client/apis/HypothesisAuthenticationManager.scss @@ -0,0 +1,26 @@ +.authorize-container { + display: flex; + flex-direction: column; + align-items: center; + + .paste-target { + padding: 5px; + width: 100%; + } + + .avatar { + border-radius: 50%; + } + + .welcome { + font-style: italic; + margin-top: 15px; + } + + .disconnect { + font-size: 10px; + margin-top: 20px; + color: red; + cursor: grab; + } +}
\ No newline at end of file diff --git a/src/client/apis/HypothesisAuthenticationManager.tsx b/src/client/apis/HypothesisAuthenticationManager.tsx new file mode 100644 index 000000000..cffb87227 --- /dev/null +++ b/src/client/apis/HypothesisAuthenticationManager.tsx @@ -0,0 +1,158 @@ +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) => { + let 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" }; + } + + render() { + return ( + <MainViewModal + isDisplayed={this.openState} + interactive={true} + contents={this.renderPrompt} + overlayDisplayedOpacity={0.9} + dialogueBoxStyle={this.dialogueBoxStyle} + /> + ); + } + +} + +Scripting.addGlobal("HypothesisAuthenticationManager", HypothesisAuthenticationManager);
\ No newline at end of file diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index f81c25bab..3355c0091 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -829,6 +829,47 @@ export namespace Docs { } export namespace DocUtils { + export function FilterDocs(docs: Doc[], docFilters: string[], docRangeFilters: string[], viewSpecScript?: ScriptField) { + const childDocs = viewSpecScript ? docs.filter(d => viewSpecScript.script.run({ doc: d }, console.log).result) : docs; + + const filterFacets: { [key: string]: { [value: string]: string } } = {}; // maps each filter key to an object with value=>modifier fields + for (let i = 0; i < docFilters.length; i += 3) { + const [key, value, modifiers] = docFilters.slice(i, i + 3); + if (!filterFacets[key]) { + filterFacets[key] = {}; + } + filterFacets[key][value] = modifiers; + } + + const filteredDocs = docFilters.length ? childDocs.filter(d => { + for (const facetKey of Object.keys(filterFacets)) { + const facet = filterFacets[facetKey]; + const satisfiesFacet = Object.keys(facet).some(value => { + if (facet[value] === "match") { + return d[facetKey] === undefined || Field.toString(d[facetKey] as Field).includes(value); + } + return (facet[value] === "x") !== Doc.matchFieldValue(d, facetKey, value); + }); + if (!satisfiesFacet) { + return false; + } + } + return true; + }) : childDocs; + const rangeFilteredDocs = filteredDocs.filter(d => { + for (let i = 0; i < docRangeFilters.length; i += 3) { + const key = docRangeFilters[i]; + const min = Number(docRangeFilters[i + 1]); + const max = Number(docRangeFilters[i + 2]); + const val = Cast(d[key], "number", null); + if (val !== undefined && (val < min || val > max)) { + return false; + } + } + return true; + }); + return rangeFilteredDocs; + } export function Publish(promoteDoc: Doc, targetID: string, addDoc: any, remDoc: any) { targetID = targetID.replace(/^-/, "").replace(/\([0-9]*\)$/, ""); diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index 4276e04e4..9f04aab04 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -424,6 +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: "houzz", click: 'HypothesisAuthenticationManager.Instance.fetchOrGenerateAccessToken(true)' }, ]; } diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts index 34f666f62..a3a023164 100644 --- a/src/client/views/GlobalKeyHandler.ts +++ b/src/client/views/GlobalKeyHandler.ts @@ -7,6 +7,7 @@ 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"; @@ -104,6 +105,7 @@ export default class KeyManager { DictationManager.Controls.stop(); // RecommendationsBox.Instance.closeMenu(); GoogleAuthenticationManager.Instance.cancel(); + HypothesisAuthenticationManager.Instance.cancel(); SharingManager.Instance.close(); break; case "delete": diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index 15f818d1f..eba9bb344 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -61,6 +61,7 @@ import { LinkMenu } from './linking/LinkMenu'; import { LinkDocPreview } from './nodes/LinkDocPreview'; import { Fade } from '@material-ui/core'; import { LinkCreatedBox } from './nodes/LinkCreatedBox'; +import HypothesisAuthenticationManager from '../apis/HypothesisAuthenticationManager'; @observer export class MainView extends React.Component { @@ -601,6 +602,7 @@ export class MainView extends React.Component { <SettingsManager /> <GroupManager /> <GoogleAuthenticationManager /> + <HypothesisAuthenticationManager /> <DocumentDecorations /> <GestureOverlay> <RichTextMenu key="rich" /> diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index ed8535ecb..ce6872695 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -106,16 +106,6 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?: [...this.props.docFilters(), ...Cast(this.props.Document._docFilters, listSpec("string"), [])]; } @computed get childDocs() { - const docFilters = this.docFilters(); - const docRangeFilters = this.props.ignoreFields?.includes("_docRangeFilters") ? [] : Cast(this.props.Document._docRangeFilters, listSpec("string"), []); - const filterFacets: { [key: string]: { [value: string]: string } } = {}; // maps each filter key to an object with value=>modifier fields - for (let i = 0; i < docFilters.length; i += 3) { - const [key, value, modifiers] = docFilters.slice(i, i + 3); - if (!filterFacets[key]) { - filterFacets[key] = {}; - } - filterFacets[key][value] = modifiers; - } let rawdocs: (Doc | Promise<Doc>)[] = []; if (this.dataField instanceof Doc) { // if collection data is just a document, then promote it to a singleton list; @@ -128,38 +118,13 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?: const rootDoc = Cast(this.props.Document.rootDocument, Doc, null); rawdocs = rootDoc && !this.props.annotationsKey ? [Doc.GetProto(rootDoc)] : []; } + const docs = rawdocs.filter(d => !(d instanceof Promise)).map(d => d as Doc); + const docFilters = this.docFilters(); const viewSpecScript = Cast(this.props.Document.viewSpecScript, ScriptField); - const childDocs = viewSpecScript ? docs.filter(d => viewSpecScript.script.run({ doc: d }, console.log).result) : docs; + const docRangeFilters = this.props.ignoreFields?.includes("_docRangeFilters") ? [] : Cast(this.props.Document._docRangeFilters, listSpec("string"), []); - const filteredDocs = docFilters.length && !this.props.dontRegisterView ? childDocs.filter(d => { - for (const facetKey of Object.keys(filterFacets)) { - const facet = filterFacets[facetKey]; - const satisfiesFacet = Object.keys(facet).some(value => { - if (facet[value] === "match") { - return d[facetKey] === undefined || Field.toString(d[facetKey] as Field).includes(value); - } - return (facet[value] === "x") !== Doc.matchFieldValue(d, facetKey, value); - }); - if (!satisfiesFacet) { - return false; - } - } - return true; - }) : childDocs; - const rangeFilteredDocs = filteredDocs.filter(d => { - for (let i = 0; i < docRangeFilters.length; i += 3) { - const key = docRangeFilters[i]; - const min = Number(docRangeFilters[i + 1]); - const max = Number(docRangeFilters[i + 2]); - const val = Cast(d[key], "number", null); - if (val !== undefined && (val < min || val > max)) { - return false; - } - } - return true; - }); - return rangeFilteredDocs; + return this.props.Document.dontRegisterView ? docs : DocUtils.FilterDocs(docs, docFilters, docRangeFilters, viewSpecScript); } @action diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index b38db9a1e..3311bfa33 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -1094,7 +1094,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu return (this.props.treeViewDoc && this.props.LayoutTemplateString) || // render nothing for: tree view anchor dots this.layoutDoc.presBox || // presentationbox nodes this.props.dontRegisterView ? (null) : // view that are not registered - DocListCast(this.Document.links).filter(d => !d.hidden && this.isNonTemporalLink).map((d, i) => + DocUtils.FilterDocs(DocListCast(this.Document.links), this.props.docFilters(), []).filter(d => !d.hidden && this.isNonTemporalLink).map((d, i) => <DocumentView {...this.props} key={i + 1} Document={d} ContainingCollectionView={this.props.ContainingCollectionView} diff --git a/src/server/ApiManagers/HypothesisManager.ts b/src/server/ApiManagers/HypothesisManager.ts new file mode 100644 index 000000000..33badbc42 --- /dev/null +++ b/src/server/ApiManagers/HypothesisManager.ts @@ -0,0 +1,44 @@ +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 fe39b84e6..55ceab9fb 100644 --- a/src/server/ApiManagers/UploadManager.ts +++ b/src/server/ApiManagers/UploadManager.ts @@ -24,7 +24,8 @@ export enum Directory { pdfs = "pdfs", text = "text", pdf_thumbnails = "pdf_thumbnails", - audio = "audio" + 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 20f96f432..b0157a85f 100644 --- a/src/server/apis/google/GoogleApiServerUtils.ts +++ b/src/server/apis/google/GoogleApiServerUtils.ts @@ -39,7 +39,8 @@ export namespace GoogleApiServerUtils { */ export enum Service { Documents = "Documents", - Slides = "Slides" + Slides = "Slides", + Hypothesis = "Hypothesis" } /** diff --git a/src/server/index.ts b/src/server/index.ts index 083173bb5..9af4b00bc 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -18,10 +18,10 @@ import PDFManager from "./ApiManagers/PDFManager"; import UploadManager from "./ApiManagers/UploadManager"; import { log_execution } from "./ActionUtilities"; import GeneralGoogleManager from "./ApiManagers/GeneralGoogleManager"; +import HypothesisManager from "./ApiManagers/HypothesisManager"; import GooglePhotosManager from "./ApiManagers/GooglePhotosManager"; import { Logger } from "./ProcessFactory"; import { yellow } from "colors"; -import { DashSessionAgent } from "./DashSession/DashSessionAgent"; import SessionManager from "./ApiManagers/SessionManager"; import { AppliedSessionAgent } from "./DashSession/Session/agents/applied_session_agent"; @@ -72,6 +72,7 @@ function routeSetter({ isRelease, addSupervisedRoute, logRegistrationOutcome }: new DeleteManager(), new UtilManager(), new GeneralGoogleManager(), + new HypothesisManager(), new GooglePhotosManager(), ]; |