aboutsummaryrefslogtreecommitdiff
path: root/src/client/apis
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/apis')
-rw-r--r--src/client/apis/GoogleAuthenticationManager.tsx119
-rw-r--r--src/client/apis/google_docs/GoogleApiClientUtils.ts164
-rw-r--r--src/client/apis/google_docs/GooglePhotosClientUtils.ts91
-rw-r--r--src/client/apis/gpt/GPT.ts18
-rw-r--r--src/client/apis/youtube/YoutubeBox.scss126
-rw-r--r--src/client/apis/youtube/YoutubeBox.tsx369
6 files changed, 196 insertions, 691 deletions
diff --git a/src/client/apis/GoogleAuthenticationManager.tsx b/src/client/apis/GoogleAuthenticationManager.tsx
index 855f48f7e..5269f763b 100644
--- a/src/client/apis/GoogleAuthenticationManager.tsx
+++ b/src/client/apis/GoogleAuthenticationManager.tsx
@@ -1,14 +1,14 @@
-import { action, IReactionDisposer, observable, reaction, runInAction } from "mobx";
-import { observer } from "mobx-react";
-import * as React from "react";
-import { Opt } from "../../fields/Doc";
-import { Networking } from "../Network";
-import { ScriptingGlobals } from "../util/ScriptingGlobals";
-import { MainViewModal } from "../views/MainViewModal";
-import "./GoogleAuthenticationManager.scss";
+import { action, IReactionDisposer, observable, reaction, runInAction } from 'mobx';
+import { observer } from 'mobx-react';
+import * as React from 'react';
+import { Opt } from '../../fields/Doc';
+import { Networking } from '../Network';
+import { ScriptingGlobals } from '../util/ScriptingGlobals';
+import { MainViewModal } from '../views/MainViewModal';
+import './GoogleAuthenticationManager.scss';
-const AuthenticationUrl = "https://accounts.google.com/o/oauth2/v2/auth";
-const prompt = "Paste authorization code here...";
+const AuthenticationUrl = 'https://accounts.google.com/o/oauth2/v2/auth';
+const prompt = 'Paste authorization code here...';
@observer
export class GoogleAuthenticationManager extends React.Component<{}> {
@@ -23,11 +23,11 @@ export class GoogleAuthenticationManager extends React.Component<{}> {
private disposer: Opt<IReactionDisposer>;
private set isOpen(value: boolean) {
- runInAction(() => this.openState = value);
+ runInAction(() => (this.openState = value));
}
private set shouldShowPasteTarget(value: boolean) {
- runInAction(() => this.showPasteTargetState = value);
+ runInAction(() => (this.showPasteTargetState = value));
}
public cancel() {
@@ -35,7 +35,7 @@ export class GoogleAuthenticationManager extends React.Component<{}> {
}
public fetchOrGenerateAccessToken = async (displayIfFound = false) => {
- let response: any = await Networking.FetchFromServer("/readGoogleAccessToken");
+ let response: any = await Networking.FetchFromServer('/readGoogleAccessToken');
// if this is an authentication url, activate the UI to register the new access token
if (new RegExp(AuthenticationUrl).test(response)) {
this.isOpen = true;
@@ -47,7 +47,7 @@ export class GoogleAuthenticationManager extends React.Component<{}> {
async authenticationCode => {
if (authenticationCode && /\d{1}\/[\w-]{55}/.test(authenticationCode)) {
this.disposer?.();
- const response = await Networking.PostToServer("/writeGoogleAccessToken", { authenticationCode });
+ const response = await Networking.PostToServer('/writeGoogleAccessToken', { authenticationCode });
runInAction(() => {
this.success = true;
this.credentials = response;
@@ -71,7 +71,7 @@ export class GoogleAuthenticationManager extends React.Component<{}> {
this.isOpen = true;
}
return response.access_token;
- }
+ };
resetState = action((visibleForMS: number = 3000, fadesOutInMS: number = 500) => {
if (!visibleForMS && !fadesOutInMS) {
@@ -89,14 +89,20 @@ export class GoogleAuthenticationManager extends React.Component<{}> {
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);
+ setTimeout(
+ action(() => {
+ this.isOpen = false;
+ setTimeout(
+ action(() => {
+ this.success = undefined;
+ this.displayLauncher = true;
+ this.credentials = undefined;
+ }),
+ fadesOutInMS
+ );
+ }),
+ visibleForMS
+ );
}
});
@@ -108,61 +114,44 @@ export class GoogleAuthenticationManager extends React.Component<{}> {
private get renderPrompt() {
return (
<div className={'authorize-container'}>
-
- {this.displayLauncher ? <button
- className={"dispatch"}
- onClick={() => {
- window.open(this.authenticationLink);
- setTimeout(() => this.shouldShowPasteTarget = true, 500);
- }}
- style={{ marginBottom: this.showPasteTargetState ? 15 : 0 }}
- >Authorize a Google account...</button> : (null)}
- {this.showPasteTargetState ? <input
- className={'paste-target'}
- onChange={action(e => this.authenticationCode = e.currentTarget.value)}
- placeholder={prompt}
- /> : (null)}
- {this.credentials ?
+ {this.displayLauncher ? (
+ <button
+ className={'dispatch'}
+ onClick={() => {
+ window.open(this.authenticationLink);
+ setTimeout(() => (this.shouldShowPasteTarget = true), 500);
+ }}
+ style={{ marginBottom: this.showPasteTargetState ? 15 : 0 }}>
+ Authorize a Google account...
+ </button>
+ ) : null}
+ {this.showPasteTargetState ? <input className={'paste-target'} onChange={action(e => (this.authenticationCode = e.currentTarget.value))} placeholder={prompt} /> : null}
+ {this.credentials ? (
<>
- <img
- className={'avatar'}
- src={this.credentials.userInfo.picture}
- />
- <span
- className={'welcome'}
- >Welcome to Dash, {this.credentials.userInfo.name}
- </span>
+ <img className={'avatar'} src={this.credentials.userInfo.picture} />
+ <span className={'welcome'}>Welcome to Dash, {this.credentials.userInfo.name}</span>
<div
className={'disconnect'}
onClick={async () => {
- await Networking.FetchFromServer("/revokeGoogleAccessToken");
+ await Networking.FetchFromServer('/revokeGoogleAccessToken');
this.resetState(0, 0);
- }}
- >Disconnect Account</div>
- </> : (null)}
+ }}>
+ 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 };
+ 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)}
- />
- );
+ return <MainViewModal isDisplayed={this.openState} interactive={true} contents={this.renderPrompt} dialogueBoxStyle={this.dialogueBoxStyle} overlayStyle={{ zIndex: 1001 }} closeOnExternalClick={action(() => (this.isOpen = false))} />;
}
-
}
-ScriptingGlobals.add("GoogleAuthenticationManager", GoogleAuthenticationManager); \ No newline at end of file
+ScriptingGlobals.add('GoogleAuthenticationManager', GoogleAuthenticationManager);
diff --git a/src/client/apis/google_docs/GoogleApiClientUtils.ts b/src/client/apis/google_docs/GoogleApiClientUtils.ts
index c8f381cc0..0b303eacf 100644
--- a/src/client/apis/google_docs/GoogleApiClientUtils.ts
+++ b/src/client/apis/google_docs/GoogleApiClientUtils.ts
@@ -1,45 +1,46 @@
-import { docs_v1 } from "googleapis";
-import { Opt } from "../../../fields/Doc";
-import { isArray } from "util";
-import { EditorState } from "prosemirror-state";
-import { Networking } from "../../Network";
+/* eslint-disable no-restricted-syntax */
+/* eslint-disable no-use-before-define */
+import { docs_v1 as docsV1 } from 'googleapis';
+// eslint-disable-next-line node/no-deprecated-api
+import { isArray } from 'util';
+import { EditorState } from 'prosemirror-state';
+import { Opt } from '../../../fields/Doc';
+import { Networking } from '../../Network';
-export const Pulls = "googleDocsPullCount";
-export const Pushes = "googleDocsPushCount";
+export const Pulls = 'googleDocsPullCount';
+export const Pushes = 'googleDocsPushCount';
export namespace GoogleApiClientUtils {
-
export enum Actions {
- Create = "create",
- Retrieve = "retrieve",
- Update = "update"
+ Create = 'create',
+ Retrieve = 'retrieve',
+ Update = 'update',
}
export namespace Docs {
-
- export type RetrievalResult = Opt<docs_v1.Schema$Document>;
- export type UpdateResult = Opt<docs_v1.Schema$BatchUpdateDocumentResponse>;
+ export type RetrievalResult = Opt<docsV1.Schema$Document>;
+ export type UpdateResult = Opt<docsV1.Schema$BatchUpdateDocumentResponse>;
export interface UpdateOptions {
documentId: DocumentId;
- requests: docs_v1.Schema$Request[];
+ requests: docsV1.Schema$Request[];
}
export enum WriteMode {
Insert,
- Replace
+ Replace,
}
export type DocumentId = string;
export type Reference = DocumentId | CreateOptions;
export interface Content {
text: string | string[];
- requests: docs_v1.Schema$Request[];
+ requests: docsV1.Schema$Request[];
}
export type IdHandler = (id: DocumentId) => any;
export type CreationResult = Opt<DocumentId>;
- export type ReadLinesResult = Opt<{ title?: string, bodyLines?: string[] }>;
- export type ReadResult = { title: string, body: string };
+ export type ReadLinesResult = Opt<{ title?: string; bodyLines?: string[] }>;
+ export type ReadResult = { title: string; body: string };
export interface ImportResult {
title: string;
text: string;
@@ -67,23 +68,23 @@ export namespace GoogleApiClientUtils {
}
/**
- * After following the authentication routine, which connects this API call to the current signed in account
- * and grants the appropriate permissions, this function programmatically creates an arbitrary Google Doc which
- * should appear in the user's Google Doc library instantaneously.
- *
- * @param options the title to assign to the new document, and the information necessary
- * to store the new documentId returned from the creation process
- * @returns the documentId of the newly generated document, or undefined if the creation process fails.
- */
+ * After following the authentication routine, which connects this API call to the current signed in account
+ * and grants the appropriate permissions, this function programmatically creates an arbitrary Google Doc which
+ * should appear in the user's Google Doc library instantaneously.
+ *
+ * @param options the title to assign to the new document, and the information necessary
+ * to store the new documentId returned from the creation process
+ * @returns the documentId of the newly generated document, or undefined if the creation process fails.
+ */
export const create = async (options: CreateOptions): Promise<CreationResult> => {
const path = `/googleDocs/Documents/${Actions.Create}`;
const parameters = {
requestBody: {
- title: options.title || `Dash Export (${new Date().toDateString()})`
- }
+ title: options.title || `Dash Export (${new Date().toDateString()})`,
+ },
};
try {
- const schema: docs_v1.Schema$Document = await Networking.PostToServer(path, parameters);
+ const schema: docsV1.Schema$Document = await Networking.PostToServer(path, parameters);
return schema.documentId === null ? undefined : schema.documentId;
} catch {
return undefined;
@@ -91,19 +92,25 @@ export namespace GoogleApiClientUtils {
};
export namespace Utils {
-
- export type ExtractResult = { text: string, paragraphs: DeconstructedParagraph[] };
- export const extractText = (document: docs_v1.Schema$Document, removeNewlines = false): ExtractResult => {
+ export type ExtractResult = { text: string; paragraphs: DeconstructedParagraph[] };
+ export const extractText = (document: docsV1.Schema$Document, removeNewlines = false): ExtractResult => {
const paragraphs = extractParagraphs(document);
- let text = paragraphs.map(paragraph => paragraph.contents.filter(content => !("inlineObjectId" in content)).map(run => (run as docs_v1.Schema$TextRun).content).join("")).join("");
+ let text = paragraphs
+ .map(paragraph =>
+ paragraph.contents
+ .filter(content => !('inlineObjectId' in content))
+ .map(run => (run as docsV1.Schema$TextRun).content)
+ .join('')
+ )
+ .join('');
text = text.substring(0, text.length - 1);
- removeNewlines && text.replace(/\n/g, "");
+ removeNewlines && text.replace(/\n/g, '');
return { text, paragraphs };
};
- export type ContentArray = (docs_v1.Schema$TextRun | docs_v1.Schema$InlineObjectElement)[];
- export type DeconstructedParagraph = { contents: ContentArray, bullet: Opt<number> };
- const extractParagraphs = (document: docs_v1.Schema$Document, filterEmpty = true): DeconstructedParagraph[] => {
+ export type ContentArray = (docsV1.Schema$TextRun | docsV1.Schema$InlineObjectElement)[];
+ export type DeconstructedParagraph = { contents: ContentArray; bullet: Opt<number> };
+ const extractParagraphs = (document: docsV1.Schema$Document, filterEmpty = true): DeconstructedParagraph[] => {
const fragments: DeconstructedParagraph[] = [];
if (document.body && document.body.content) {
for (const element of document.body.content) {
@@ -132,7 +139,7 @@ export namespace GoogleApiClientUtils {
return fragments;
};
- export const endOf = (schema: docs_v1.Schema$Document): number | undefined => {
+ export const endOf = (schema: docsV1.Schema$Document): number | undefined => {
if (schema.body && schema.body.content) {
const paragraphs = schema.body.content.filter(el => el.paragraph);
if (paragraphs.length) {
@@ -146,10 +153,10 @@ export namespace GoogleApiClientUtils {
}
}
}
+ return undefined;
};
- export const initialize = async (reference: Reference) => typeof reference === "string" ? reference : create(reference);
-
+ export const initialize = async (reference: Reference) => (typeof reference === 'string' ? reference : create(reference));
}
export const retrieve = async (options: RetrieveOptions): Promise<RetrievalResult> => {
@@ -168,8 +175,8 @@ export namespace GoogleApiClientUtils {
const parameters = {
documentId: options.documentId,
requestBody: {
- requests: options.requests
- }
+ requests: options.requests,
+ },
};
try {
const replies: UpdateResult = await Networking.PostToServer(path, parameters);
@@ -179,83 +186,84 @@ export namespace GoogleApiClientUtils {
}
};
- export const read = async (options: ReadOptions): Promise<Opt<ReadResult>> => {
- return retrieve({ documentId: options.documentId }).then(document => {
+ export const read = async (options: ReadOptions): Promise<Opt<ReadResult>> =>
+ retrieve({ documentId: options.documentId }).then(document => {
if (document) {
const title = document.title!;
const body = Utils.extractText(document, options.removeNewlines).text;
return { title, body };
}
+ return undefined;
});
- };
- export const readLines = async (options: ReadOptions): Promise<Opt<ReadLinesResult>> => {
- return retrieve({ documentId: options.documentId }).then(document => {
+ export const readLines = async (options: ReadOptions): Promise<Opt<ReadLinesResult>> =>
+ retrieve({ documentId: options.documentId }).then(document => {
if (document) {
- const title = document.title;
- let bodyLines = Utils.extractText(document).text.split("\n");
+ const { title } = document;
+ let bodyLines = Utils.extractText(document).text.split('\n');
options.removeNewlines && (bodyLines = bodyLines.filter(line => line.length));
- return { title: title ?? "", bodyLines };
+ return { title: title ?? '', bodyLines };
}
+ return undefined;
});
- };
export const setStyle = async (options: UpdateOptions) => {
const replies: any = await update({
documentId: options.documentId,
- requests: options.requests
+ requests: options.requests,
});
- if ("errors" in replies) {
- console.log("Write operation failed:");
+ if ('errors' in replies) {
+ console.log('Write operation failed:');
console.log(replies.errors.map((error: any) => error.message));
}
return replies;
};
export const write = async (options: WriteOptions): Promise<UpdateResult> => {
- const requests: docs_v1.Schema$Request[] = [];
+ const requests: docsV1.Schema$Request[] = [];
const documentId = await Utils.initialize(options.reference);
if (!documentId) {
return undefined;
}
- let index = options.index;
- const mode = options.mode;
+ let { index } = options;
+ const { mode } = options;
if (!(index && mode === WriteMode.Insert)) {
const schema = await retrieve({ documentId });
+ // eslint-disable-next-line no-cond-assign
if (!schema || !(index = Utils.endOf(schema))) {
return undefined;
}
}
if (mode === WriteMode.Replace) {
- index > 1 && requests.push({
- deleteContentRange: {
- range: {
- startIndex: 1,
- endIndex: index
- }
- }
- });
+ index > 1 &&
+ requests.push({
+ deleteContentRange: {
+ range: {
+ startIndex: 1,
+ endIndex: index,
+ },
+ },
+ });
index = 1;
}
- const text = options.content.text;
- text.length && requests.push({
- insertText: {
- text: isArray(text) ? text.join("\n") : text,
- location: { index }
- }
- });
+ const { text } = options.content;
+ text.length &&
+ requests.push({
+ insertText: {
+ text: isArray(text) ? text.join('\n') : text,
+ location: { index },
+ },
+ });
if (!requests.length) {
return undefined;
}
requests.push(...options.content.requests);
const replies: any = await update({ documentId, requests });
- if ("errors" in replies) {
- console.log("Write operation failed:");
+ if ('errors' in replies) {
+ console.log('Write operation failed:');
console.log(replies.errors.map((error: any) => error.message));
}
return replies;
};
-
}
-
-} \ No newline at end of file
+}
diff --git a/src/client/apis/google_docs/GooglePhotosClientUtils.ts b/src/client/apis/google_docs/GooglePhotosClientUtils.ts
index e8fd8fb8a..fdc185a8e 100644
--- a/src/client/apis/google_docs/GooglePhotosClientUtils.ts
+++ b/src/client/apis/google_docs/GooglePhotosClientUtils.ts
@@ -1,18 +1,19 @@
+/* eslint-disable no-use-before-define */
+import Photos = require('googlephotos');
import { AssertionError } from 'assert';
import { EditorState } from 'prosemirror-state';
+import { ClientUtils } from '../../../ClientUtils';
import { Doc, DocListCastAsync, Opt } from '../../../fields/Doc';
import { Id } from '../../../fields/FieldSymbols';
import { RichTextField } from '../../../fields/RichTextField';
import { RichTextUtils } from '../../../fields/RichTextUtils';
-import { Cast, StrCast } from '../../../fields/Types';
-import { ImageField } from '../../../fields/URLField';
+import { Cast, ImageCast, StrCast } from '../../../fields/Types';
import { MediaItem, NewMediaItemResult } from '../../../server/apis/google/SharedTypes';
-import { Utils } from '../../../Utils';
-import { Docs, DocumentOptions, DocUtils } from '../../documents/Documents';
import { Networking } from '../../Network';
+import { Docs, DocumentOptions } from '../../documents/Documents';
+import { DocUtils } from '../../documents/DocUtils';
import { FormattedTextBox } from '../../views/nodes/formattedText/FormattedTextBox';
import { GoogleAuthenticationManager } from '../GoogleAuthenticationManager';
-import Photos = require('googlephotos');
export namespace GooglePhotos {
const endpoint = async () => new Photos(await GoogleAuthenticationManager.Instance.fetchOrGenerateAccessToken());
@@ -76,17 +77,16 @@ export namespace GooglePhotos {
export const CollectionToAlbum = async (options: AlbumCreationOptions): Promise<Opt<AlbumCreationResult>> => {
const { collection, title, descriptionKey, tag } = options;
const dataDocument = Doc.GetProto(collection);
- const images = ((await DocListCastAsync(dataDocument.data)) || []).filter(doc => Cast(doc.data, ImageField));
+ const images = ((await DocListCastAsync(dataDocument.data)) || []).filter(doc => ImageCast(doc.data));
if (!images || !images.length) {
return undefined;
}
- const resolved = title ? title : StrCast(collection.title) || `Dash Collection (${collection[Id]}`;
+ const resolved = title || StrCast(collection.title) || `Dash Collection (${collection[Id]}`;
const { id, productUrl } = await Create.Album(resolved);
const response = await Transactions.UploadImages(images, { id }, descriptionKey);
if (response) {
const { results, failed } = response;
- let index: Opt<number>;
- while ((index = failed.pop()) !== undefined) {
+ for (let index = failed.pop(); index !== undefined; index = failed.pop()) {
Doc.RemoveDocFromList(dataDocument, 'data', images.splice(index, 1)[0]);
}
const mediaItems: MediaItem[] = results.map(item => item.mediaItem);
@@ -97,13 +97,12 @@ export namespace GooglePhotos {
for (let i = 0; i < images.length; i++) {
const image = Doc.GetProto(images[i]);
const mediaItem = mediaItems[i];
- if (!mediaItem) {
- continue;
+ if (mediaItem) {
+ image.googlePhotosId = mediaItem.id;
+ image.googlePhotosAlbumUrl = productUrl;
+ image.googlePhotosUrl = mediaItem.productUrl || mediaItem.baseUrl;
+ idMapping[mediaItem.id] = image;
}
- image.googlePhotosId = mediaItem.id;
- image.googlePhotosAlbumUrl = productUrl;
- image.googlePhotosUrl = mediaItem.productUrl || mediaItem.baseUrl;
- idMapping[mediaItem.id] = image;
}
collection.googlePhotosAlbumUrl = productUrl;
collection.googlePhotosIdMapping = idMapping;
@@ -111,9 +110,10 @@ export namespace GooglePhotos {
await Query.TagChildImages(collection);
}
collection.albumId = id;
- Transactions.AddTextEnrichment(collection, `Find me at ${Utils.prepend(`/doc/${collection[Id]}?sharing=true`)}`);
+ Transactions.AddTextEnrichment(collection, `Find me at ${ClientUtils.prepend(`/doc/${collection[Id]}?sharing=true`)}`);
return { albumId: id, mediaItems };
}
+ return undefined;
};
}
@@ -124,7 +124,7 @@ export namespace GooglePhotos {
await GoogleAuthenticationManager.Instance.fetchOrGenerateAccessToken();
const response = await Query.ContentSearch(requested);
const uploads = await Transactions.WriteMediaItemsToServer(response);
- const children = uploads.map((upload: Transactions.UploadInformation) => Docs.Create.ImageDocument(Utils.fileUrl(upload.fileNames.clean) /*, {"data_contentSize":upload.contentSize}*/));
+ const children = uploads.map((upload: Transactions.UploadInformation) => Docs.Create.ImageDocument(ClientUtils.fileUrl(upload.fileNames.clean) /* , {"data_contentSize":upload.contentSize} */));
const options = { _width: 500, _height: 500 };
return constructor(children, options);
};
@@ -144,7 +144,7 @@ export namespace GooglePhotos {
const images = (await DocListCastAsync(collection.data))!.map(Doc.GetProto);
images?.forEach(image => tagMapping.set(image[Id], ContentCategories.NONE));
const values = Object.values(ContentCategories).filter(value => value !== ContentCategories.NONE);
- for (const value of values) {
+ values.forEach(async value => {
const searched = (await ContentSearch({ included: [value] }))?.mediaItems?.map(({ id }) => id);
searched?.forEach(async id => {
const image = await Cast(idMapping[id], Doc);
@@ -154,7 +154,7 @@ export namespace GooglePhotos {
!tags?.includes(value) && tagMapping.set(key, tags + delimiter + value);
}
});
- }
+ });
images?.forEach(image => {
const concatenated = tagMapping.get(image[Id])!;
const tags = concatenated.split(delimiter);
@@ -200,9 +200,10 @@ export namespace GooglePhotos {
export const AlbumSearch = async (albumId: string, pageSize = 100): Promise<MediaItem[]> => {
const photos = await endpoint();
const mediaItems: MediaItem[] = [];
- let nextPageTokenStored: Opt<string> = undefined;
+ let nextPageTokenStored: Opt<string>;
const found = 0;
do {
+ // eslint-disable-next-line no-await-in-loop
const response: any = await photos.mediaItems.search(albumId, pageSize, nextPageTokenStored);
mediaItems.push(...response.mediaItems);
nextPageTokenStored = response.nextPageToken;
@@ -222,7 +223,7 @@ export namespace GooglePhotos {
excluded.length && excluded.forEach(category => contentFilter.addExcludedContentCategories(category));
filters.setContentFilter(contentFilter);
- const date = options.date;
+ const { date } = options;
if (date) {
const dateFilter = new photos.DateFilter();
if (date instanceof Date) {
@@ -240,15 +241,11 @@ export namespace GooglePhotos {
});
};
- export const GetImage = async (mediaItemId: string): Promise<Transactions.MediaItem> => {
- return (await endpoint()).mediaItems.get(mediaItemId);
- };
+ export const GetImage = async (mediaItemId: string): Promise<Transactions.MediaItem> => (await endpoint()).mediaItems.get(mediaItemId);
}
namespace Create {
- export const Album = async (title: string) => {
- return (await endpoint()).albums.create(title);
- };
+ export const Album = async (title: string) => (await endpoint()).albums.create(title);
}
export namespace Transactions {
@@ -278,6 +275,7 @@ export namespace GooglePhotos {
return enrichmentItem.id;
}
}
+ return undefined;
};
export const WriteMediaItemsToServer = async (body: { mediaItems: any[] }): Promise<UploadInformation[]> => {
@@ -291,9 +289,12 @@ export namespace GooglePhotos {
return undefined;
}
const baseUrls: string[] = await Promise.all(
- response.results.map(item => {
- return new Promise<string>(resolve => Query.GetImage(item.mediaItem.id).then(item => resolve(item.baseUrl)));
- })
+ response.results.map(
+ item =>
+ new Promise<string>(resolve => {
+ Query.GetImage(item.mediaItem.id).then(itm => resolve(itm.baseUrl));
+ })
+ )
);
return baseUrls;
};
@@ -303,31 +304,29 @@ export namespace GooglePhotos {
failed: number[];
}
- export const UploadImages = async (sources: Doc[], album?: AlbumReference, descriptionKey = 'caption'): Promise<Opt<ImageUploadResults>> => {
+ export const UploadImages = async (sources: Doc[], albumIn?: AlbumReference, descriptionKey = 'caption'): Promise<Opt<ImageUploadResults>> => {
await GoogleAuthenticationManager.Instance.fetchOrGenerateAccessToken();
- if (album && 'title' in album) {
- album = await Create.Album(album.title);
- }
+ const album = albumIn && 'title' in albumIn ? await Create.Album(albumIn.title) : albumIn;
const media: MediaInput[] = [];
- for (const source of sources) {
- const data = Cast(Doc.GetProto(source).data, ImageField);
- if (!data) {
- return;
- }
- const url = data.url.href;
- const target = Doc.MakeEmbedding(source);
- const description = parseDescription(target, descriptionKey);
- await DocUtils.makeCustomViewClicked(target, Docs.Create.FreeformDocument);
- media.push({ url, description });
- }
+ sources
+ .filter(source => ImageCast(Doc.GetProto(source).data))
+ .forEach(async source => {
+ const data = ImageCast(Doc.GetProto(source).data);
+ const url = data.url.href;
+ const target = Doc.MakeEmbedding(source);
+ const description = parseDescription(target, descriptionKey);
+ await DocUtils.makeCustomViewClicked(target, Docs.Create.FreeformDocument);
+ media.push({ url, description });
+ });
if (media.length) {
const results = await Networking.PostToServer('/googlePhotosMediaPost', { media, album });
return results;
}
+ return undefined;
};
const parseDescription = (document: Doc, descriptionKey: string) => {
- let description: string = Utils.prepend(`/doc/${document[Id]}?sharing=true`);
+ let description: string = ClientUtils.prepend(`/doc/${document[Id]}?sharing=true`);
const target = document[descriptionKey];
if (typeof target === 'string') {
description = target;
diff --git a/src/client/apis/gpt/GPT.ts b/src/client/apis/gpt/GPT.ts
index 30194f9f8..8f58ec364 100644
--- a/src/client/apis/gpt/GPT.ts
+++ b/src/client/apis/gpt/GPT.ts
@@ -27,24 +27,28 @@ const callTypeMap: { [type: string]: GPTCallOpts } = {
data: { model: 'gpt-3.5-turbo', maxTokens: 256, temp: 0.5, prompt: "You are a helpful resarch assistant. Analyze the user's data to find meaningful patterns and/or correlation. Please keep your response short and to the point." },
};
+let lastCall = '';
+let lastResp = '';
/**
* Calls the OpenAI API.
*
* @param inputText Text to process
* @returns AI Output
*/
-const gptAPICall = async (inputText: string, callType: GPTCallType, prompt?: any) => {
- if (callType === GPTCallType.SUMMARY) inputText += '.';
+const gptAPICall = async (inputTextIn: string, callType: GPTCallType, prompt?: any) => {
+ const inputText = callType === GPTCallType.SUMMARY ? inputTextIn + '.' : inputTextIn;
const opts: GPTCallOpts = callTypeMap[callType];
+ if (lastCall === inputText) return lastResp;
try {
const configuration: ClientOptions = {
apiKey: process.env.OPENAI_KEY,
dangerouslyAllowBrowser: true,
};
+ lastCall = inputText;
const openai = new OpenAI(configuration);
- let usePrompt = prompt ? opts.prompt + prompt : opts.prompt;
- let messages: ChatCompletionMessageParam[] = [
+ const usePrompt = prompt ? opts.prompt + prompt : opts.prompt;
+ const messages: ChatCompletionMessageParam[] = [
{ role: 'system', content: usePrompt },
{ role: 'user', content: inputText },
];
@@ -55,8 +59,8 @@ const gptAPICall = async (inputText: string, callType: GPTCallType, prompt?: any
temperature: opts.temp,
messages,
});
- const content = response.choices[0].message.content;
- return content;
+ lastResp = response.choices[0].message.content ?? '';
+ return lastResp;
} catch (err) {
console.log(err);
return 'Error connecting with API.';
@@ -79,8 +83,8 @@ const gptImageCall = async (prompt: string, n?: number) => {
return response.data.map(data => data.url);
} catch (err) {
console.error(err);
- return;
}
+ return undefined;
};
export { gptAPICall, gptImageCall, GPTCallType };
diff --git a/src/client/apis/youtube/YoutubeBox.scss b/src/client/apis/youtube/YoutubeBox.scss
deleted file mode 100644
index eabdbb1ac..000000000
--- a/src/client/apis/youtube/YoutubeBox.scss
+++ /dev/null
@@ -1,126 +0,0 @@
-.youtubeBox-cont {
- ul {
- list-style-type: none;
- padding-inline-start: 10px;
- }
-
-
- li {
- margin: 4px;
- display: inline-flex;
- }
-
- li:hover {
- cursor: pointer;
- opacity: 0.8;
- }
-
- .search_wrapper {
- width: 100%;
- display: inline-flex;
- height: 175px;
-
- .video_duration {
- // margin: 0;
- // padding: 0;
- border: 0;
- background: transparent;
- display: inline-block;
- position: relative;
- bottom: 25px;
- left: 85%;
- margin: 4px;
- color: #FFFFFF;
- background-color: rgba(0, 0, 0, 0.80);
- padding: 2px 4px;
- border-radius: 2px;
- letter-spacing: .5px;
- font-size: 1.2rem;
- font-weight: 500;
- line-height: 1.2rem;
-
- }
-
- .textual_info {
- font-family: Arial, Helvetica, sans-serif;
-
- .videoTitle {
- margin-left: 4px;
- // display: inline-block;
- color: #0D0D0D;
- -webkit-line-clamp: 2;
- display: block;
- max-height: 4.8rem;
- overflow: hidden;
- font-size: 1.8rem;
- font-weight: 400;
- line-height: 2.4rem;
- -webkit-box-orient: vertical;
- text-overflow: ellipsis;
- white-space: normal;
- display: -webkit-box;
- }
-
- .channelName {
- color: #606060;
- margin-left: 4px;
- font-size: 1.3rem;
- font-weight: 400;
- line-height: 1.8rem;
- text-transform: none;
- margin-top: 0px;
- display: inline-block;
- }
-
- .video_description {
- margin-left: 4px;
- // font-size: 12px;
- color: #606060;
- padding-top: 8px;
- margin-bottom: 8px;
- display: block;
- line-height: 1.8rem;
- max-height: 4.2rem;
- overflow: hidden;
- font-size: 1.3rem;
- font-weight: 400;
- text-transform: none;
- }
-
- .publish_time {
- //display: inline-block;
- margin-left: 8px;
- padding: 0;
- border: 0;
- background: transparent;
- color: #606060;
- max-width: 100%;
- line-height: 1.8rem;
- max-height: 3.6rem;
- overflow: hidden;
- font-size: 1.3rem;
- font-weight: 400;
- text-transform: none;
- }
-
- .viewCount {
-
- margin-left: 8px;
- padding: 0;
- border: 0;
- background: transparent;
- color: #606060;
- max-width: 100%;
- line-height: 1.8rem;
- max-height: 3.6rem;
- overflow: hidden;
- font-size: 1.3rem;
- font-weight: 400;
- text-transform: none;
- }
-
-
-
- }
- }
-} \ No newline at end of file
diff --git a/src/client/apis/youtube/YoutubeBox.tsx b/src/client/apis/youtube/YoutubeBox.tsx
deleted file mode 100644
index d3a15cd84..000000000
--- a/src/client/apis/youtube/YoutubeBox.tsx
+++ /dev/null
@@ -1,369 +0,0 @@
-import { action, observable, runInAction } from 'mobx';
-import { observer } from 'mobx-react';
-import { Doc, DocListCastAsync } from '../../../fields/Doc';
-import { InkTool } from '../../../fields/InkField';
-import { Cast, NumCast, StrCast } from '../../../fields/Types';
-import { Utils } from '../../../Utils';
-import { DocServer } from '../../DocServer';
-import { Docs } from '../../documents/Documents';
-import { DocumentView } from '../../views/nodes/DocumentView';
-import { FieldView, FieldViewProps } from '../../views/nodes/FieldView';
-import '../../views/nodes/WebBox.scss';
-import './YoutubeBox.scss';
-import * as React from 'react';
-import { SnappingManager } from '../../util/SnappingManager';
-
-interface VideoTemplate {
- thumbnailUrl: string;
- videoTitle: string;
- videoId: string;
- duration: string;
- channelTitle: string;
- viewCount: string;
- publishDate: string;
- videoDescription: string;
-}
-
-/**
- * This class models the youtube search document that can be dropped on to canvas.
- */
-@observer
-export class YoutubeBox extends React.Component<FieldViewProps> {
- @observable YoutubeSearchElement: HTMLInputElement | undefined;
- @observable searchResultsFound: boolean = false;
- @observable searchResults: any[] = [];
- @observable videoClicked: boolean = false;
- @observable selectedVideoUrl: string = '';
- @observable lisOfBackUp: JSX.Element[] = [];
- @observable videoIds: string | undefined;
- @observable videoDetails: any[] = [];
- @observable curVideoTemplates: VideoTemplate[] = [];
-
- public static LayoutString(fieldKey: string) {
- return FieldView.LayoutString(YoutubeBox, fieldKey);
- }
-
- /**
- * When component mounts, last search's results are laoded in based on the back up stored
- * in the document of the props.
- */
- async componentDidMount() {
- //DocServer.getYoutubeChannels();
- const castedSearchBackUp = Cast(this.props.Document.cachedSearchResults, Doc);
- const awaitedBackUp = await castedSearchBackUp;
- const castedDetailBackUp = Cast(this.props.Document.cachedDetails, Doc);
- const awaitedDetails = await castedDetailBackUp;
-
- if (awaitedBackUp) {
- const jsonList = await DocListCastAsync(awaitedBackUp.json);
- const jsonDetailList = await DocListCastAsync(awaitedDetails!.json);
-
- if (jsonList!.length !== 0) {
- runInAction(() => (this.searchResultsFound = true));
- let index = 0;
- //getting the necessary information from backUps and building templates that will be used to map in render
- for (const video of jsonList!) {
- const videoId = await Cast(video.id, Doc);
- const id = StrCast(videoId!.videoId);
- const snippet = await Cast(video.snippet, Doc);
- const videoTitle = this.filterYoutubeTitleResult(StrCast(snippet!.title));
- const thumbnail = await Cast(snippet!.thumbnails, Doc);
- const thumbnailMedium = await Cast(thumbnail!.medium, Doc);
- const thumbnailUrl = StrCast(thumbnailMedium!.url);
- const videoDescription = StrCast(snippet!.description);
- const pusblishDate = this.roundPublishTime(StrCast(snippet!.publishedAt))!;
- const channelTitle = StrCast(snippet!.channelTitle);
- let duration: string = '';
- let viewCount: string = '';
- if (jsonDetailList!.length !== 0) {
- const contentDetails = await Cast(jsonDetailList![index].contentDetails, Doc);
- const statistics = await Cast(jsonDetailList![index].statistics, Doc);
- duration = this.convertIsoTimeToDuration(StrCast(contentDetails!.duration));
- viewCount = this.abbreviateViewCount(parseInt(StrCast(statistics!.viewCount)))!;
- }
- index = index + 1;
- const newTemplate: VideoTemplate = {
- videoId: id,
- videoTitle: videoTitle,
- thumbnailUrl: thumbnailUrl,
- publishDate: pusblishDate,
- channelTitle: channelTitle,
- videoDescription: videoDescription,
- duration: duration,
- viewCount: viewCount,
- };
- runInAction(() => this.curVideoTemplates.push(newTemplate));
- }
- }
- }
- }
-
- _ignore = 0;
- onPreWheel = (e: React.WheelEvent) => {
- this._ignore = e.timeStamp;
- };
- onPrePointer = (e: React.PointerEvent) => {
- this._ignore = e.timeStamp;
- };
- onPostPointer = (e: React.PointerEvent) => {
- if (this._ignore !== e.timeStamp) {
- e.stopPropagation();
- }
- };
- onPostWheel = (e: React.WheelEvent) => {
- if (this._ignore !== e.timeStamp) {
- e.stopPropagation();
- }
- };
-
- /**
- * Function that submits the title entered by user on enter press.
- */
- onEnterKeyDown = (e: React.KeyboardEvent) => {
- if (e.keyCode === 13) {
- const submittedTitle = this.YoutubeSearchElement!.value;
- this.YoutubeSearchElement!.value = '';
- this.YoutubeSearchElement!.blur();
- DocServer.getYoutubeVideos(submittedTitle, this.processesVideoResults);
- }
- };
-
- /**
- * The callback that is passed in to server, which functions as a way to
- * get videos that is returned by search. It also makes a call to server
- * to get details for the videos found.
- */
- @action
- processesVideoResults = (videos: any[]) => {
- this.searchResults = videos;
- if (this.searchResults.length > 0) {
- this.searchResultsFound = true;
- this.videoIds = '';
- videos.forEach(video => {
- if (this.videoIds === '') {
- this.videoIds = video.id.videoId;
- } else {
- this.videoIds = this.videoIds! + ', ' + video.id.videoId;
- }
- });
- //Asking for details that include duration and viewCount from server for videoIds
- DocServer.getYoutubeVideoDetails(this.videoIds, this.processVideoDetails);
- this.backUpSearchResults(videos);
- if (this.videoClicked) {
- this.videoClicked = false;
- }
- }
- };
-
- /**
- * The callback that is given to server to process and receive returned details about the videos.
- */
- @action
- processVideoDetails = (videoDetails: any[]) => {
- this.videoDetails = videoDetails;
- this.props.Document.cachedDetails = Doc.Get.FromJson({ data: videoDetails, title: 'detailBackUp' });
- };
-
- /**
- * The function that stores the search results in the props document.
- */
- backUpSearchResults = (videos: any[]) => {
- this.props.Document.cachedSearchResults = Doc.Get.FromJson({ data: videos, title: 'videosBackUp' });
- };
-
- /**
- * The function that filters out escaped characters returned by the api
- * in the title of the videos.
- */
- filterYoutubeTitleResult = (resultTitle: string) => {
- let processedTitle: string = resultTitle.replace(/&amp;/g, '&'); //.ReplaceAll("&amp;", "&");
- processedTitle = processedTitle.replace(/"&#39;/g, "'");
- processedTitle = processedTitle.replace(/&quot;/g, '"');
- return processedTitle;
- };
-
- /**
- * The function that converts ISO date, which is passed in, to normal date and finds the
- * difference between today's date and that date, in terms of "ago" to imitate youtube.
- */
- roundPublishTime = (publishTime: string) => {
- const date = new Date(publishTime).getTime();
- const curDate = new Date().getTime();
- const timeDif = curDate - date;
- const totalSeconds = timeDif / 1000;
- const totalMin = totalSeconds / 60;
- const totalHours = totalMin / 60;
- const totalDays = totalHours / 24;
- const totalMonths = totalDays / 30.417;
- const totalYears = totalMonths / 12;
-
- const truncYears = Math.trunc(totalYears);
- const truncMonths = Math.trunc(totalMonths);
- const truncDays = Math.trunc(totalDays);
- const truncHours = Math.trunc(totalHours);
- const truncMin = Math.trunc(totalMin);
- const truncSec = Math.trunc(totalSeconds);
-
- let pluralCase = '';
-
- if (truncYears !== 0) {
- truncYears > 1 ? (pluralCase = 's') : (pluralCase = '');
- return truncYears + ' year' + pluralCase + ' ago';
- } else if (truncMonths !== 0) {
- truncMonths > 1 ? (pluralCase = 's') : (pluralCase = '');
- return truncMonths + ' month' + pluralCase + ' ago';
- } else if (truncDays !== 0) {
- truncDays > 1 ? (pluralCase = 's') : (pluralCase = '');
- return truncDays + ' day' + pluralCase + ' ago';
- } else if (truncHours !== 0) {
- truncHours > 1 ? (pluralCase = 's') : (pluralCase = '');
- return truncHours + ' hour' + pluralCase + ' ago';
- } else if (truncMin !== 0) {
- truncMin > 1 ? (pluralCase = 's') : (pluralCase = '');
- return truncMin + ' minute' + pluralCase + ' ago';
- } else if (truncSec !== 0) {
- truncSec > 1 ? (pluralCase = 's') : (pluralCase = '');
- return truncSec + ' second' + pluralCase + ' ago';
- }
- };
-
- /**
- * The function that converts the passed in ISO time to normal duration time.
- */
- convertIsoTimeToDuration = (isoDur: string) => {
- const convertedTime = isoDur.replace(/D|H|M/g, ':').replace(/P|T|S/g, '').split(':');
-
- if (1 === convertedTime.length) {
- 2 !== convertedTime[0].length && (convertedTime[0] = '0' + convertedTime[0]), (convertedTime[0] = '0:' + convertedTime[0]);
- } else {
- for (var r = 1, l = convertedTime.length - 1; l >= r; r++) {
- 2 !== convertedTime[r].length && (convertedTime[r] = '0' + convertedTime[r]);
- }
- }
-
- return convertedTime.join(':');
- };
-
- /**
- * The function that rounds the viewCount to the nearest
- * thousand, million or billion, given a viewCount number.
- */
- abbreviateViewCount = (viewCount: number) => {
- if (viewCount < 1000) {
- return viewCount.toString();
- } else if (viewCount >= 1000 && viewCount < 1000000) {
- return Math.trunc(viewCount / 1000) + 'K';
- } else if (viewCount >= 1000000 && viewCount < 1000000000) {
- return Math.trunc(viewCount / 1000000) + 'M';
- } else if (viewCount >= 1000000000) {
- return Math.trunc(viewCount / 1000000000) + 'B';
- }
- };
-
- /**
- * The function that is called to decide on what'll be rendered by the component.
- * It renders search Results if found. If user didn't do a new search, it renders from the videoTemplates
- * generated by the backUps. If none present, renders nothing.
- */
- renderSearchResultsOrVideo = () => {
- if (this.searchResultsFound) {
- if (this.searchResults.length !== 0) {
- return (
- <ul>
- {this.searchResults.map((video, index) => {
- const filteredTitle = this.filterYoutubeTitleResult(video.snippet.title);
- const channelTitle = video.snippet.channelTitle;
- const videoDescription = video.snippet.description;
- const pusblishDate = this.roundPublishTime(video.snippet.publishedAt);
- let duration;
- let viewCount;
- if (this.videoDetails.length !== 0) {
- duration = this.convertIsoTimeToDuration(this.videoDetails[index].contentDetails.duration);
- viewCount = this.abbreviateViewCount(this.videoDetails[index].statistics.viewCount);
- }
-
- return (
- <li onClick={() => this.embedVideoOnClick(video.id.videoId, filteredTitle)} key={Utils.GenerateGuid()}>
- <div className="search_wrapper">
- <div style={{ backgroundColor: 'yellow' }}>
- <img src={video.snippet.thumbnails.medium.url} />
- <span className="video_duration">{duration}</span>
- </div>
- <div className="textual_info">
- <span className="videoTitle">{filteredTitle}</span>
- <span className="channelName">{channelTitle}</span>
- <span className="viewCount">{viewCount}</span>
- <span className="publish_time">{pusblishDate}</span>
- <p className="video_description">{videoDescription}</p>
- </div>
- </div>
- </li>
- );
- })}
- </ul>
- );
- } else if (this.curVideoTemplates.length !== 0) {
- return (
- <ul>
- {this.curVideoTemplates.map((video: VideoTemplate) => {
- return (
- <li onClick={() => this.embedVideoOnClick(video.videoId, video.videoTitle)} key={Utils.GenerateGuid()}>
- <div className="search_wrapper">
- <div style={{ backgroundColor: 'yellow' }}>
- <img src={video.thumbnailUrl} />
- <span className="video_duration">{video.duration}</span>
- </div>
- <div className="textual_info">
- <span className="videoTitle">{video.videoTitle}</span>
- <span className="channelName">{video.channelTitle}</span>
- <span className="viewCount">{video.viewCount}</span>
- <span className="publish_time">{video.publishDate}</span>
- <p className="video_description">{video.videoDescription}</p>
- </div>
- </div>
- </li>
- );
- })}
- </ul>
- );
- }
- } else {
- return null;
- }
- };
-
- /**
- * Given a videoId and title, creates a new youtube embedded url, and uses that
- * to create a new video document.
- */
- @action
- embedVideoOnClick = (videoId: string, filteredTitle: string) => {
- const embeddedUrl = 'https://www.youtube.com/embed/' + videoId;
- this.selectedVideoUrl = embeddedUrl;
- const addFunction = this.props.addDocument!;
- const newVideoX = NumCast(this.props.Document.x);
- const newVideoY = NumCast(this.props.Document.y) + NumCast(this.props.Document.height);
-
- addFunction(Docs.Create.VideoDocument(embeddedUrl, { title: filteredTitle, _width: 400, _height: 315, x: newVideoX, y: newVideoY }));
- this.videoClicked = true;
- };
-
- render() {
- const content = (
- <div className="youtubeBox-cont" style={{ width: '100%', height: '100%', position: 'absolute' }} onWheel={this.onPostWheel} onPointerDown={this.onPostPointer} onPointerMove={this.onPostPointer} onPointerUp={this.onPostPointer}>
- <input type="text" placeholder="Search for a video" onKeyDown={this.onEnterKeyDown} style={{ height: 40, width: '100%', border: '1px solid black', padding: 5, textAlign: 'center' }} ref={e => (this.YoutubeSearchElement = e!)} />
- {this.renderSearchResultsOrVideo()}
- </div>
- );
-
- const frozen = !this.props.isSelected() || SnappingManager.IsResizing;
-
- const classname = 'webBox-cont' + (this.props.isSelected() && Doc.ActiveTool === InkTool.None && !SnappingManager.IsResizing ? '-interactive' : '');
- return (
- <>
- <div className={classname}>{content}</div>
- {!frozen ? null : <div className="webBox-overlay" onWheel={this.onPreWheel} onPointerDown={this.onPrePointer} onPointerMove={this.onPrePointer} onPointerUp={this.onPrePointer} />}
- </>
- );
- }
-}