diff options
Diffstat (limited to 'src/client/apis')
| -rw-r--r-- | src/client/apis/GoogleAuthenticationManager.scss | 19 | ||||
| -rw-r--r-- | src/client/apis/GoogleAuthenticationManager.tsx | 140 | ||||
| -rw-r--r-- | src/client/apis/google_docs/GooglePhotosClientUtils.ts | 38 |
3 files changed, 183 insertions, 14 deletions
diff --git a/src/client/apis/GoogleAuthenticationManager.scss b/src/client/apis/GoogleAuthenticationManager.scss new file mode 100644 index 000000000..13bde822d --- /dev/null +++ b/src/client/apis/GoogleAuthenticationManager.scss @@ -0,0 +1,19 @@ +.authorize-container { + display: flex; + flex-direction: column; + align-items: center; + + .paste-target { + padding: 5px; + width: 100%; + } + + .avatar { + border-radius: 50%; + } + + .welcome { + font-style: italic; + margin-top: 15px; + } +}
\ No newline at end of file diff --git a/src/client/apis/GoogleAuthenticationManager.tsx b/src/client/apis/GoogleAuthenticationManager.tsx new file mode 100644 index 000000000..01dac3996 --- /dev/null +++ b/src/client/apis/GoogleAuthenticationManager.tsx @@ -0,0 +1,140 @@ +import { observable, action, reaction, runInAction } from "mobx"; +import { observer } from "mobx-react"; +import * as React from "react"; +import MainViewModal from "../views/MainViewModal"; +import { Opt } from "../../new_fields/Doc"; +import { Identified } from "../Network"; +import { RouteStore } from "../../server/RouteStore"; +import "./GoogleAuthenticationManager.scss"; + +const AuthenticationUrl = "https://accounts.google.com/o/oauth2/v2/auth"; +const prompt = "Paste authorization code here..."; + +@observer +export default class GoogleAuthenticationManager extends React.Component<{}> { + public static Instance: GoogleAuthenticationManager; + @observable private openState = false; + private authenticationLink: Opt<string> = undefined; + @observable private authenticationCode: Opt<string> = undefined; + @observable private clickedState = false; + @observable private success: Opt<boolean> = undefined; + @observable private displayLauncher = true; + @observable private avatar: Opt<string> = undefined; + @observable private username: Opt<string> = undefined; + + private set isOpen(value: boolean) { + runInAction(() => this.openState = value); + } + + private set hasBeenClicked(value: boolean) { + runInAction(() => this.clickedState = value); + } + + public fetchOrGenerateAccessToken = async () => { + let response = await Identified.FetchFromServer(RouteStore.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; + this.authenticationLink = response; + return new Promise<string>(async resolve => { + const disposer = reaction( + () => this.authenticationCode, + authenticationCode => { + if (authenticationCode) { + Identified.PostToServer(RouteStore.writeGoogleAccessToken, { authenticationCode }).then( + ({ access_token, avatar, name }) => { + runInAction(() => { + this.avatar = avatar; + this.username = name; + }); + this.beginFadeout(); + disposer(); + resolve(access_token); + }, + action(() => { + this.hasBeenClicked = false; + this.success = false; + }) + ); + } + } + ); + }); + } + // otherwise, we already have a valid, stored access token + return response; + } + + beginFadeout = action(() => { + this.success = true; + this.authenticationCode = undefined; + this.displayLauncher = false; + this.hasBeenClicked = false; + setTimeout(action(() => { + this.isOpen = false; + setTimeout(action(() => { + this.success = undefined; + this.displayLauncher = true; + this.avatar = undefined; + this.username = undefined; + }), 500); + }), 3000); + }); + + constructor(props: {}) { + super(props); + GoogleAuthenticationManager.Instance = this; + } + + private handleClick = () => { + window.open(this.authenticationLink); + setTimeout(() => this.hasBeenClicked = true, 500); + } + + private handlePaste = action((e: React.ChangeEvent<HTMLInputElement>) => { + this.authenticationCode = e.currentTarget.value; + }); + + private get renderPrompt() { + return ( + <div className={'authorize-container'}> + {this.displayLauncher ? <button + className={"dispatch"} + onClick={this.handleClick} + style={{ marginBottom: this.clickedState ? 15 : 0 }} + >Authorize a Google account...</button> : (null)} + {this.clickedState ? <input + className={'paste-target'} + onChange={this.handlePaste} + placeholder={prompt} + /> : (null)} + {this.avatar ? <img + className={'avatar'} + src={this.avatar} + /> : (null)} + {this.username ? <span + className={'welcome'} + >Welcome to Dash, {this.username} + </span> : (null)} + </div> + ); + } + + private get dialogueBoxStyle() { + const borderColor = this.success === undefined ? "black" : this.success ? "green" : "red"; + return { borderColor, transition: "0.2s borderColor ease" }; + } + + render() { + return ( + <MainViewModal + isDisplayed={this.openState} + interactive={true} + contents={this.renderPrompt} + overlayDisplayedOpacity={0.9} + dialogueBoxStyle={this.dialogueBoxStyle} + /> + ); + } + +}
\ 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 29cc042b6..e93fa6eb4 100644 --- a/src/client/apis/google_docs/GooglePhotosClientUtils.ts +++ b/src/client/apis/google_docs/GooglePhotosClientUtils.ts @@ -13,15 +13,12 @@ import { Docs, DocumentOptions } from "../../documents/Documents"; import { NewMediaItemResult, MediaItem } from "../../../server/apis/google/SharedTypes"; import { AssertionError } from "assert"; import { DocumentView } from "../../views/nodes/DocumentView"; -import { DocumentManager } from "../../util/DocumentManager"; import { Identified } from "../../Network"; +import GoogleAuthenticationManager from "../GoogleAuthenticationManager"; export namespace GooglePhotos { - const endpoint = async () => { - const accessToken = await Identified.FetchFromServer(RouteStore.googlePhotosAccessToken); - return new Photos(accessToken); - }; + const endpoint = async () => new Photos(await GoogleAuthenticationManager.Instance.fetchOrGenerateAccessToken()); export enum MediaType { ALL_MEDIA = 'ALL_MEDIA', @@ -89,9 +86,14 @@ export namespace GooglePhotos { } const resolved = title ? title : (StrCast(collection.title) || `Dash Collection (${collection[Id]}`); const { id, productUrl } = await Create.Album(resolved); - const newMediaItemResults = await Transactions.UploadImages(images, { id }, descriptionKey); - if (newMediaItemResults) { - const mediaItems = newMediaItemResults.map(item => item.mediaItem); + const response = await Transactions.UploadImages(images, { id }, descriptionKey); + if (response) { + const { results, failed } = response; + let index: Opt<number>; + while ((index = failed.pop()) !== undefined) { + Doc.RemoveDocFromList(dataDocument, "data", images.splice(index, 1)[0]); + } + const mediaItems: MediaItem[] = results.map(item => item.mediaItem); if (mediaItems.length !== images.length) { throw new AssertionError({ actual: mediaItems.length, expected: images.length }); } @@ -99,6 +101,9 @@ export namespace GooglePhotos { for (let i = 0; i < images.length; i++) { const image = Doc.GetProto(images[i]); const mediaItem = mediaItems[i]; + if (!mediaItem) { + continue; + } image.googlePhotosId = mediaItem.id; image.googlePhotosAlbumUrl = productUrl; image.googlePhotosUrl = mediaItem.productUrl || mediaItem.baseUrl; @@ -304,17 +309,22 @@ export namespace GooglePhotos { }; export const UploadThenFetch = async (sources: Doc[], album?: AlbumReference, descriptionKey = "caption") => { - const newMediaItems = await UploadImages(sources, album, descriptionKey); - if (!newMediaItems) { + const response = await UploadImages(sources, album, descriptionKey); + if (!response) { return undefined; } - const baseUrls: string[] = await Promise.all(newMediaItems.map(item => { + 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))); })); return baseUrls; }; - export const UploadImages = async (sources: Doc[], album?: AlbumReference, descriptionKey = "caption"): Promise<Opt<NewMediaItemResult[]>> => { + export interface ImageUploadResults { + results: NewMediaItemResult[]; + failed: number[]; + } + + export const UploadImages = async (sources: Doc[], album?: AlbumReference, descriptionKey = "caption"): Promise<Opt<ImageUploadResults>> => { if (album && "title" in album) { album = await Create.Album(album.title); } @@ -331,8 +341,8 @@ export namespace GooglePhotos { media.push({ url, description }); } if (media.length) { - const uploads: NewMediaItemResult[] = await Identified.PostToServer(RouteStore.googlePhotosMediaUpload, { media, album }); - return uploads; + const results = await Identified.PostToServer(RouteStore.googlePhotosMediaUpload, { media, album }); + return results; } }; |
