diff --git a/package-lock.json b/package-lock.json index e768bba9c..7bf302809 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2872,6 +2872,14 @@ "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", "dev": true }, + "abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "requires": { + "event-target-shim": "^5.0.0" + } + }, "accepts": { "version": "1.3.7", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", @@ -4306,12 +4314,34 @@ "tweetnacl": "^0.14.3" } }, + "bent": { + "version": "7.3.12", + "resolved": "https://registry.npmjs.org/bent/-/bent-7.3.12.tgz", + "integrity": "sha512-T3yrKnVGB63zRuoco/7Ybl7BwwGZR0lceoVG5XmQyMIH9s19SV5m+a8qam4if0zQuAmOQTyPTPmsQBdAorGK3w==", + "requires": { + "bytesish": "^0.4.1", + "caseless": "~0.12.0", + "is-stream": "^2.0.0" + }, + "dependencies": { + "is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==" + } + } + }, "big.js": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", "dev": true }, + "bignumber.js": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.1.tgz", + "integrity": "sha512-IdZR9mh6ahOBv/hYGiXyVuyCetmGJhtYkqLBpTStdhEGjegpPlUawydyaF3pbIOFynJTpllEs+NP+CS9jKFLjA==" + }, "binary": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/binary/-/binary-0.3.0.tgz", @@ -4719,6 +4749,11 @@ "resolved": "https://registry.npmjs.org/buffer-equal/-/buffer-equal-0.0.1.tgz", "integrity": "sha1-kbx0sR6kBbyRa8aqkI+q+ltKrEs=" }, + "buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=" + }, "buffer-from": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", @@ -4754,6 +4789,11 @@ "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==", "dev": true }, + "bytesish": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/bytesish/-/bytesish-0.4.4.tgz", + "integrity": "sha512-i4uu6M4zuMUiyfZN4RU2+i9+peJh//pXhd9x1oSe1LBkZ3LEbCoygu8W0bXTukU1Jme2txKuotpCZRaC3FLxcQ==" + }, "cacache": { "version": "13.0.1", "resolved": "https://registry.npmjs.org/cacache/-/cacache-13.0.1.tgz", @@ -4932,8 +4972,7 @@ "caseless": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", - "dev": true + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" }, "chai": { "version": "4.2.0", @@ -6710,6 +6749,14 @@ "safer-buffer": "^2.1.0" } }, + "ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "requires": { + "safe-buffer": "^5.0.1" + } + }, "echarts": { "version": "4.9.0", "resolved": "https://registry.npmjs.org/echarts/-/echarts-4.9.0.tgz", @@ -7785,6 +7832,11 @@ "es5-ext": "~0.10.14" } }, + "event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==" + }, "eventemitter3": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", @@ -8278,6 +8330,11 @@ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=" }, + "fast-text-encoding": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/fast-text-encoding/-/fast-text-encoding-1.0.3.tgz", + "integrity": "sha512-dtm4QZH9nZtcDt8qJiOH9fcQd1NAgi+K1O2DbE6GG1PPCK/BWfOH3idCTRQ4ImXRUOyopDEgDEnVEE7Y/2Wrig==" + }, "faye-websocket": { "version": "0.10.0", "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.10.0.tgz", @@ -8913,6 +8970,39 @@ "integrity": "sha512-j48B/ZI7VKs3sgeI2cZp7WXWmZXu7Iq5pl5/vptV5N2mq+DGFuS/ulaDjtaoLpYzuD6u8UgrUKHfgo7fDTSiBA==", "dev": true }, + "gaxios": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-4.3.0.tgz", + "integrity": "sha512-pHplNbslpwCLMyII/lHPWFQbJWOX0B3R1hwBEOvzYi1GmdKZruuEHK4N9V6f7tf1EaPYyF80mui1+344p6SmLg==", + "requires": { + "abort-controller": "^3.0.0", + "extend": "^3.0.2", + "https-proxy-agent": "^5.0.0", + "is-stream": "^2.0.0", + "node-fetch": "^2.3.0" + }, + "dependencies": { + "is-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", + "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==" + }, + "node-fetch": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", + "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==" + } + } + }, + "gcp-metadata": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-4.3.0.tgz", + "integrity": "sha512-L9XQUpvKJCM76YRSmcxrR4mFPzPGsgZUH+GgHMxAET8qc6+BhRJq63RLhWakgEO2KKVgeSDVfyiNjkGSADwNTA==", + "requires": { + "gaxios": "^4.0.0", + "json-bigint": "^1.0.0" + } + }, "gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -9081,6 +9171,45 @@ } } }, + "google-auth-library": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-7.1.2.tgz", + "integrity": "sha512-FMipHgfe2u1LzWsf2n9zEB9KsJ8M3n8OYTHbHtlkzPCyo7IknXQR5X99nfvwUHGuX+iEpihUZxDuPm7+qBYeXg==", + "requires": { + "arrify": "^2.0.0", + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "fast-text-encoding": "^1.0.0", + "gaxios": "^4.0.0", + "gcp-metadata": "^4.2.0", + "gtoken": "^5.0.4", + "jws": "^4.0.0", + "lru-cache": "^6.0.0" + }, + "dependencies": { + "arrify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", + "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==" + }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "requires": { + "yallist": "^4.0.0" + } + } + } + }, + "google-p12-pem": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-3.1.0.tgz", + "integrity": "sha512-JUtEHXL4DY/N+xhlm7TC3qL797RPAtk0ZGXNs3/gWyiDHYoA/8Rjes0pztkda+sZv4ej1EoO2KhWgW5V9KTrSQ==", + "requires": { + "node-forge": "^0.10.0" + } + }, "got": { "version": "6.7.1", "resolved": "https://registry.npmjs.org/got/-/got-6.7.1.tgz", @@ -9120,6 +9249,16 @@ "integrity": "sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE=", "dev": true }, + "gtoken": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-5.3.0.tgz", + "integrity": "sha512-mCcISYiaRZrJpfqOs0QWa6lfEM/C1V9ASkzFmuz43XBb5s1Vynh+CZy1ECeeJXVGx2PRByjYzb4Y4/zr1byr0w==", + "requires": { + "gaxios": "^4.0.0", + "google-p12-pem": "^3.0.3", + "jws": "^4.0.0" + } + }, "gud": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/gud/-/gud-1.0.0.tgz", @@ -10552,12 +10691,6 @@ "resolved": "https://registry.npmjs.org/is-whitespace-character/-/is-whitespace-character-1.0.4.tgz", "integrity": "sha512-SDweEzfIZM0SJV0EUga669UTKlmL0Pq8Lno0QDQsPnvECB3IM2aP0gdx5TrU0A01MAPfViaZiI2V1QMZLaKK5w==" }, - "is-windows": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", - "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", - "dev": true - }, "is-word-character": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-word-character/-/is-word-character-1.0.4.tgz", @@ -12552,6 +12685,14 @@ "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", "dev": true }, + "json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "requires": { + "bignumber.js": "^9.0.0" + } + }, "json-parse-better-errors": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", @@ -12766,6 +12907,25 @@ "resolved": "https://registry.npmjs.org/jszip-utils/-/jszip-utils-0.1.0.tgz", "integrity": "sha512-tBNe0o3HAf8vo0BrOYnLPnXNo5A3KsRMnkBFYjh20Y3GPYGfgyoclEMgvVchx0nnL+mherPi74yLPIusHUQpZg==" }, + "jwa": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", + "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "requires": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "requires": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, "kareem": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.3.2.tgz", @@ -14093,6 +14253,12 @@ "to-regex": "^3.0.1" }, "dependencies": { + "is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "dev": true + }, "kind-of": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", @@ -14198,8 +14364,7 @@ "node-forge": { "version": "0.10.0", "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.10.0.tgz", - "integrity": "sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA==", - "dev": true + "integrity": "sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA==" }, "node-int64": { "version": "0.4.0", @@ -22622,8 +22787,7 @@ "yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "yaml": { "version": "1.10.0", diff --git a/package.json b/package.json index c617343c5..8e14a715f 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "fontsource-roboto": "^4.0.0", "formik": "^1.3.2", "history": "^4.10.1", + "google-auth-library": "^7.1.0", "i18n-iso-countries": "4.3.1", "intl": "^1.2.5", "is-url": "1.2.4", diff --git a/public/index.html b/public/index.html index ca70e77df..a57485cb8 100644 --- a/public/index.html +++ b/public/index.html @@ -26,6 +26,7 @@ https://www.youtube.com/ https://*.cloudfront.net/ https://*.visualstudio.com/ + https://*.googleapis.com/ https://*.microsoft.com/cognitiveservices/ wss://*.microsoft.com/cognitiveservices/ blob: diff --git a/src/api/api.js b/src/api/api.js index 322ca23b6..34ddd4425 100644 --- a/src/api/api.js +++ b/src/api/api.js @@ -175,6 +175,13 @@ class API { return data; } + async authGooglePhotos(query) { + const { data } = await this.axiosInstance.get( + `/auth/google-photos/callback${query}` + ); + return data; + } + async getBoards({ page = 1, limit = 10, @@ -382,6 +389,28 @@ class API { return response.data.url; } + async uploadFromUrl(url) { + const authToken = getAuthToken(); + if (!(authToken && authToken.length)) { + throw new Error('Need to be authenticated to perform this request'); + } + + const headers = { + Authorization: `Bearer ${authToken}`, + 'Content-Type': 'application/json' + }; + + const response = await this.axiosInstance.post( + 'media/url', + { url }, + { + headers + } + ); + + return response.data.url; + } + async createCommunicator(communicator) { const authToken = getAuthToken(); if (!(authToken && authToken.length)) { diff --git a/src/components/Account/GooglePhotosAuth/GooglePhotosAuth.js b/src/components/Account/GooglePhotosAuth/GooglePhotosAuth.js new file mode 100644 index 000000000..f32eab144 --- /dev/null +++ b/src/components/Account/GooglePhotosAuth/GooglePhotosAuth.js @@ -0,0 +1,23 @@ +import React, { useEffect } from 'react'; +import { useLocation } from 'react-router-dom'; + +import API from '../../../api'; + +export default function GooglePhotosAuth() { + const { search } = useLocation(); + console.log('search', search); + + useEffect( + () => { + async function fetchData() { + console.log('fetching'); + const data = await API['authGooglePhotos'](search); + console.log('data', data); + } + fetchData(); + }, + [search] + ); + + return

