From 1a73ed731ea17c46ac7823577143047097927326 Mon Sep 17 00:00:00 2001
From: Melissa Zhang <mzhang19096@gmail.com>
Date: Thu, 9 Jul 2020 00:00:28 -0700
Subject: check for valid API key, then display extracted hypothes.is username

---
 src/client/apis/HypothesisAuthenticationManager.tsx | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

(limited to 'src/client/apis/HypothesisAuthenticationManager.tsx')

diff --git a/src/client/apis/HypothesisAuthenticationManager.tsx b/src/client/apis/HypothesisAuthenticationManager.tsx
index cffb87227..b299f233e 100644
--- a/src/client/apis/HypothesisAuthenticationManager.tsx
+++ b/src/client/apis/HypothesisAuthenticationManager.tsx
@@ -6,6 +6,7 @@ import { Opt } from "../../fields/Doc";
 import { Networking } from "../Network";
 import "./HypothesisAuthenticationManager.scss";
 import { Scripting } from "../util/Scripting";
+import { Hypothesis } from "./hypothesis/HypothesisApiUtils";
 
 const prompt = "Paste authorization code here...";
 
@@ -44,12 +45,13 @@ export default class HypothesisAuthenticationManager extends React.Component<{}>
                 this.disposer = reaction(
                     () => this.authenticationCode,
                     async authenticationCode => {
-                        if (authenticationCode) {
+                        const userProfile = authenticationCode && await Hypothesis.fetchUser(authenticationCode);
+                        if (userProfile && userProfile.userid !== null) {
                             this.disposer?.();
                             Networking.PostToServer("/writeHypothesisAccessToken", { authenticationCode });
                             runInAction(() => {
                                 this.success = true;
-                                this.credentials = response;
+                                this.credentials = Hypothesis.extractUsername(userProfile.userid); // extract username from profile
                             });
                             this.resetState();
                             resolve(authenticationCode);
-- 
cgit v1.2.3-70-g09d2


From 8594b7e8f3058a8d441413a033aee311ee59bdfd Mon Sep 17 00:00:00 2001
From: Melissa Zhang <mzhang19096@gmail.com>
Date: Fri, 10 Jul 2020 16:26:08 -0700
Subject: hypothes.is authentication is fully functional, no longer need to
 manually input username/api key

---
 src/client/apis/HypothesisAuthenticationManager.tsx | 20 +++++++++++---------
 src/client/apis/hypothesis/HypothesisApiUtils.ts    | 21 ++++++++++++---------
 src/client/views/nodes/DocumentLinksButton.tsx      |  2 +-
 src/server/ApiManagers/HypothesisManager.ts         |  4 ++--
 src/server/database.ts                              |  5 +++--
 5 files changed, 29 insertions(+), 23 deletions(-)

(limited to 'src/client/apis/HypothesisAuthenticationManager.tsx')

diff --git a/src/client/apis/HypothesisAuthenticationManager.tsx b/src/client/apis/HypothesisAuthenticationManager.tsx
index b299f233e..f995dee3b 100644
--- a/src/client/apis/HypothesisAuthenticationManager.tsx
+++ b/src/client/apis/HypothesisAuthenticationManager.tsx
@@ -19,7 +19,7 @@ export default class HypothesisAuthenticationManager extends React.Component<{}>
     @observable private showPasteTargetState = false;
     @observable private success: Opt<boolean> = undefined;
     @observable private displayLauncher = true;
-    @observable private credentials: string;
+    @observable private credentials: { username: string, apiKey: string } | undefined;
     private disposer: Opt<IReactionDisposer>;
 
     private set isOpen(value: boolean) {
@@ -35,9 +35,10 @@ export default class HypothesisAuthenticationManager extends React.Component<{}>
     }
 
     public fetchAccessToken = async (displayIfFound = false) => {
-        let response: any = await Networking.FetchFromServer("/readHypothesisAccessToken");
+        const jsonResponse = await Networking.FetchFromServer("/readHypothesisAccessToken");
+        const response = jsonResponse !== "" ? JSON.parse(jsonResponse) : undefined;
         // if this is an authentication url, activate the UI to register the new access token
-        if (!response) { // new RegExp(AuthenticationUrl).test(response)) {
+        if (!response) {
             this.isOpen = true;
             this.authenticationLink = response;
             return new Promise<string>(async resolve => {
@@ -48,10 +49,11 @@ export default class HypothesisAuthenticationManager extends React.Component<{}>
                         const userProfile = authenticationCode && await Hypothesis.fetchUser(authenticationCode);
                         if (userProfile && userProfile.userid !== null) {
                             this.disposer?.();
-                            Networking.PostToServer("/writeHypothesisAccessToken", { authenticationCode });
+                            const hypothesisUsername = Hypothesis.extractUsername(userProfile.userid); // extract username from profile
+                            Networking.PostToServer("/writeHypothesisAccessToken", { authenticationCode, hypothesisUsername });
                             runInAction(() => {
                                 this.success = true;
-                                this.credentials = Hypothesis.extractUsername(userProfile.userid); // extract username from profile
+                                this.credentials = response;
                             });
                             this.resetState();
                             resolve(authenticationCode);
@@ -69,7 +71,7 @@ export default class HypothesisAuthenticationManager extends React.Component<{}>
             this.resetState(-1, -1);
             this.isOpen = true;
         }
-        return response.access_token;
+        return response;
     }
 
     resetState = action((visibleForMS: number = 3000, fadesOutInMS: number = 500) => {
@@ -78,7 +80,7 @@ export default class HypothesisAuthenticationManager extends React.Component<{}>
                 this.isOpen = false;
                 this.success = undefined;
                 this.displayLauncher = true;
-                this.credentials = "";
+                this.credentials = undefined;
                 this.shouldShowPasteTarget = false;
                 this.authenticationCode = undefined;
             });
@@ -93,7 +95,7 @@ export default class HypothesisAuthenticationManager extends React.Component<{}>
                 setTimeout(action(() => {
                     this.success = undefined;
                     this.displayLauncher = true;
-                    this.credentials = "";
+                    this.credentials = undefined;
                 }), fadesOutInMS);
             }), visibleForMS);
         }
@@ -124,7 +126,7 @@ export default class HypothesisAuthenticationManager extends React.Component<{}>
                     <>
                         <span
                             className={'welcome'}
-                        >Welcome to Dash, {this.credentials}
+                        >Welcome to Dash, {this.credentials.username}
                         </span>
                         <div
                             className={'disconnect'}
diff --git a/src/client/apis/hypothesis/HypothesisApiUtils.ts b/src/client/apis/hypothesis/HypothesisApiUtils.ts
index 9ba481c8c..83881cd89 100644
--- a/src/client/apis/hypothesis/HypothesisApiUtils.ts
+++ b/src/client/apis/hypothesis/HypothesisApiUtils.ts
@@ -1,6 +1,10 @@
 import { StrCast } from "../../../fields/Types";
+import HypothesisAuthenticationManager from "../HypothesisAuthenticationManager";
 
 export namespace Hypothesis {
+
+    const getCredentials = async () => HypothesisAuthenticationManager.Instance.fetchAccessToken();
+
     export const fetchAnnotation = async (annotationId: string) => {
         const response = await fetch(`https://api.hypothes.is/api/annotations/${annotationId}`);
         if (response.ok) {
@@ -11,12 +15,12 @@ export namespace Hypothesis {
     };
 
     /**
-     * Searches for annotations made by @param username that 
-     * contain @param searchKeyWord 
+     * Searches for annotations authored by the current user that contain @param searchKeyWord 
      */
-    export const searchAnnotation = async (username: string, searchKeyWord: string) => {
+    export const searchAnnotation = async (searchKeyWord: string) => {
+        const credentials = await getCredentials();
         const base = 'https://api.hypothes.is/api/search';
-        const request = base + `?user=acct:${username}@hypothes.is&text=${searchKeyWord}`;
+        const request = base + `?user=acct:${credentials.username}@hypothes.is&text=${searchKeyWord}`;
         console.log("DASH Querying " + request);
         const response = await fetch(request);
         if (response.ok) {
@@ -40,8 +44,8 @@ export namespace Hypothesis {
     };
 
     // Find the most recent placeholder annotation created, and return its ID
-    export const getPlaceholderId = async (username: string, searchKeyWord: string) => {
-        const getResponse = await Hypothesis.searchAnnotation(username, searchKeyWord);
+    export const getPlaceholderId = async (searchKeyWord: string) => {
+        const getResponse = await Hypothesis.searchAnnotation(searchKeyWord);
         const id = getResponse.rows.length > 0 ? getResponse.rows[0].id : undefined;
         const uri = getResponse.rows.length > 0 ? getResponse.rows[0].uri : undefined;
         return id ? { id, uri } : undefined;
@@ -49,8 +53,7 @@ export namespace Hypothesis {
 
     // Send request to Hypothes.is client to modify a placeholder annotation into a hyperlink to Dash
     export const dispatchLinkRequest = async (title: string, url: string, annotationId: string) => {
-        const apiKey = "6879-DnMTKjWjnnLPa0Php7f5Ra2kunZ_X0tMRDbTF220_q0";
-
+        const credentials = await getCredentials();
         const oldAnnotation = await fetchAnnotation(annotationId);
         const oldText = StrCast(oldAnnotation.text);
         const newHyperlink = `[${title}\n](${url})`;
@@ -58,7 +61,7 @@ export namespace Hypothesis {
 
         console.log("DASH dispatching linkRequest");
         document.dispatchEvent(new CustomEvent<{ newText: string, id: string, apiKey: string }>("linkRequest", {
-            detail: { newText: newText, id: annotationId, apiKey: apiKey },
+            detail: { newText: newText, id: annotationId, apiKey: credentials.apiKey },
             bubbles: true
         }));
     };
diff --git a/src/client/views/nodes/DocumentLinksButton.tsx b/src/client/views/nodes/DocumentLinksButton.tsx
index 535711193..ab97e8531 100644
--- a/src/client/views/nodes/DocumentLinksButton.tsx
+++ b/src/client/views/nodes/DocumentLinksButton.tsx
@@ -41,7 +41,7 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp
         window.addEventListener("message", async (e: any) => {
             if (e.origin === "http://localhost:1050" && e.data.message === "annotation created") {
                 console.log("DASH RECEIVED MESSAGE:", e.data.message);
-                const response = await Hypothesis.getPlaceholderId("melissaz", "placeholder"); // delete once eventListening between client & Dash works
+                const response = await Hypothesis.getPlaceholderId("placeholder"); // delete once eventListening between client & Dash works
                 const source = SelectionManager.SelectedDocuments()[0];
                 response && runInAction(() => {
                     DocumentLinksButton.AnnotationId = response.id;
diff --git a/src/server/ApiManagers/HypothesisManager.ts b/src/server/ApiManagers/HypothesisManager.ts
index 73c707a55..370d02a49 100644
--- a/src/server/ApiManagers/HypothesisManager.ts
+++ b/src/server/ApiManagers/HypothesisManager.ts
@@ -14,7 +14,7 @@ export default class HypothesisManager extends ApiManager {
             subscription: "/readHypothesisAccessToken",
             secureHandler: async ({ user, res }) => {
                 const credentials = await Database.Auxiliary.HypothesisAccessToken.Fetch(user.id);
-                res.send(credentials?.hypothesisApiKey ?? "");
+                res.send(credentials ? { username: credentials.hypothesisUsername, apiKey: credentials.hypothesisApiKey } : "");
             }
         });
 
@@ -22,7 +22,7 @@ export default class HypothesisManager extends ApiManager {
             method: Method.POST,
             subscription: "/writeHypothesisAccessToken",
             secureHandler: async ({ user, req, res }) => {
-                await Database.Auxiliary.HypothesisAccessToken.Write(user.id, req.body.authenticationCode);
+                await Database.Auxiliary.HypothesisAccessToken.Write(user.id, req.body.authenticationCode, req.body.hypothesisUsername);
                 res.send();
             }
         });
diff --git a/src/server/database.ts b/src/server/database.ts
index 767d38350..456c1c254 100644
--- a/src/server/database.ts
+++ b/src/server/database.ts
@@ -413,6 +413,7 @@ export namespace Database {
             interface StoredCredentials {
                 userId: string;
                 hypothesisApiKey: string;
+                hypothesisUsername: string;
                 _id?: string;
             }
 
@@ -420,8 +421,8 @@ export namespace Database {
              * Writes the @param hypothesisApiKey to the database, associated
              * with @param userId for later retrieval and updating. 
              */
-            export const Write = async (userId: string, hypothesisApiKey: string) => {
-                return Instance.insert({ userId, hypothesisApiKey }, AuxiliaryCollections.HypothesisAccess);
+            export const Write = async (userId: string, hypothesisApiKey: string, hypothesisUsername: string) => {
+                return Instance.insert({ userId, hypothesisApiKey, hypothesisUsername }, AuxiliaryCollections.HypothesisAccess);
             };
 
             /**
-- 
cgit v1.2.3-70-g09d2


From eb6e1b6705560d6fe94cb5787839b9138ab4b979 Mon Sep 17 00:00:00 2001
From: Melissa Zhang <mzhang19096@gmail.com>
Date: Mon, 13 Jul 2020 12:14:56 -0700
Subject: fixed authentication UI

---
 src/client/apis/HypothesisAuthenticationManager.tsx | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

(limited to 'src/client/apis/HypothesisAuthenticationManager.tsx')

diff --git a/src/client/apis/HypothesisAuthenticationManager.tsx b/src/client/apis/HypothesisAuthenticationManager.tsx
index f995dee3b..9a6d78bac 100644
--- a/src/client/apis/HypothesisAuthenticationManager.tsx
+++ b/src/client/apis/HypothesisAuthenticationManager.tsx
@@ -53,7 +53,7 @@ export default class HypothesisAuthenticationManager extends React.Component<{}>
                             Networking.PostToServer("/writeHypothesisAccessToken", { authenticationCode, hypothesisUsername });
                             runInAction(() => {
                                 this.success = true;
-                                this.credentials = response;
+                                this.credentials = { username: hypothesisUsername, apiKey: authenticationCode! };
                             });
                             this.resetState();
                             resolve(authenticationCode);
-- 
cgit v1.2.3-70-g09d2


From fabb86b78874a38619d821b315eaf9ce204e1afe Mon Sep 17 00:00:00 2001
From: Melissa Zhang <mzhang19096@gmail.com>
Date: Tue, 28 Jul 2020 21:27:38 -0700
Subject: "handle editing annotations in client, bypass authorization issues"

---
 .../apis/HypothesisAuthenticationManager.tsx       |   2 +-
 src/client/apis/hypothesis/HypothesisApiUtils.ts   | 116 ---------------------
 src/client/apis/hypothesis/HypothesisUtils.ts      |  77 ++++++++++++++
 src/client/documents/Documents.ts                  |   1 -
 src/client/views/MainView.tsx                      |   2 +-
 src/client/views/linking/LinkMenuItem.tsx          |   2 +-
 src/client/views/nodes/DocumentLinksButton.tsx     |   2 +-
 src/client/views/nodes/DocumentView.tsx            |   1 -
 8 files changed, 81 insertions(+), 122 deletions(-)
 delete mode 100644 src/client/apis/hypothesis/HypothesisApiUtils.ts
 create mode 100644 src/client/apis/hypothesis/HypothesisUtils.ts

(limited to 'src/client/apis/HypothesisAuthenticationManager.tsx')

diff --git a/src/client/apis/HypothesisAuthenticationManager.tsx b/src/client/apis/HypothesisAuthenticationManager.tsx
index d4a81b3eb..653f21a7a 100644
--- a/src/client/apis/HypothesisAuthenticationManager.tsx
+++ b/src/client/apis/HypothesisAuthenticationManager.tsx
@@ -6,7 +6,7 @@ import { Opt } from "../../fields/Doc";
 import { Networking } from "../Network";
 import "./HypothesisAuthenticationManager.scss";
 import { Scripting } from "../util/Scripting";
-import { Hypothesis } from "./hypothesis/HypothesisApiUtils";
+import { Hypothesis } from "./hypothesis/HypothesisUtils";
 
 const prompt = "Paste authorization code here...";
 
diff --git a/src/client/apis/hypothesis/HypothesisApiUtils.ts b/src/client/apis/hypothesis/HypothesisApiUtils.ts
deleted file mode 100644
index e2fb91af9..000000000
--- a/src/client/apis/hypothesis/HypothesisApiUtils.ts
+++ /dev/null
@@ -1,116 +0,0 @@
-import { StrCast, Cast } from "../../../fields/Types";
-import HypothesisAuthenticationManager from "../HypothesisAuthenticationManager";
-import { SearchUtil } from "../../util/SearchUtil";
-import { action } from "mobx";
-import { Doc } from "../../../fields/Doc";
-import { DocumentType } from "../../documents/DocumentTypes";
-import { WebField } from "../../../fields/URLField";
-import { DocumentManager } from "../../util/DocumentManager";
-
-export namespace Hypothesis {
-
-    const getCredentials = async () => HypothesisAuthenticationManager.Instance.fetchAccessToken();
-
-    export const fetchAnnotation = async (annotationId: string) => {
-        const response = await fetch(`https://api.hypothes.is/api/annotations/${annotationId}`);
-        if (response.ok) {
-            return response.json();
-        } else {
-            throw new Error('DASH: Error in fetchAnnotation GET request');
-        }
-    };
-
-    /**
-     * Searches for annotations authored by the current user that contain @param searchKeyWord 
-     */
-    export const searchAnnotation = async (searchKeyWord: string) => {
-        const credentials = await getCredentials();
-        const base = 'https://api.hypothes.is/api/search';
-        const request = base + `?user=acct:${credentials.username}@hypothes.is&text=${searchKeyWord}`;
-        console.log("DASH Querying " + request);
-        const response = await fetch(request);
-        if (response.ok) {
-            return response.json();
-        } else {
-            throw new Error('DASH: Error in searchAnnotation GET request');
-        }
-    };
-
-    export const fetchUser = async (apiKey: string) => {
-        const response = await fetch('https://api.hypothes.is/api/profile', {
-            headers: {
-                'Authorization': `Bearer ${apiKey}`,
-            },
-        });
-        if (response.ok) {
-            return response.json();
-        } else {
-            throw new Error('DASH: Error in fetchUser GET request');
-        }
-    };
-
-    export const editAnnotation = async (annotationId: string, newText: string) => {
-        console.log("DASH dispatching editAnnotation");
-        const credentials = await getCredentials();
-        document.dispatchEvent(new CustomEvent<{ newText: string, id: string, apiKey: string }>("editAnnotation", {
-            detail: { newText: newText, id: annotationId, apiKey: credentials.apiKey },
-            bubbles: true
-        }));
-    };
-
-    /**
-     * Edit an annotation with ID @param annotationId to add a hyperlink to a Dash document, which needs to be 
-     * written in the format [@param title](@param url)
-     */
-    export const makeLink = async (title: string, url: string, annotationId: string) => {
-        const oldAnnotation = await fetchAnnotation(annotationId);
-        const oldText = StrCast(oldAnnotation.text);
-        const newHyperlink = `[${title}\n](${url})`;
-        const newText = oldText === "placeholder" ? newHyperlink : oldText + '\n\n' + newHyperlink; // if this is not the first link in the annotation, add link on new line
-        await editAnnotation(annotationId, newText);
-    };
-
-    export const deleteLink = async (annotationId: string, linkUrl: string) => {
-        const annotation = await fetchAnnotation(annotationId);
-        const regex = new RegExp(`\\[[^\\]]*\\]\\(${linkUrl}\\)`); // finds the link (written in [title](hyperlink) format) to be deleted
-        const out = annotation.text.replace(regex, "");
-        editAnnotation(annotationId, out);
-    };
-
-    // Construct an URL which will scroll the web page to a specific annotation's position
-    export const makeAnnotationUrl = (annotationId: string, baseUrl: string) => {
-        return `https://hyp.is/${annotationId}/${baseUrl}`; // embeds the generic version of Hypothes.is client, not the Dash version
-        // return baseUrl + '#annotations:' + annotationId;
-    };
-
-    // Extract username from Hypothe.is's userId format
-    export const extractUsername = (userid: string) => {
-        const regex = new RegExp('(?<=\:)(.*?)(?=\@)/');
-        return regex.exec(userid)![0];
-    };
-
-    // Return corres
-    export const getSourceWebDoc = async (uri: string) => {
-        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.type === DocumentType.WEB && doc.data
-            );
-            filteredDocs.forEach(doc => {
-                console.log(Cast(doc.data, WebField)?.url.href);
-                if (uri === Cast(doc.data, WebField)?.url.href) results.push(doc); // TODO check history? imperfect matches?
-            });
-        }));
-
-        // TODO: open & return new Web doc with given uri if no matching Web docs are found
-        return results.length ? DocumentManager.Instance.getFirstDocumentView(results[0]) : undefined;
-    };
-
-    export const scrollToAnnotation = (annotationId: string) => {
-        document.dispatchEvent(new CustomEvent("scrollToAnnotation", {
-            detail: annotationId,
-            bubbles: true
-        }));
-    };
-}
\ 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..e15620a91
--- /dev/null
+++ b/src/client/apis/hypothesis/HypothesisUtils.ts
@@ -0,0 +1,77 @@
+import { StrCast, Cast } from "../../../fields/Types";
+import HypothesisAuthenticationManager from "../HypothesisAuthenticationManager";
+import { SearchUtil } from "../../util/SearchUtil";
+import { action } from "mobx";
+import { Doc } from "../../../fields/Doc";
+import { DocumentType } from "../../documents/DocumentTypes";
+import { WebField } from "../../../fields/URLField";
+import { DocumentManager } from "../../util/DocumentManager";
+
+export namespace Hypothesis {
+    export const fetchUser = async (apiKey: string) => {
+        const response = await fetch('https://api.hypothes.is/api/profile', {
+            headers: {
+                'Authorization': `Bearer ${apiKey}`,
+            },
+        });
+        if (response.ok) {
+            return response.json();
+        } else {
+            throw new Error('DASH: Error in fetchUser GET request');
+        }
+    };
+
+    // Send Hypothes.is client request to edit an annotation to add a Dash hyperlink
+    export const makeLink = async (title: string, url: string, annotationId: string) => {
+        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
+        }));
+    };
+
+    // Construct an URL which will scroll the web page to a specific annotation's position
+    export const makeAnnotationUrl = (annotationId: string, baseUrl: string) => {
+        return `https://hyp.is/${annotationId}/${baseUrl}`; // embeds the generic version of Hypothes.is client, not the Dash version
+        // return baseUrl + '#annotations:' + annotationId;
+    };
+
+    // Extract username from Hypothe.is's userId format
+    export const extractUsername = (userid: string) => {
+        const regex = new RegExp('(?<=\:)(.*?)(?=\@)/');
+        return regex.exec(userid)![0];
+    };
+
+    // Return corres
+    export const getSourceWebDoc = async (uri: string) => {
+        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.type === DocumentType.WEB && doc.data
+            );
+            filteredDocs.forEach(doc => {
+                console.log(Cast(doc.data, WebField)?.url.href);
+                if (uri === Cast(doc.data, WebField)?.url.href) results.push(doc); // TODO check history? imperfect matches?
+            });
+        }));
+
+        // TODO: open & return new Web doc with given uri if no matching Web docs are found
+        return results.length ? DocumentManager.Instance.getFirstDocumentView(results[0]) : undefined;
+    };
+
+    export const scrollToAnnotation = (annotationId: string) => {
+        document.dispatchEvent(new CustomEvent("scrollToAnnotation", {
+            detail: annotationId,
+            bubbles: true
+        }));
+    };
+}
\ No newline at end of file
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index f1d4ec46c..529a25bd9 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -50,7 +50,6 @@ import { DashWebRTCVideo } from "../views/webcam/DashWebRTCVideo";
 import { DocumentType } from "./DocumentTypes";
 import { Networking } from "../Network";
 import { Upload } from "../../server/SharedMediaTypes";
-import { Hypothesis } from "../apis/hypothesis/HypothesisApiUtils";
 const path = require('path');
 
 export interface DocumentOptions {
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 64538d015..5bf9ac2a3 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -66,7 +66,7 @@ import { LinkDescriptionPopup } from './nodes/LinkDescriptionPopup';
 import FormatShapePane from "./collections/collectionFreeForm/FormatShapePane";
 import HypothesisAuthenticationManager from '../apis/HypothesisAuthenticationManager';
 import CollectionMenu from './collections/CollectionMenu';
-import { Hypothesis } from '../apis/hypothesis/HypothesisApiUtils';
+import { Hypothesis } from '../apis/hypothesis/HypothesisUtils';
 import { SelectionManager } from '../util/SelectionManager';
 
 @observer
diff --git a/src/client/views/linking/LinkMenuItem.tsx b/src/client/views/linking/LinkMenuItem.tsx
index 8f7a2481d..475133010 100644
--- a/src/client/views/linking/LinkMenuItem.tsx
+++ b/src/client/views/linking/LinkMenuItem.tsx
@@ -15,7 +15,7 @@ import { setupMoveUpEvents, emptyFunction, Utils } from '../../../Utils';
 import { DocumentView } from '../nodes/DocumentView';
 import { DocumentLinksButton } from '../nodes/DocumentLinksButton';
 import { LinkDocPreview } from '../nodes/LinkDocPreview';
-import { Hypothesis } from '../../apis/hypothesis/HypothesisApiUtils';
+import { Hypothesis } from '../../apis/hypothesis/HypothesisUtils';
 import { Id } from '../../../fields/FieldSymbols';
 import { Tooltip } from '@material-ui/core';
 import { DocumentType } from '../../documents/DocumentTypes';
diff --git a/src/client/views/nodes/DocumentLinksButton.tsx b/src/client/views/nodes/DocumentLinksButton.tsx
index 8b4dc2957..e68a85664 100644
--- a/src/client/views/nodes/DocumentLinksButton.tsx
+++ b/src/client/views/nodes/DocumentLinksButton.tsx
@@ -17,7 +17,7 @@ import { StrCast } from "../../../fields/Types";
 
 import { LinkDescriptionPopup } from "./LinkDescriptionPopup";
 import { LinkManager } from "../../util/LinkManager";
-import { Hypothesis } from "../../apis/hypothesis/HypothesisApiUtils";
+import { Hypothesis } from "../../apis/hypothesis/HypothesisUtils";
 import { Id } from "../../../fields/FieldSymbols";
 import { Tooltip } from "@material-ui/core";
 const higflyout = require("@hig/flyout");
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 555e58bb0..998c6798e 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -42,7 +42,6 @@ import React = require("react");
 import { DocumentLinksButton } from './DocumentLinksButton';
 import { MobileInterface } from '../../../mobile/MobileInterface';
 import { LinkCreatedBox } from './LinkCreatedBox';
-import { Hypothesis } from '../../apis/hypothesis/HypothesisApiUtils';
 import { LinkDescriptionPopup } from './LinkDescriptionPopup';
 import { LinkManager } from '../../util/LinkManager';
 
-- 
cgit v1.2.3-70-g09d2


From ba8549220295d03fb7eb7d7d31c90af72b30b1a6 Mon Sep 17 00:00:00 2001
From: Melissa Zhang <mzhang19096@gmail.com>
Date: Thu, 30 Jul 2020 10:38:57 -0700
Subject: remove unnecessary authentication/database stuff

---
 .../apis/HypothesisAuthenticationManager.tsx       | 164 ---------------------
 src/client/apis/hypothesis/HypothesisUtils.ts      |  18 +--
 src/client/util/CurrentUserUtils.ts                |   1 -
 src/client/util/SettingsManager.tsx                |   6 -
 src/client/views/GlobalKeyHandler.ts               |   2 -
 src/client/views/linking/LinkMenuItem.tsx          |   2 +-
 src/server/ApiManagers/HypothesisManager.ts        |  40 -----
 src/server/ApiManagers/UploadManager.ts            |   1 -
 src/server/apis/google/GoogleApiServerUtils.ts     |   1 -
 src/server/database.ts                             |  38 -----
 src/server/index.ts                                |   2 -
 11 files changed, 4 insertions(+), 271 deletions(-)
 delete mode 100644 src/client/apis/HypothesisAuthenticationManager.tsx
 delete mode 100644 src/server/ApiManagers/HypothesisManager.ts

(limited to 'src/client/apis/HypothesisAuthenticationManager.tsx')

diff --git a/src/client/apis/HypothesisAuthenticationManager.tsx b/src/client/apis/HypothesisAuthenticationManager.tsx
deleted file mode 100644
index 653f21a7a..000000000
--- a/src/client/apis/HypothesisAuthenticationManager.tsx
+++ /dev/null
@@ -1,164 +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";
-import { Hypothesis } from "./hypothesis/HypothesisUtils";
-
-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: { username: string, apiKey: string } | undefined;
-    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 jsonResponse = await Networking.FetchFromServer("/readHypothesisAccessToken");
-        const response = jsonResponse !== "" ? JSON.parse(jsonResponse) : undefined;
-        // if this is an authentication url, activate the UI to register the new access token
-        if (!response) {
-            this.isOpen = true;
-            this.authenticationLink = response;
-            return new Promise<string>(async resolve => {
-                this.disposer?.();
-                this.disposer = reaction(
-                    () => this.authenticationCode,
-                    async authenticationCode => {
-                        const userProfile = authenticationCode && await Hypothesis.fetchUser(authenticationCode);
-                        if (userProfile && userProfile.userid !== null) {
-                            this.disposer?.();
-                            const hypothesisUsername = Hypothesis.extractUsername(userProfile.userid); // extract username from profile
-                            Networking.PostToServer("/writeHypothesisAccessToken", { authenticationCode, hypothesisUsername });
-                            runInAction(() => {
-                                this.success = true;
-                                this.credentials = { username: hypothesisUsername, apiKey: authenticationCode! };
-                            });
-                            this.resetState();
-                            resolve(authenticationCode);
-                        }
-                    }
-                );
-            });
-        }
-
-        if (displayIfFound) {
-            runInAction(() => {
-                this.success = true;
-                this.credentials = response;
-            });
-            this.resetState(-1, -1);
-            this.isOpen = true;
-        }
-        return response;
-    }
-
-    resetState = action((visibleForMS: number = 3000, fadesOutInMS: number = 500) => {
-        if (!visibleForMS && !fadesOutInMS) {
-            runInAction(() => {
-                this.isOpen = false;
-                this.success = undefined;
-                this.displayLauncher = true;
-                this.credentials = undefined;
-                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 = undefined;
-                }), 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.username}
-                        </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
index 8eaad2905..a9d807976 100644
--- a/src/client/apis/hypothesis/HypothesisUtils.ts
+++ b/src/client/apis/hypothesis/HypothesisUtils.ts
@@ -1,5 +1,4 @@
 import { StrCast, Cast } from "../../../fields/Types";
-import HypothesisAuthenticationManager from "../HypothesisAuthenticationManager";
 import { SearchUtil } from "../../util/SearchUtil";
 import { action } from "mobx";
 import { Doc } from "../../../fields/Doc";
@@ -8,18 +7,6 @@ import { WebField } from "../../../fields/URLField";
 import { DocumentManager } from "../../util/DocumentManager";
 
 export namespace Hypothesis {
-    export const fetchUser = async (apiKey: string) => {
-        const response = await fetch('https://api.hypothes.is/api/profile', {
-            headers: {
-                'Authorization': `Bearer ${apiKey}`,
-            },
-        });
-        if (response.ok) {
-            return response.json();
-        } else {
-            throw new Error('DASH: Error in fetchUser GET request');
-        }
-    };
 
     // Send Hypothes.is client request to edit an annotation to add a Dash hyperlink
     export const makeLink = async (title: string, url: string, annotationId: string) => {
@@ -40,6 +27,7 @@ export namespace Hypothesis {
 
     // Construct an URL which will automatically scroll the web page to a specific annotation's position
     export const makeAnnotationUrl = (annotationId: string, baseUrl: string) => {
+        console.log("baseUrl", baseUrl, annotationId);
         return `${baseUrl}#annotations:${annotationId}`;
     };
 
@@ -58,8 +46,8 @@ export namespace Hypothesis {
                 doc.type === DocumentType.WEB && doc.data
             );
             filteredDocs.forEach(doc => {
-                console.log(Cast(doc.data, WebField)?.url.href);
-                if (uri === Cast(doc.data, WebField)?.url.href) results.push(doc); // TODO check history? imperfect matches?
+                console.log(uri, Cast(doc.data, WebField)?.url.href, uri === Cast(doc.data, WebField)?.url.href);
+                (uri === Cast(doc.data, WebField)?.url.href) && results.push(doc); // TODO check history? imperfect matches?
             });
         }));
 
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index ff33d35e0..1fe611b12 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -441,7 +441,6 @@ export class CurrentUserUtils {
             { 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)' },
-            { toolTip: "Connect a Hypothesis Account", title: "Hypothesis Account", icon: "heading", click: 'HypothesisAuthenticationManager.Instance.fetchAccessToken(true)' },
         ];
 
     }
diff --git a/src/client/util/SettingsManager.tsx b/src/client/util/SettingsManager.tsx
index 90d59aa51..a9c2d5e15 100644
--- a/src/client/util/SettingsManager.tsx
+++ b/src/client/util/SettingsManager.tsx
@@ -12,7 +12,6 @@ import { CurrentUserUtils } from "./CurrentUserUtils";
 import { Utils } from "../../Utils";
 import { Doc } from "../../fields/Doc";
 import GroupManager from "./GroupManager";
-import HypothesisAuthenticationManager from "../apis/HypothesisAuthenticationManager";
 import GoogleAuthenticationManager from "../apis/GoogleAuthenticationManager";
 import { togglePlaygroundMode } from "../../fields/util";
 
@@ -92,10 +91,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 = () => {
@@ -118,7 +113,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 086085db5..e1232e6f8 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";
@@ -107,7 +106,6 @@ export default class KeyManager {
                 DictationManager.Controls.stop();
                 // RecommendationsBox.Instance.closeMenu();
                 GoogleAuthenticationManager.Instance.cancel();
-                HypothesisAuthenticationManager.Instance.cancel();
                 SharingManager.Instance.close();
                 GroupManager.Instance.close();
                 CollectionFreeFormViewChrome.Instance.clearKeep();
diff --git a/src/client/views/linking/LinkMenuItem.tsx b/src/client/views/linking/LinkMenuItem.tsx
index 079e130ea..8503bcbeb 100644
--- a/src/client/views/linking/LinkMenuItem.tsx
+++ b/src/client/views/linking/LinkMenuItem.tsx
@@ -156,7 +156,7 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> {
         const linkDoc = this.props.linkDoc;
 
         if (linkDoc.followLinkLocation === "openExternal" && this.props.destinationDoc.type === DocumentType.WEB) {
-            window.open(Hypothesis.makeAnnotationUrl(StrCast(this.props.linkDoc.annotationId), '_blank'));
+            window.open(Hypothesis.makeAnnotationUrl(StrCast(linkDoc.annotationId), StrCast(linkDoc.annotationUri)), '_blank');
             return;
         }
 
diff --git a/src/server/ApiManagers/HypothesisManager.ts b/src/server/ApiManagers/HypothesisManager.ts
deleted file mode 100644
index 370d02a49..000000000
--- a/src/server/ApiManagers/HypothesisManager.ts
+++ /dev/null
@@ -1,40 +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 }) => {
-                const credentials = await Database.Auxiliary.HypothesisAccessToken.Fetch(user.id);
-                res.send(credentials ? { username: credentials.hypothesisUsername, apiKey: credentials.hypothesisApiKey } : "");
-            }
-        });
-
-        register({
-            method: Method.POST,
-            subscription: "/writeHypothesisAccessToken",
-            secureHandler: async ({ user, req, res }) => {
-                await Database.Auxiliary.HypothesisAccessToken.Write(user.id, req.body.authenticationCode, req.body.hypothesisUsername);
-                res.send();
-            }
-        });
-
-        register({
-            method: Method.GET,
-            subscription: "/revokeHypothesisAccessToken",
-            secureHandler: async ({ user, res }) => {
-                await Database.Auxiliary.HypothesisAccessToken.Revoke(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 456c1c254..64c5b7e4b 100644
--- a/src/server/database.ts
+++ b/src/server/database.ts
@@ -305,7 +305,6 @@ export namespace Database {
         export enum AuxiliaryCollections {
             GooglePhotosUploadHistory = "uploadedFromGooglePhotos",
             GoogleAccess = "googleAuthentication",
-            HypothesisAccess = "hypothesisAuthentication"
         }
 
         /**
@@ -406,43 +405,6 @@ export namespace Database {
 
         }
 
-        export namespace HypothesisAccessToken {
-            /**
-             * Format stored in database.
-             */
-            interface StoredCredentials {
-                userId: string;
-                hypothesisApiKey: string;
-                hypothesisUsername: string;
-                _id?: string;
-            }
-
-            /**
-             * Writes the @param hypothesisApiKey to the database, associated
-             * with @param userId for later retrieval and updating. 
-             */
-            export const Write = async (userId: string, hypothesisApiKey: string, hypothesisUsername: string) => {
-                return Instance.insert({ userId, hypothesisApiKey, hypothesisUsername }, AuxiliaryCollections.HypothesisAccess);
-            };
-
-            /**
-             * Retrieves the credentials associaed with @param userId
-             * and optionally removes their database id according to @param removeId. 
-             */
-            export const Fetch = async (userId: string, removeId = true): Promise<Opt<StoredCredentials>> => {
-                return SanitizedSingletonQuery<StoredCredentials>({ userId }, AuxiliaryCollections.HypothesisAccess, removeId);
-            };
-
-            /**
-            * Revokes the credentials associated with @param userId. 
-            */
-            export const Revoke = async (userId: string) => {
-                const entry = await Fetch(userId, false);
-                if (entry) {
-                    Instance.delete({ _id: entry._id }, AuxiliaryCollections.HypothesisAccess);
-                }
-            };
-        }
     }
 
 }
diff --git a/src/server/index.ts b/src/server/index.ts
index 9af4b00bc..5c313654c 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -18,7 +18,6 @@ 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";
@@ -72,7 +71,6 @@ function routeSetter({ isRelease, addSupervisedRoute, logRegistrationOutcome }:
         new DeleteManager(),
         new UtilManager(),
         new GeneralGoogleManager(),
-        new HypothesisManager(),
         new GooglePhotosManager(),
     ];
 
-- 
cgit v1.2.3-70-g09d2

