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 = (
+ }
+ onClick={this.logOutGooglePhotosBtn}
+ >
+ {intl.formatMessage(messages.disconnect)}
+
+ );
+ 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)}
+ >
+
+
+ ))}
+
+
+ );
+};
+
+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={