From 8264ac5484c0c7b5d47cb78ce60f5d6568e736d9 Mon Sep 17 00:00:00 2001
From: anika-ahluwalia <anika.ahluwalia@gmail.com>
Date: Sat, 4 Jul 2020 17:25:10 -0500
Subject: redesigned editing menu, added link descriptions and popup on created

---
 src/client/views/MainView.tsx | 2 ++
 1 file changed, 2 insertions(+)

(limited to 'src/client/views/MainView.tsx')

diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 15f818d1f..68ea51456 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 { LinkDescriptionPopup } from './nodes/LinkDescriptionPopup';
 
 @observer
 export class MainView extends React.Component {
@@ -608,6 +609,7 @@ export class MainView extends React.Component {
             </GestureOverlay>
             <PreviewCursor />
             <LinkCreatedBox />
+            {LinkDescriptionPopup.descriptionPopup ? <LinkDescriptionPopup /> : null}
             {DocumentLinksButton.EditLink ? <LinkMenu location={DocumentLinksButton.EditLinkLoc} docView={DocumentLinksButton.EditLink} addDocTab={DocumentLinksButton.EditLink.props.addDocTab} changeFlyout={emptyFunction} /> : (null)}
             {LinkDocPreview.LinkInfo ? <LinkDocPreview location={LinkDocPreview.LinkInfo.Location} backgroundColor={this.defaultBackgroundColors}
                 linkDoc={LinkDocPreview.LinkInfo.linkDoc} linkSrc={LinkDocPreview.LinkInfo.linkSrc} href={LinkDocPreview.LinkInfo.href}
-- 
cgit v1.2.3-70-g09d2


From 0438137cd435c47ce334b15a4ad00cbd70d80662 Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Tue, 7 Jul 2020 00:23:24 -0400
Subject: made docFilters apply to links.  added hypothesis api token input

---
 .../apis/HypothesisAuthenticationManager.scss      |  26 ++++
 .../apis/HypothesisAuthenticationManager.tsx       | 158 +++++++++++++++++++++
 src/client/documents/Documents.ts                  |  41 ++++++
 src/client/util/CurrentUserUtils.ts                |   1 +
 src/client/views/GlobalKeyHandler.ts               |   2 +
 src/client/views/MainView.tsx                      |   2 +
 src/client/views/collections/CollectionSubView.tsx |  43 +-----
 src/client/views/nodes/DocumentView.tsx            |   2 +-
 src/server/ApiManagers/HypothesisManager.ts        |  44 ++++++
 src/server/ApiManagers/UploadManager.ts            |   3 +-
 src/server/apis/google/GoogleApiServerUtils.ts     |   3 +-
 src/server/index.ts                                |   3 +-
 12 files changed, 285 insertions(+), 43 deletions(-)
 create mode 100644 src/client/apis/HypothesisAuthenticationManager.scss
 create mode 100644 src/client/apis/HypothesisAuthenticationManager.tsx
 create mode 100644 src/server/ApiManagers/HypothesisManager.ts

(limited to 'src/client/views/MainView.tsx')

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(),
     ];
 
-- 
cgit v1.2.3-70-g09d2


From 92aaf350e7b1baebd681eaeafa2a11c104f0708a Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Tue, 7 Jul 2020 10:32:14 -0400
Subject: fixed hypothesis icon

---
 src/client/util/CurrentUserUtils.ts | 2 +-
 src/client/views/MainView.tsx       | 6 ++++--
 2 files changed, 5 insertions(+), 3 deletions(-)

(limited to 'src/client/views/MainView.tsx')

diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index 9f04aab04..8099228c6 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: "houzz", click: 'HypothesisAuthenticationManager.Instance.fetchOrGenerateAccessToken(true)' },
+            { title: "Connect a Hypothesis Account", label: "Hypothesis Account", icon: "heading", click: 'HypothesisAuthenticationManager.Instance.fetchOrGenerateAccessToken(true)' },
         ];
 
     }
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 584ed2f1c..81195b550 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -5,7 +5,8 @@ import {
     faQuestionCircle, faArrowLeft, faArrowRight, faArrowDown, faArrowUp, faBolt, faBullseye, faCaretUp, faCat, faCheck, faChevronRight, faClipboard, faClone, faCloudUploadAlt,
     faCommentAlt, faCompressArrowsAlt, faCut, faEllipsisV, faEraser, faExclamation, faFileAlt, faFileAudio, faFilePdf, faFilm, faFilter, faFont, faGlobeAsia, faHighlighter,
     faLongArrowAltRight, faMicrophone, faMousePointer, faMusic, faObjectGroup, faPause, faPen, faPenNib, faPhone, faPlay, faPortrait, faRedoAlt, faStamp, faStickyNote, faTimesCircle,
-    faThumbtack, faTree, faTv, faUndoAlt, faVideo, faAsterisk, faBrain, faImage, faPaintBrush, faTimes, faEye, faArrowsAlt, faQuoteLeft, faSortAmountDown, faAlignLeft, faAlignCenter, faAlignRight
+    faThumbtack, faTree, faTv, faUndoAlt, faVideo, faAsterisk, faBrain, faImage, faPaintBrush, faTimes, faEye, faArrowsAlt, faQuoteLeft, faSortAmountDown, faAlignLeft, faAlignCenter, faAlignRight,
+    faHeading
 } from '@fortawesome/free-solid-svg-icons';
 import { ANTIMODEMENU_HEIGHT } from './globalCssVariables.scss';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
@@ -145,7 +146,8 @@ export class MainView extends React.Component {
             faQuestionCircle, faArrowLeft, faArrowRight, faArrowDown, faArrowUp, faBolt, faBullseye, faCaretUp, faCat, faCheck, faChevronRight, faClipboard, faClone, faCloudUploadAlt,
             faCommentAlt, faCompressArrowsAlt, faCut, faEllipsisV, faEraser, faExclamation, faFileAlt, faFileAudio, faFilePdf, faFilm, faFilter, faFont, faGlobeAsia, faHighlighter,
             faLongArrowAltRight, faMicrophone, faMousePointer, faMusic, faObjectGroup, faPause, faPen, faPenNib, faPhone, faPlay, faPortrait, faRedoAlt, faStamp, faStickyNote, faTrashAlt, faAngleRight, faBell,
-            faThumbtack, faTree, faTv, faUndoAlt, faVideo, faAsterisk, faBrain, faImage, faPaintBrush, faTimes, faEye, faArrowsAlt, faQuoteLeft, faSortAmountDown, faAlignLeft, faAlignCenter, faAlignRight);
+            faThumbtack, faTree, faTv, faUndoAlt, faVideo, faAsterisk, faBrain, faImage, faPaintBrush, faTimes, faEye, faArrowsAlt, faQuoteLeft, faSortAmountDown, faAlignLeft, faAlignCenter, faAlignRight,
+            faHeading);
         this.initEventListeners();
         this.initAuthenticationRouters();
     }
-- 
cgit v1.2.3-70-g09d2