loading

; +} diff --git a/src/components/Account/GooglePhotosAuth/index.js b/src/components/Account/GooglePhotosAuth/index.js new file mode 100644 index 000000000..46ca4848b --- /dev/null +++ b/src/components/Account/GooglePhotosAuth/index.js @@ -0,0 +1 @@ +export { default } from './GooglePhotosAuth'; diff --git a/src/components/App/App.actions.js b/src/components/App/App.actions.js index 433c19cc6..e002256c2 100644 --- a/src/components/App/App.actions.js +++ b/src/components/App/App.actions.js @@ -3,10 +3,14 @@ import { UPDATE_DISPLAY_SETTINGS, UPDATE_NAVIGATION_SETTINGS, UPDATE_USER_DATA, + LOG_IN_GOOGLE_PHOTOS, + LOG_OUT_GOOGLE_PHOTOS, DISABLE_TOUR, ENABLE_ALL_TOURS } from './App.constants'; +import API from '../../api'; + export function updateDisplaySettings(payload = {}) { return { type: UPDATE_DISPLAY_SETTINGS, @@ -46,3 +50,28 @@ export function updateUserData(userData) { userData }; } + +export function logInGooglePhotosAuth({ googlePhotosCode, refreshToken }) { + return dispatch => + new Promise(resolve => { + if (googlePhotosCode) { + API['authGooglePhotos'](googlePhotosCode) + .then(googlePhotosAuth => { + dispatch({ + type: LOG_IN_GOOGLE_PHOTOS, + googlePhotosAuth: googlePhotosAuth + }); + resolve(); + }) + .catch(error => { + throw error; + }); + } + }); +} + +export function logOutGooglePhotos() { + return { + type: LOG_OUT_GOOGLE_PHOTOS + }; +} diff --git a/src/components/App/App.constants.js b/src/components/App/App.constants.js index 78d604fcd..40ddab602 100644 --- a/src/components/App/App.constants.js +++ b/src/components/App/App.constants.js @@ -7,6 +7,9 @@ export const UPDATE_DISPLAY_SETTINGS = 'cboard/App/UPDATE_DISPLAY_SETTINGS'; export const UPDATE_NAVIGATION_SETTINGS = 'cboard/App/UPDATE_NAVIGATION_SETTINGS'; export const UPDATE_USER_DATA = 'cboard/App/UPDATE_USER_DATA'; +export const LOG_IN_GOOGLE_PHOTOS = 'cboard/App/LOG_IN_GOOGLE_PHOTOS'; +export const LOG_OUT_GOOGLE_PHOTOS = 'cboard/App/LOG_OUT_GOOGLE_PHOTOS'; + // language constants export const DEFAULT_LANG = 'en-US'; export const APP_LANGS = [ diff --git a/src/components/App/App.reducer.js b/src/components/App/App.reducer.js index d2db71f0f..ccea376da 100644 --- a/src/components/App/App.reducer.js +++ b/src/components/App/App.reducer.js @@ -4,6 +4,8 @@ import { UPDATE_DISPLAY_SETTINGS, UPDATE_NAVIGATION_SETTINGS, UPDATE_USER_DATA, + LOG_IN_GOOGLE_PHOTOS, + LOG_OUT_GOOGLE_PHOTOS, DISABLE_TOUR, ENABLE_ALL_TOURS } from './App.constants'; @@ -131,6 +133,22 @@ function appReducer(state = initialState, action) { ...state, userData: action.userData }; + case LOG_IN_GOOGLE_PHOTOS: + return { + ...state, + userData: { + ...state.userData, + googlePhotosAuth: action.googlePhotosAuth + } + }; + case LOG_OUT_GOOGLE_PHOTOS: + return { + ...state, + userData: { + ...state.userData, + googlePhotosAuth: null + } + }; default: return state; } diff --git a/src/components/Board/Board.actions.js b/src/components/Board/Board.actions.js index 59dc11c04..0867f4c6d 100644 --- a/src/components/Board/Board.actions.js +++ b/src/components/Board/Board.actions.js @@ -36,7 +36,12 @@ import { DOWNLOAD_IMAGES_FAILURE, DOWNLOAD_IMAGES_STARTED, DOWNLOAD_IMAGE_SUCCESS, - DOWNLOAD_IMAGE_FAILURE + DOWNLOAD_IMAGE_FAILURE, + SET_EDITING_TILES, + CLEAR_EDITING_TILES, + UPDATE_EDITING_TILES, + EDITING_TILES_NEXT_STEP, + EDITING_TILES_PREV_STEP } from './Board.constants'; import API from '../../api'; @@ -165,6 +170,40 @@ export function focusTile(tileId, boardId) { }; } +export function setEditingTiles(editingTiles) { + return { + type: SET_EDITING_TILES, + editingTiles + }; +} + +export function clearEditingTiles() { + return { + type: CLEAR_EDITING_TILES + }; +} + +export function updateEditingTiles(id, property, value) { + return { + type: UPDATE_EDITING_TILES, + id, + property, + value + }; +} + +export function editingTilesNextStep() { + return { + type: EDITING_TILES_NEXT_STEP + }; +} + +export function editingTilesPrevStep() { + return { + type: EDITING_TILES_PREV_STEP + }; +} + export function clickSymbol(symbolLabel) { return { type: CLICK_SYMBOL, diff --git a/src/components/Board/Board.constants.js b/src/components/Board/Board.constants.js index 003b2a8b7..e6fe2b4ed 100644 --- a/src/components/Board/Board.constants.js +++ b/src/components/Board/Board.constants.js @@ -37,5 +37,14 @@ export const DOWNLOAD_IMAGES_FAILURE = 'cboard/Board/DOWNLOAD_IMAGES_FAILURE'; export const DOWNLOAD_IMAGES_STARTED = 'cboard/Board/DOWNLOAD_IMAGES_STARTED'; export const DOWNLOAD_IMAGE_SUCCESS = 'cboard/Board/DOWNLOAD_IMAGE_SUCCESS'; export const DOWNLOAD_IMAGE_FAILURE = 'cboard/Board/DOWNLOAD_IMAGE_FAILURE'; +export const SET_EDITING_TILES = 'cboard/Board/TileEditor/SET_EDITING_TILES'; +export const CLEAR_EDITING_TILES = + 'cboard/Board/TileEditor/CLEAR_EDITING_TILES'; +export const UPDATE_EDITING_TILES = + 'cboard/Board/TileEditor/UPDATE_EDITING_TILES'; +export const EDITING_TILES_NEXT_STEP = + 'cboard/Board/TileEditor/EDITING_TILES_NEXT_STEP'; +export const EDITING_TILES_PREV_STEP = + 'cboard/Board/TileEditor/EDITING_TILES_PREV_STEP'; export const DEFAULT_ROWS_NUMBER = 5; export const DEFAULT_COLUMNS_NUMBER = 5; diff --git a/src/components/Board/Board.container.js b/src/components/Board/Board.container.js index 99bdc40a5..ebb21f1fb 100644 --- a/src/components/Board/Board.container.js +++ b/src/components/Board/Board.container.js @@ -44,7 +44,9 @@ import { updateApiObjects, updateApiObjectsNoChild, getApiObjects, - downloadImages + downloadImages, + setEditingTiles, + clearEditingTiles } from './Board.actions'; import { upsertCommunicator, @@ -135,6 +137,9 @@ export class BoardContainer extends Component { * Focuses a board tile */ focusTile: PropTypes.func, + editingTiles: PropTypes.object, + setEditingTiles: PropTypes.func, + clearEditingTiles: PropTypes.func, /** * Change output */ @@ -184,16 +189,26 @@ export class BoardContainer extends Component { isGettingApiObjects: false, copyPublicBoard: false, blockedPrivateBoard: false, - isFixedBoard: false + isFixedBoard: false, + loading: false }; async componentDidMount() { const { match: { params: { id } - } + }, + location: { search: query } } = this.props; + let isGooglePhotosCode = false; + if (query.indexOf('code=') >= 0) { + isGooglePhotosCode = true; + this.setState({ + loading: true + }); + } + const { board, boards, @@ -271,6 +286,7 @@ export class BoardContainer extends Component { //set board type this.setState({ isFixedBoard: !!boardExists.isFixed }); + if (isGooglePhotosCode) this.performGooglePhotos(query); if (isAndroid()) downloadImages(); } @@ -531,7 +547,17 @@ export class BoardContainer extends Component { }; handleEditClick = () => { + const { board, setEditingTiles } = this.props; this.setState({ tileEditorOpen: true }); + const editingTilesValue = this.state.selectedTileIds.map(selectedTileId => { + //this should be on reducer? + const tiles = board.tiles.filter(tile => { + return tile.id === selectedTileId; + })[0]; + + return tiles; + }); + setEditingTiles(editingTilesValue); }; handleBoardTypeChange = async () => { @@ -573,11 +599,13 @@ export class BoardContainer extends Component { }; handleTileEditorCancel = () => { + const { clearEditingTiles } = this.props; this.setState({ tileEditorOpen: false }); + clearEditingTiles(); }; handleEditTileEditorSubmit = tiles => { - const { board, editTiles, userData } = this.props; + const { board, editTiles, userData, clearEditingTiles } = this.props; this.updateIfFeaturedBoard(board); editTiles(tiles, board.id); @@ -586,6 +614,7 @@ export class BoardContainer extends Component { this.handleApiUpdates(null, null, tiles); } this.toggleSelectMode(); + clearEditingTiles(); }; handleAddTileEditorSubmit = tile => { @@ -666,6 +695,8 @@ export class BoardContainer extends Component { }; handleAddClick = () => { + const { clearEditingTiles } = this.props; + clearEditingTiles(); //to prevent error if user navigate during editing this.setState({ tileEditorOpen: true, selectedTileIds: [], @@ -1327,8 +1358,26 @@ export class BoardContainer extends Component { this.saveApiBoardOperation(processedBoard); }; + performGooglePhotos = query => { + this.googlePhotosCode = query; + this.setState({ + tileEditorOpen: true, + loading: false + }); + this.setState({ + isLocked: false + }); + }; + + onExchangeCode = () => { + this.googlePhotosCode = null; + this.setState({ + tileEditorOpen: true + }); + }; + render() { - const { navHistory, board, focusTile } = this.props; + const { navHistory, focusTile } = this.props; if (!this.state.translatedBoard) { return ( @@ -1339,18 +1388,14 @@ export class BoardContainer extends Component { } const disableBackButton = navHistory.length === 1; - const editingTiles = this.state.tileEditorOpen - ? this.state.selectedTileIds.map(selectedTileId => { - const tiles = board.tiles.filter(tile => { - return tile.id === selectedTileId; - })[0]; - - return tiles; - }) - : []; return ( + {this.state.loading && ( +
+ {this.props.intl.formatMessage(messages.boardLoading)} +
+ )} + b.id === action.id + ? { ...b, ...{ [action.property]: action.value } } + : b + ), + activeEditStep: state.editingTiles.activeEditStep + } + }; + case EDITING_TILES_NEXT_STEP: + return { + ...state, + editingTiles: { + editingTiles: state.editingTiles.editingTiles, + activeEditStep: state.editingTiles.activeEditStep + 1 + } + }; + case EDITING_TILES_PREV_STEP: + return { + ...state, + editingTiles: { + editingTiles: state.editingTiles.editingTiles, + activeEditStep: state.editingTiles.activeEditStep - 1 + } + }; case UNMARK_BOARD: return { ...state, diff --git a/src/components/Board/GooglePhotosSearch/GooglePhotosButton/GooglePhotosConnect.Button.css b/src/components/Board/GooglePhotosSearch/GooglePhotosButton/GooglePhotosConnect.Button.css new file mode 100644 index 000000000..8a9c92bca --- /dev/null +++ b/src/components/Board/GooglePhotosSearch/GooglePhotosButton/GooglePhotosConnect.Button.css @@ -0,0 +1,30 @@ +.customBtn { + display: inline-block; + background: white; + color: #444; + width: 100%; + border-radius: 5px; + border: thin solid #888; + white-space: nowrap; +} +.customBtn:hover { + cursor: pointer; + box-shadow: 1px 1px 1px grey; +} +span.icon { + background: url('./Google_Photos_icon.png') transparent 5px 50% no-repeat; + background-position: center; + background-size: 60% 60%; + display: inline-block; + vertical-align: middle; + width: 42px; + height: 42px; +} +span.buttonText { + display: inline-block; + vertical-align: middle; + padding-left: 8px; + font-size: 14px; + /* Use the Roboto font that is loaded in the */ + font-family: 'Roboto', sans-serif; +} diff --git a/src/components/Board/GooglePhotosSearch/GooglePhotosButton/GooglePhotosConnect.Button.js b/src/components/Board/GooglePhotosSearch/GooglePhotosButton/GooglePhotosConnect.Button.js new file mode 100644 index 000000000..89ea81ada --- /dev/null +++ b/src/components/Board/GooglePhotosSearch/GooglePhotosButton/GooglePhotosConnect.Button.js @@ -0,0 +1,18 @@ +/*this button is created using Google Photos UXguidelines https://developers.google.com/photos/library/guides/ux-guidelines*/ +import React from 'react'; +import './GooglePhotosConnect.Button.css'; +import { injectIntl } from 'react-intl'; +import messages from './../GooglePhotosSearch.messages'; + +export default injectIntl(function ConnectToGooglePhotosButton(props) { + return ( +
+ + + {props.intl.formatMessage(messages.addFrom)} +
+ Google Photos +
+
+ ); +}); diff --git a/src/components/Board/GooglePhotosSearch/GooglePhotosButton/Google_Photos_icon.png b/src/components/Board/GooglePhotosSearch/GooglePhotosButton/Google_Photos_icon.png new file mode 100644 index 000000000..5ed351dd0 Binary files /dev/null and b/src/components/Board/GooglePhotosSearch/GooglePhotosButton/Google_Photos_icon.png differ diff --git a/src/components/Board/GooglePhotosSearch/GooglePhotosButton/index.js b/src/components/Board/GooglePhotosSearch/GooglePhotosButton/index.js new file mode 100644 index 000000000..74b090ee7 --- /dev/null +++ b/src/components/Board/GooglePhotosSearch/GooglePhotosButton/index.js @@ -0,0 +1 @@ +export { default } from './GooglePhotosConnect.Button'; diff --git a/src/components/Board/GooglePhotosSearch/GooglePhotosFilter/GooglePhotosFilter.component.js b/src/components/Board/GooglePhotosSearch/GooglePhotosFilter/GooglePhotosFilter.component.js new file mode 100644 index 000000000..316117233 --- /dev/null +++ b/src/components/Board/GooglePhotosSearch/GooglePhotosFilter/GooglePhotosFilter.component.js @@ -0,0 +1,95 @@ +import React, { useState } from 'react'; +import Chip from '@material-ui/core/Chip'; +import Paper from '@material-ui/core/Paper'; + +import './GooglePhotosFilter.css'; +import { Button } from '@material-ui/core'; + +import { injectIntl } from 'react-intl'; +import messages from './GooglePhotosFilter.messages'; + +export default injectIntl(function GooglePhotosFilter(props) { + const { intl } = props; + const [chipData, setChipData] = useState([ + { key: 'LANDSCAPES', label: 'landscapes', value: false }, + { key: 'RECEIPTS', label: 'receipts', value: false }, + { key: 'CITYSCAPES', label: 'cityscapes', value: false }, + { key: 'LANDMARKS', label: 'landmarks', value: false }, + { key: 'SELFIES', label: 'selfies', value: false }, + { key: 'PEOPLE', label: 'people', value: false }, + { key: 'PETS', label: 'pets', value: false }, + { key: 'WEDDINGS', label: 'weddings', value: false }, + { key: 'BIRTHDAYS', label: 'birthdays', value: false }, + { key: 'DOCUMENTS', label: 'documents', value: false }, + { key: 'TRAVEL', label: 'travel', value: false }, + { key: 'ANIMALS', label: 'animals', value: false }, + { key: 'FOOD', label: 'food', value: false }, + { key: 'SPORT', label: 'sport', value: false }, + { key: 'NIGHT', label: 'night', value: false }, + { key: 'PERFORMANCES', label: 'performances', value: false }, + { key: 'WHITEBOARDS', label: 'whiteboards', value: false }, + { key: 'SCREENSHOTS', label: 'screenshots', value: false }, + { key: 'UTILITY', label: 'utility', value: false }, + { key: 'ARTS', label: 'arts', value: false }, + { key: 'CRAFTS', label: 'crafts', value: false }, + { key: 'FASHION', label: 'fashion', value: false }, + { key: 'HOUSES', label: 'houses', value: false }, + { key: 'GARDENS', label: 'gardens', value: false }, + { key: 'FLOWERS', label: 'flowers', value: false }, + { key: 'HOLIDAYS', label: 'holidays', value: false } + ]); + + const handleclick = chipToToogle => () => { + setChipData(chips => + chips.map(chip => { + if (chip.key === chipToToogle.key) { + chip.value = !chip.value; + } + return chip; + }) + ); + }; + + const handleSearchClick = () => { + const filters = []; + const activeFilters = chipData.filter(chip => chip.value); + activeFilters.forEach(element => { + filters.push(element.key); + }); + props.filterSearch(filters); + }; + + return ( + +
+

+ {intl.formatMessage(messages.instructions)} +

+
+
+ {chipData.map(data => { + return ( +
  • + +
  • + ); + })} +
    +
    + +
    +
    + ); +}); diff --git a/src/components/Board/GooglePhotosSearch/GooglePhotosFilter/GooglePhotosFilter.css b/src/components/Board/GooglePhotosSearch/GooglePhotosFilter/GooglePhotosFilter.css new file mode 100644 index 000000000..f787dd97b --- /dev/null +++ b/src/components/Board/GooglePhotosSearch/GooglePhotosFilter/GooglePhotosFilter.css @@ -0,0 +1,45 @@ +.filter_Paper { + display: flex; + justify-content: center; + flex-wrap: wrap; + flex-direction: column; + padding: 4px; + align-items: center; + margin-bottom: 15px; +} + +.filter_content { + display: flex; + justify-content: center; +} + +.filter_subtitle { + font-weight: bold; + text-align: center; +} + +.filter_list { + display: flex; + justify-content: center; + flex-wrap: wrap; + list-style-type: none; + padding: 4px; + margin: 3px; + width: 80%; +} + +.chip { + margin-right: 4px !important; + margin-bottom: 4px !important; + padding: 5px; +} + +.search_button { + margin: 5px !important; +} + +@media (orientation: portrait) { + .filter_list { + width: 95%; + } +} diff --git a/src/components/Board/GooglePhotosSearch/GooglePhotosFilter/GooglePhotosFilter.messages.js b/src/components/Board/GooglePhotosSearch/GooglePhotosFilter/GooglePhotosFilter.messages.js new file mode 100644 index 000000000..c8457f3a0 --- /dev/null +++ b/src/components/Board/GooglePhotosSearch/GooglePhotosFilter/GooglePhotosFilter.messages.js @@ -0,0 +1,14 @@ +import { defineMessages } from 'react-intl'; + +export default defineMessages({ + instructions: { + id: + 'cboard.components.Board.TileEditor.GooglePhotosSearch.googlePhotosFilter.instructions', + defaultMessage: 'SELECT THE FILTERS AND PRESS "SEARCH" BUTTON' + }, + search: { + id: + 'cboard.components.Board.TileEditor.GooglePhotosSearch.googlePhotosFilter.search', + defaultMessage: 'SEARCH' + } +}); diff --git a/src/components/Board/GooglePhotosSearch/GooglePhotosSearch.axios.js b/src/components/Board/GooglePhotosSearch/GooglePhotosSearch.axios.js new file mode 100644 index 000000000..c4068bcba --- /dev/null +++ b/src/components/Board/GooglePhotosSearch/GooglePhotosSearch.axios.js @@ -0,0 +1,55 @@ +import axios from 'axios'; + +//GET https://photoslibrary.googleapis.com/v1/albums/{albumId} +export async function getAlbums(token) { + const urlQuery = 'https://photoslibrary.googleapis.com/v1/albums'; + return axios + .get(urlQuery, { + headers: { + 'Content-type': 'application/json', + Authorization: `Bearer ${token}` + } + }) + .then(response => { + return response.data; + }) + .catch(err => { + throw new Error(err.message); + }); +} + +export function getContent(params) { + const urlQuery = + 'https://content-photoslibrary.googleapis.com/v1/mediaItems:search'; + const body = {}; + + if (params.id) body.albumId = params.id; + + if (params.filters) { + const filtersObject = { + contentFilter: { + includedContentCategories: [params.filters] + } + }; + body.filters = filtersObject; + } + + if (params.nextPage) body.pageToken = params.nextPage; + return axios + .post(urlQuery, body, { + headers: { + 'Content-type': 'application/json', + Authorization: `Bearer ${params.token}` + } + }) + .then(response => { + const onlyImages = response.data.mediaItems?.filter( + file => !file.mimeType.includes('video') + ); + const data = { ...response.data, mediaItems: onlyImages }; //avoid videos + return data; + }) + .catch(err => { + throw new Error(err.message); + }); +} diff --git a/src/components/Board/GooglePhotosSearch/GooglePhotosSearch.container.js b/src/components/Board/GooglePhotosSearch/GooglePhotosSearch.container.js new file mode 100644 index 000000000..2ee63b3d4 --- /dev/null +++ b/src/components/Board/GooglePhotosSearch/GooglePhotosSearch.container.js @@ -0,0 +1,617 @@ +import React, { PureComponent } from 'react'; +import PropTypes from 'prop-types'; +import { injectIntl, intlShape } from 'react-intl'; +import messages from './GooglePhotosSearch.messages'; + +import FullScreenDialog, { + FullScreenDialogContent +} from '../../UI/FullScreenDialog'; +import Alert from '@material-ui/lab/Alert'; + +import BottomNavigation from '@material-ui/core/BottomNavigation'; +import BottomNavigationAction from '@material-ui/core/BottomNavigationAction'; +import ImageSearchIcon from '@material-ui/icons/ImageSearch'; +import PhotoAlbumRoundedIcon from '@material-ui/icons/PhotoAlbumRounded'; +import VisibilityIcon from '@material-ui/icons/Visibility'; + +import List from '@material-ui/core/List'; +import ListItem from '@material-ui/core/ListItem'; +import ListItemAvatar from '@material-ui/core/ListItemAvatar'; +import ListItemText from '@material-ui/core/ListItemText'; +import Avatar from '@material-ui/core/Avatar'; + +import GooglePhotosSearchGallery from './GooglePhotosSearchGallery'; +import Fab from '@material-ui/core/Fab'; +import KeyboardBackspaceIcon from '@material-ui/icons/KeyboardBackspace'; + +import { getAlbums, getContent } from './GooglePhotosSearch.axios'; + +import { Button, Paper } from '@material-ui/core'; +import CircularProgress from '@material-ui/core/CircularProgress'; +import AccountCircleIcon from '@material-ui/icons/AccountCircle'; + +import API from '../../../api'; +import { connect } from 'react-redux'; +import { + logInGooglePhotosAuth, + logOutGooglePhotos +} from '../../App/App.actions'; + +import GooglePhotosFilter from './GooglePhotosFilter/GooglePhotosFilter.component'; + +import './GooglePhotosSearch.css'; + +export class GooglePhotosSearch extends PureComponent { + state = { + isGPhotosConnected: false, + albumsList: null, + albumData: null, + filterData: null, + recentData: null, + view: 'albums', + loading: false, + error: null + // pageMananger: { // to add te ability for manage pages after download more than one + // pagesStored: 0, + // page: 0 + // } + }; + + static propTypes = { + intl: intlShape.isRequired, + open: PropTypes.bool, + onClose: PropTypes.func.isRequired, + googlePhotosCode: PropTypes.string, + onExchangeCode: PropTypes.func, + googlePhotosAuth: PropTypes.object, + logInGooglePhotosAuth: PropTypes.func + }; + + static defaultProps = { + open: false, + googlePhotosCode: null + }; + + logOutGooglePhotosBtn = () => { + const { logOutGooglePhotos, onExchangeCode } = this.props; + logOutGooglePhotos(); + onExchangeCode(); + this.props.onClose(); + }; + + authTokenVerify = async () => { + const { + googlePhotosCode, + googlePhotosAuth, + logOutGooglePhotos + } = this.props; + try { + if (googlePhotosCode && !googlePhotosAuth) { + this.logInGooglePhotos({ googlePhotosCode }); + } else if (googlePhotosAuth) { + try { + await this.gotAlbums(true); //check if expired token + } catch { + logOutGooglePhotos(); // if expired delete token + } + } + } catch (error) { + console.log('logInGooglePhotosAuth error:', error); + this.setState({ + error: error + }); + } + }; + + logInGooglePhotos(params) { + const { logInGooglePhotosAuth } = this.props; + logInGooglePhotosAuth(params).then( + () => { + this.setState({ + error: null + }); + }, + error => { + this.setState({ + error: error + }); + } + ); + } + + gotAlbums = async (checkIfExpired = false) => { + this.setState({ + error: null, + loading: true + }); + try { + const albumsList = await getAlbums( + this.props.googlePhotosAuth?.access_token.toString() + ); + this.setState({ + loading: false, + albumsList: albumsList + }); + } catch (error) { + if (checkIfExpired) throw new Error(error.message); + console.log('getAlbums error:', error); + this.setState({ + loading: false, + error: error + }); + } + }; + + handleBottomNavChange = async (e, value) => { + this.setState({ + view: value, + loading: false, + error: null, + albumData: null, + filterData: null, + recentData: null + }); + if (value === 'recent') await this.handleRecentClick(); + }; + + handleFilterSearch = async filters => { + this.setState({ + loading: true, + error: null + }); + + let params = { + token: this.props.googlePhotosAuth.access_token.toString(), + filters: filters + }; + + try { + const filterData = await getContent(params); + + if (this.state.view !== 'search') return; + + if (filterData.nextPageToken) filterData.filters = filters; + + this.setState({ + filterData: filterData, + loading: false + }); + } catch (error) { + console.log('filter search error:', error); + this.setState({ + error: error, + loading: false + }); + } + }; + + handleFilterSearchNextPage = async () => { + const { filterData } = this.state; + const filters = filterData.filters; + + this.setState({ + loading: true, + error: null + }); + + let params = { + token: this.props.googlePhotosAuth.access_token.toString(), + filters: filters, + nextPage: filterData.nextPageToken + }; + + try { + const filterData = await getContent(params); + + if (this.state.view !== 'search') return; + + if (filterData.nextPageToken) filterData.filters = filters; + + this.setState({ + filterData: filterData, + loading: false + }); + } catch (error) { + console.log('filter search error:', error); + this.setState({ + error: error, + loading: false + }); + } + }; + + /*TO DO add the posibility tu return to before page later*/ + // managePages = (nextPage) => { + // const {pageMananger} = this.state; + // if(nextPage){ + // if(pageMananger.page + 1 >= pageMananger.pagesStored) return this.handleAlbumItemClick({getNextPage: true}) + // this.sliceAlbumData() + // } + + // } + + // sliceAlbumData = () => { + // const {albumData, pageMananger} = this.state; + // const PAGE_SIZE = 26 + // const pageContent = albumData.mediaItems.slice(pageMananger.page, pageMananger.page + PAGE_SIZE) + // return pageContent; + // } + + handleAlbumItemClick = async albumItemData => { + let params = { + token: this.props.googlePhotosAuth.access_token.toString(), + id: albumItemData.albumId?.toString() + }; + this.setState({ + loading: true, + error: null + }); + try { + const albumData = await getContent(params); + + if (this.state.view !== 'albums') return; + + if (albumData.nextPageToken) albumData.albumId = albumItemData.albumId; + this.setState({ + albumData: albumData, + loading: false + }); + } catch (error) { + console.log('getAlbumContent error:', error); + this.setState({ + error: error, //here is posible set an AlbumItem flag action in HandleTryAgain + loading: false + }); + } + }; + + handleAlbumNextPage = async () => { + const { albumData } = this.state; + const albumId = albumData.albumId; + + this.setState({ + loading: true, + error: null + }); + + let params = { + token: this.props.googlePhotosAuth.access_token.toString(), + id: albumId, + nextPage: albumData.nextPageToken + }; + + try { + const albumData = await getContent(params); + + if (this.state.view !== 'albums') return; + + if (albumData.nextPageToken) albumData.albumId = albumId; + + this.setState({ + albumData: albumData, + loading: false + }); + } catch (error) { + console.log('album Next page error:', error); + this.setState({ + error: error, + loading: false + }); + } + }; + + handleRecentClick = async (nextPage = false) => { + this.setState({ + loading: true, + error: null + }); + + const params = { + token: this.props.googlePhotosAuth.access_token.toString() + }; + + if (nextPage) params.nextPage = this.state.recentData?.nextPageToken; + + try { + const recentData = await getContent(params); + if (this.state.view !== 'recent') return; + + this.setState({ + recentData: recentData, + loading: false + }); + } catch (error) { + console.log('recent data error:', error); + this.setState({ + error: error, + loading: false + }); + } + }; + + onBackGallery = () => { + this.setState({ + albumData: null, + filterData: null + }); + }; + + handlePhotoSelected = async imageData => { + const { onChange, onClose, user } = this.props; + // Loggedin user? + if (user) { + this.setState({ + error: null, + loading: true + }); + try { + const imageUrl = await API.uploadFromUrl(imageData); + onChange(imageUrl); + this.setState({ + loading: false + }); + onClose(); + return; + } catch (error) { + this.setState({ + error: error, + loading: false + }); + console.log(error); + return; + } + } + console.log( + 'you need to be loged on cboard to upload photos from Gooogle Photos' + ); + }; + + handleClose = () => { + const { onClose } = this.props; + this.setState({ + albumData: null, + filterData: null + }); + onClose(); + }; + + handletryAgainClick = async view => { + if (view === 'albums') { + await this.gotAlbums(); + return; + } + await this.handleRecentClick(); + }; + + renderAlbumsList = () => { + return this.state.albumsList.albums.map(el => { + return ( + + await this.handleAlbumItemClick({ albumId: el.id }) + } + key={el.id} + > + + + + + + ); + }); + }; + + componentDidMount = async () => { + this.setState({ + albumsList: null, + loading: true + }); + + await this.authTokenVerify(); + }; + + componentDidUpdate(prevProps) { + if (this.props.googlePhotosAuth !== prevProps.googlePhotosAuth) { + if (this.props.googlePhotosAuth) this.gotAlbums(); + } + } + + render() { + const { open, googlePhotosCode, googlePhotosAuth, intl } = this.props; + const { + albumData, + filterData, + recentData, + albumsList, + loading, + error, + view + } = this.state; + + const getViewData = (view, data) => { + if (view === 'albums') return data.albumData; + if (view === 'search') return data.filterData; + if (view === 'recent') return data.recentData; + }; + + const handleNextPageClick = view => { + if (view === 'albums') return this.handleAlbumNextPage(); + if (view === 'search') return this.handleFilterSearchNextPage(); + if (view === 'recent') return this.handleRecentClick(true); + }; + + const viewData = getViewData(view, { + albumData, + filterData, + recentData + }); + + const buttons = ( + + ); + return ( +
    + + + + {error && ( + { + await this.handletryAgainClick(view); + }} + aria-label={intl.formatMessage(messages.tryAgain)} + > + {intl.formatMessage(messages.tryAgain)} + + ) + } + > + {intl.formatMessage(messages.error)} + + )} + {googlePhotosAuth && ( + <> +
    + {view === 'search' && ( + + )} + {loading && ( +
    + +
    + )} + {!loading && ( +
    + {viewData && ( + <> + {(!viewData.mediaItems || + viewData.mediaItems?.length < 1) && ( + + {intl.formatMessage(messages.noImagesError)} + {/* This alert showed if next page only contains videos or there arn't images for the selected filter */} + + )} + {viewData.mediaItems && ( + + )} + {viewData.nextPageToken && ( +
    + +
    + )} + {view === 'albums' && ( + + + + )} + + )} + {!viewData && view === 'albums' && ( + <> +
    + {!albumsList?.albums && ( + + {intl.formatMessage(messages.noAlbumsError)} + + )} + {albumsList !== null && albumsList.albums && ( + {this.renderAlbumsList()} + )} +
    + + )} +
    + )} +
    + + )} + {!googlePhotosAuth && googlePhotosCode && ( +
    + +
    + )} +
    + + } + /> + } + /> + } + /> + +
    +
    +
    +
    +
    + ); + } +} + +const mapStateToProps = ({ app: { userData } }) => { + const googlePhotosAuth = userData.googlePhotosAuth; + return { + googlePhotosAuth: googlePhotosAuth, + user: userData.email ? userData : null + }; +}; + +const mapDispatchToProps = { logInGooglePhotosAuth, logOutGooglePhotos }; + +export default connect( + mapStateToProps, + mapDispatchToProps +)(injectIntl(GooglePhotosSearch)); diff --git a/src/components/Board/GooglePhotosSearch/GooglePhotosSearch.css b/src/components/Board/GooglePhotosSearch/GooglePhotosSearch.css new file mode 100644 index 000000000..10cb82b68 --- /dev/null +++ b/src/components/Board/GooglePhotosSearch/GooglePhotosSearch.css @@ -0,0 +1,42 @@ +:root { + --Bottom-navigation-height: 56px; +} + +.fullHeight { + min-height: calc(100vh - 64px); /*remember avoid scroll*/ +} + +.navigation { + position: fixed; + bottom: 0px; + left: 0px; + width: 100%; +} + +.loading_container { + display: flex !important; + justify-content: center; + margin-top: 15px; +} + +.gallery_container { + display: flex; + flex-direction: column; + margin-bottom: calc(var(--Bottom-navigation-height) + 14px); +} + +.manage_pages { + display: flex; + margin-top: 4px; +} + +.next_page_btn { + position: relative; + margin-left: auto !important; +} + +.float_button { + position: fixed !important; + bottom: calc(var(--Bottom-navigation-height) + 8px); + left: 3%; +} diff --git a/src/components/Board/GooglePhotosSearch/GooglePhotosSearch.messages.js b/src/components/Board/GooglePhotosSearch/GooglePhotosSearch.messages.js new file mode 100644 index 000000000..84fcc9e99 --- /dev/null +++ b/src/components/Board/GooglePhotosSearch/GooglePhotosSearch.messages.js @@ -0,0 +1,48 @@ +import { defineMessages } from 'react-intl'; + +export default defineMessages({ + addFrom: { + id: 'cboard.components.Board.TileEditor.GooglePhotosSearch.addFrom', + defaultMessage: 'Add from' + }, + disconnect: { + id: 'cboard.components.Board.TileEditor.GooglePhotosSearch.disconnect', + defaultMessage: 'Disconect Google Photos' + }, + error: { + id: 'cboard.components.Board.TileEditor.googlePhotosSearch.error', + defaultMessage: 'Sorry an error ocurred. Try it again' + }, + noAlbumsError: { + id: 'cboard.components.Board.TileEditor.googlePhotosSearch.noAlbumsError', + defaultMessage: "You don't have albums in your Google Photos acount." + }, + noImagesError: { + id: 'cboard.components.Board.TileEditor.googlePhotosSearch.noImagesError', + defaultMessage: 'No images availables.' + }, + albums: { + id: 'cboard.components.Board.TileEditor.GooglePhotosSearch.albums', + defaultMessage: 'Albums' + }, + search: { + id: 'cboard.components.Board.TileEditor.googlePhotosSearch.search', + defaultMessage: 'Search' + }, + recent: { + id: 'cboard.components.Board.TileEditor.googlePhotosSearch.recent', + defaultMessage: 'Recent' + }, + nextPage: { + id: 'cboard.components.Board.TileEditor.googlePhotosSearch.nextPage', + defaultMessage: 'Next page' + }, + backToAlbumList: { + id: 'cboard.components.Board.TileEditor.googlePhotosSearch.backToAlbumList', + defaultMessage: 'Go back to album list view' + }, + tryAgain: { + id: 'cboard.components.Board.TileEditor.googlePhotosSearch.tryAgain', + defaultMessage: 'Try again' + } +}); diff --git a/src/components/Board/GooglePhotosSearch/GooglePhotosSearchGallery/GooglePhotosSearchGallery.component.js b/src/components/Board/GooglePhotosSearch/GooglePhotosSearchGallery/GooglePhotosSearchGallery.component.js new file mode 100644 index 000000000..e0b7bbf2f --- /dev/null +++ b/src/components/Board/GooglePhotosSearch/GooglePhotosSearchGallery/GooglePhotosSearchGallery.component.js @@ -0,0 +1,47 @@ +import * as React from 'react'; +import { ImageList, ImageListItem } from '@material-ui/core'; //insta MUI dependency to avoid white space + +import useMediaQuery from '@material-ui/core/useMediaQuery'; + +import './GooglePhotosSearchGallery.css'; + +const getCols = proportionData => { + const proportion = proportionData.width / proportionData.height; + if (proportion <= 1.2) return 1; + return 2; +}; + +const GooglePhotosSearchGallery = props => { + const bigScreen = useMediaQuery('(min-width:600px)'); + + return ( +
    + + {props.imagesData.map(tile => ( + { + props.onSelect(tile.baseUrl); + }} + key={tile.id} + cols={getCols(tile.mediaMetadata)} + > + {tile.filename} + + ))} + +
    + ); +}; + +export default GooglePhotosSearchGallery; diff --git a/src/components/Board/GooglePhotosSearch/GooglePhotosSearchGallery/GooglePhotosSearchGallery.css b/src/components/Board/GooglePhotosSearch/GooglePhotosSearchGallery/GooglePhotosSearchGallery.css new file mode 100644 index 000000000..412c44140 --- /dev/null +++ b/src/components/Board/GooglePhotosSearch/GooglePhotosSearchGallery/GooglePhotosSearchGallery.css @@ -0,0 +1,12 @@ +.root { + display: 'flex'; + flex-wrap: 'wrap'; + justify-content: 'space-around'; + overflow: 'hidden'; + /*background-color: theme.palette.background.paper;*/ +} + +.gridList { + width: 500; + height: 450; +} diff --git a/src/components/Board/GooglePhotosSearch/GooglePhotosSearchGallery/index.js b/src/components/Board/GooglePhotosSearch/GooglePhotosSearchGallery/index.js new file mode 100644 index 000000000..6f207f04b --- /dev/null +++ b/src/components/Board/GooglePhotosSearch/GooglePhotosSearchGallery/index.js @@ -0,0 +1 @@ +export { default } from './GooglePhotosSearchGallery.component'; diff --git a/src/components/Board/GooglePhotosSearch/index.js b/src/components/Board/GooglePhotosSearch/index.js new file mode 100644 index 000000000..214a4c31b --- /dev/null +++ b/src/components/Board/GooglePhotosSearch/index.js @@ -0,0 +1 @@ +export { default } from './GooglePhotosSearch.container'; diff --git a/src/components/Board/TileEditor/TileEditor.component.js b/src/components/Board/TileEditor/TileEditor.component.js index 9b80cdec0..6ada21f52 100644 --- a/src/components/Board/TileEditor/TileEditor.component.js +++ b/src/components/Board/TileEditor/TileEditor.component.js @@ -29,7 +29,22 @@ import InputImage from '../../UI/InputImage'; import IconButton from '../../UI/IconButton'; import ColorSelect from '../../UI/ColorSelect'; import VoiceRecorder from '../../VoiceRecorder'; + +import GooglePhotosSearch from '../GooglePhotosSearch/GooglePhotosSearch.container'; +import ConnectToGooglePhotosButton from '../GooglePhotosSearch/GooglePhotosButton'; + +import { connect } from 'react-redux'; +import { + updateEditingTiles, + editingTilesNextStep, + editingTilesPrevStep +} from '../Board.actions'; + +import { showNotification } from '../../Notifications/Notifications.actions'; + import './TileEditor.css'; +import { isCordova } from '../../../cordova-util'; +import { API_URL } from '../../../constants'; export class TileEditor extends Component { static propTypes = { @@ -48,7 +63,7 @@ export class TileEditor extends Component { /** * Tiles array to work on */ - editingTiles: PropTypes.array, + editingTiles: PropTypes.object, /** * Callback fired when submitting edited board tiles */ @@ -57,11 +72,26 @@ export class TileEditor extends Component { * Callback fired when submitting a new board tile */ onAddSubmit: PropTypes.func.isRequired, - boards: PropTypes.array - }; - - static defaultProps = { - editingTiles: [] + boards: PropTypes.array, + /** + * code to exchange for google photos auth + */ + googlePhotosCode: PropTypes.string, + /** + * To verify if user is logged on google Photos + */ + googlePhotosAuth: PropTypes.object, + /** + * callback to delete googlephotosCode after exchange it + */ + onExchangeCode: PropTypes.func, + /** + * if true. user is logged + */ + isLoggedUser: PropTypes.bool, + updateEditingTiles: PropTypes.func, + editingTilesNextStep: PropTypes.func, + editingTilesPrevStep: PropTypes.func }; constructor(props) { @@ -86,9 +116,8 @@ export class TileEditor extends Component { }; this.state = { - activeStep: 0, - editingTiles: props.editingTiles, isSymbolSearchOpen: false, + isGooglePhotosSearchOpen: false, selectedBackgroundColor: '', tile: this.defaultTile, linkedBoard: '' @@ -96,12 +125,13 @@ export class TileEditor extends Component { } UNSAFE_componentWillReceiveProps(props) { - this.updateTileProperty('id', shortid.generate()); // todo not here - this.setState({ editingTiles: props.editingTiles }); + if (!this.editingTile()) this.updateTileProperty('id', shortid.generate()); // todo not here } editingTile() { - return this.state.editingTiles[this.state.activeStep]; + return this.props.editingTiles?.editingTiles[ + this.props.editingTiles.activeEditStep + ]; } currentTileProp(prop) { @@ -110,12 +140,8 @@ export class TileEditor extends Component { } updateEditingTile(id, property, value) { - return state => { - const editingTiles = state.editingTiles.map(b => - b.id === id ? { ...b, ...{ [property]: value } } : b - ); - return { ...state, editingTiles }; - }; + const { updateEditingTiles } = this.props; + updateEditingTiles(id, property, value); } updateNewTile(property, value) { @@ -127,9 +153,7 @@ export class TileEditor extends Component { updateTileProperty(property, value) { if (this.editingTile()) { - this.setState( - this.updateEditingTile(this.editingTile().id, property, value) - ); + this.updateEditingTile(this.editingTile().id, property, value); } else { this.setState(this.updateNewTile(property, value)); } @@ -145,7 +169,7 @@ export class TileEditor extends Component { }); if (this.editingTile()) { - onEditSubmit(this.state.editingTiles); + onEditSubmit(this.props.editingTiles.editingTiles); } else { const tileToAdd = this.state.tile; const selectedBackgroundColor = this.state.selectedBackgroundColor; @@ -182,6 +206,28 @@ export class TileEditor extends Component { this.setState({ isSymbolSearchOpen: false }); }; + handleGooglePhotosSearchClick = event => { + const { isLoggedUser, intl, googlePhotosAuth } = this.props; + if (!isLoggedUser) { + this.props.showNotification(intl.formatMessage(messages.unlogedMessage)); + return; + } + if (!googlePhotosAuth) { + window.location = `${API_URL}/auth/google-photos`; + return; + } + this.setState({ isGooglePhotosSearchOpen: true }); + }; + + handleGooglePhotosSearchChange = image => { + this.updateTileProperty('image', image); + }; + + handleGooglePhotosSearchClose = event => { + this.props.onExchangeCode(); + this.setState({ isGooglePhotosSearchOpen: false }); + }; + handleLabelChange = event => { this.updateTileProperty('label', event.target.value); this.updateTileProperty('labelKey', ''); @@ -216,12 +262,12 @@ export class TileEditor extends Component { }; handleBack = event => { - this.setState({ activeStep: this.state.activeStep - 1 }); + this.props.editingTilesPrevStep(); this.setState({ selectedBackgroundColor: '', linkedBoard: '' }); }; handleNext = event => { - this.setState({ activeStep: this.state.activeStep + 1 }); + this.props.editingTilesNextStep(); this.setState({ selectedBackgroundColor: '', linkedBoard: '' }); }; @@ -263,7 +309,7 @@ export class TileEditor extends Component { }; render() { - const { open, intl, boards } = this.props; + const { open, intl, boards, googlePhotosCode } = this.props; const currentLabel = this.currentTileProp('labelKey') ? intl.formatMessage({ id: this.currentTileProp('labelKey') }) @@ -351,6 +397,14 @@ export class TileEditor extends Component { > {intl.formatMessage(messages.symbols)} + {!isCordova() && ( +
    + +
    + )}
    @@ -445,18 +499,18 @@ export class TileEditor extends Component { - {this.state.editingTiles.length > 1 && ( + {this.props.editingTiles.editingTiles.length > 1 && ( {intl.formatMessage(messages.next)}{' '} @@ -466,7 +520,7 @@ export class TileEditor extends Component { backButton={