diff --git a/README.md b/README.md index 41b3cce62..193f0bbec 100644 --- a/README.md +++ b/README.md @@ -69,8 +69,8 @@ _Customized themes are synced with local session storage_ - Instant loading with [Service Workers](https://developers.google.com/web/fundamentals/primers/service-workers) - Offline support - Low RAM/memory and CPU usage - - [Add to Home Screen](https://developers.google.com/web/fundamentals/app-install-banners) (button in footer) - - [Desktop PWA](https://developers.google.com/web/progressive-web-apps/desktop) support (button in footer) + - Add to Home Screen + - Desktop PWA - ([full features](https://developers.google.com/web/progressive-web-apps)) 🚀 **Request**: Retrieve response from endpoint instantly. @@ -220,11 +220,13 @@ _**All `i18n` contributions are welcome to `i18n` [branch](https://github.com/li - **[Proxy β](https://github.com/postwoman-io/postwoman-proxy)** - A simple proxy server created for Postwoman - **[CLI β](https://github.com/postwoman-io/postwoman-cli)** - A CLI solution for Postwoman - - **[Browser Extensions](https://github.com/AndrewBastin/postwoman-firefox)** - Browser extensions that simplifies access to Postwoman + - **Browser Extensions** - Browser extensions that simplifies access to Postwoman - > [ **Firefox**](https://addons.mozilla.org/en-US/firefox/addon/postwoman) |  **Chrome (coming soon)** + [ **Firefox**](https://addons.mozilla.org/en-US/firefox/addon/postwoman) ([GitHub](https://github.com/AndrewBastin/postwoman-firefox)) | [ **Chrome**](https://chrome.google.com/webstore/detail/postwoman-extension-for-c/amknoiejhlmhancpahfcfcfhllgkpbld) ([GitHub](https://github.com/AndrewBastin/postwoman-chrome)) -_Add-ons are developed and maintained under **[Official Postwoman Organization](https://github.com/postwoman-io)**_ + >**Extensions fixes `CORS` issues.** + +_Add-ons are developed and maintained under **[Official Postwoman Organization](https://github.com/postwoman-io)**._ **To find out more, please check out [Postwoman Wiki](https://github.com/liyasthomas/postwoman/wiki).** diff --git a/assets/css/fonts.scss b/assets/css/fonts.scss index 313b4b58a..0c714f199 100644 --- a/assets/css/fonts.scss +++ b/assets/css/fonts.scss @@ -27,6 +27,7 @@ -webkit-font-feature-settings: "liga"; -webkit-font-smoothing: antialiased; font-feature-settings: "liga"; + border-radius: 50%; } /* poppins-500 - latin */ diff --git a/assets/css/styles.scss b/assets/css/styles.scss index 48b6130c3..574f57c94 100644 --- a/assets/css/styles.scss +++ b/assets/css/styles.scss @@ -698,6 +698,7 @@ ol li { .show-on-large-screen { display: inline-flex; + flex: 1; } .info-response { @@ -852,10 +853,7 @@ input[type="radio"]:checked + label + .tab { } @media (max-width: $responsiveWidth) { - .content { - flex-flow: column; - } - + .content, .columns { flex-flow: column; } diff --git a/components/collections/addCollection.vue b/components/collections/addCollection.vue index 2789a52d7..c39b7b257 100644 --- a/components/collections/addCollection.vue +++ b/components/collections/addCollection.vue @@ -56,6 +56,10 @@ export default { }, methods: { addNewCollection() { + if (!this.$data.name) { + this.$toast.info('Please provide a valid name for the collection') + return; + } this.$store.commit("postwoman/addNewCollection", { name: this.$data.name }); diff --git a/components/collections/editCollection.vue b/components/collections/editCollection.vue index 4021557fe..98c9154be 100644 --- a/components/collections/editCollection.vue +++ b/components/collections/editCollection.vue @@ -59,6 +59,10 @@ export default { }, methods: { saveCollection() { + if (!this.$data.name) { + this.$toast.info('Please provide a valid name for the collection'); + return; + } const collectionUpdated = { ...this.$props.editingCollection, name: this.$data.name diff --git a/components/firebase/feeds.vue b/components/firebase/feeds.vue new file mode 100644 index 000000000..547a8cecd --- /dev/null +++ b/components/firebase/feeds.vue @@ -0,0 +1,85 @@ + + + + + + + + + get_app + + + delete + + + + + + + {{ $t("empty") }} + + + + + + + diff --git a/components/firebase/inputform.vue b/components/firebase/inputform.vue new file mode 100644 index 000000000..0eb0a9e62 --- /dev/null +++ b/components/firebase/inputform.vue @@ -0,0 +1,55 @@ + + + + + + + + + + + + + add + Add + + + + + + diff --git a/components/firebase/login.vue b/components/firebase/login.vue new file mode 100644 index 000000000..0f06d87b2 --- /dev/null +++ b/components/firebase/login.vue @@ -0,0 +1,115 @@ + + + + vpn_key + + + + + + + + Google + + + + + + + + GitHub + + + + + + + diff --git a/components/graphql/queryeditor.vue b/components/graphql/queryeditor.vue new file mode 100644 index 000000000..7f1084b85 --- /dev/null +++ b/components/graphql/queryeditor.vue @@ -0,0 +1,136 @@ + + + + + diff --git a/components/history.vue b/components/history.vue index 289f5586b..c769aa30c 100644 --- a/components/history.vue +++ b/components/history.vue @@ -21,7 +21,7 @@ import { findStatusGroup } from "../pages/index"; +import { fb } from "../functions/fb"; const updateOnLocalStorage = (propertyName, property) => window.localStorage.setItem(propertyName, JSON.stringify(property)); @@ -341,11 +342,11 @@ export default { VirtualList: () => import("vue-virtual-scroll-list") }, data() { - const localStorageHistory = JSON.parse( - window.localStorage.getItem("history") - ); return { - history: localStorageHistory || [], + history: + fb.currentUser !== null + ? fb.currentHistory + : JSON.parse(window.localStorage.getItem("history")) || [], filterText: "", showFilter: false, isClearingHistory: false, @@ -359,6 +360,10 @@ export default { }, computed: { filteredHistory() { + this.history = + fb.currentUser !== null + ? fb.currentHistory + : JSON.parse(window.localStorage.getItem("history")) || []; return this.history.filter(entry => { const filterText = this.filterText.toLowerCase(); return Object.keys(entry).some(key => { @@ -371,6 +376,9 @@ export default { }, methods: { clearHistory() { + if (fb.currentUser !== null) { + fb.clearHistory(); + } this.history = []; this.filterText = ""; this.disableHistoryClearing(); @@ -391,6 +399,9 @@ export default { ); }, deleteHistory(entry) { + if (fb.currentUser !== null) { + fb.deleteHistory(entry); + } this.history.splice(this.history.indexOf(entry), 1); if (this.history.length === 0) { this.filterText = ""; @@ -497,8 +508,11 @@ export default { toggleCollapse() { this.showMore = !this.showMore; }, - toggleStar(index) { - this.history[index]["star"] = !this.history[index]["star"]; + toggleStar(entry) { + if (fb.currentUser !== null) { + fb.toggleStar(entry, !entry.star); + } + entry.star = !entry.star; updateOnLocalStorage("history", this.history); } } diff --git a/firestore.rules b/firestore.rules index 31eda1745..ce0a1b949 100644 --- a/firestore.rules +++ b/firestore.rules @@ -1,7 +1,14 @@ service cloud.firestore { match /databases/{database}/documents { match /{document=**} { - allow read, write; + allow read, write: if request.auth.uid != null; + } + // Make sure the uid of the requesting user matches name of the user + // document. The wildcard expression {userId} makes the userId variable + // available in rules. + match /users/{userId} { + allow read, update, delete: if request.auth.uid == userId; + allow create: if request.auth.uid != null; } } } diff --git a/functions/fb.js b/functions/fb.js new file mode 100644 index 000000000..698b9150e --- /dev/null +++ b/functions/fb.js @@ -0,0 +1,173 @@ +import firebase from "firebase/app"; +import "firebase/firestore"; +import "firebase/auth"; + +// Initialize Firebase, copied from cloud console +const firebaseConfig = { + apiKey: "AIzaSyCMsFreESs58-hRxTtiqQrIcimh4i1wbsM", + authDomain: "postwoman-api.firebaseapp.com", + databaseURL: "https://postwoman-api.firebaseio.com", + projectId: "postwoman-api", + storageBucket: "postwoman-api.appspot.com", + messagingSenderId: "421993993223", + appId: "1:421993993223:web:ec0baa8ee8c02ffa1fc6a2", + measurementId: "G-ERJ6025CEB" +}; +firebase.initializeApp(firebaseConfig); + +// a reference to the users collection +const usersCollection = firebase.firestore().collection("users"); + +// the shared state object that any vue component +// can get access to +export const fb = { + currentUser: {}, + currentFeeds: [], + currentSettings: [], + currentHistory: [], + writeFeeds: async (message, label) => { + const dt = { + createdOn: new Date(), + author: fb.currentUser.uid, + author_name: fb.currentUser.displayName, + author_image: fb.currentUser.photoURL, + message, + label + }; + try { + return usersCollection + .doc(fb.currentUser.uid) + .collection("feeds") + .add(dt); + } catch (e) { + return console.error("error inserting", dt, e); + } + }, + deleteFeed: id => { + usersCollection + .doc(fb.currentUser.uid) + .collection("feeds") + .doc(id) + .delete() + .catch(e => console.error("error deleting", id, e)); + }, + writeSettings: async (setting, value) => { + const st = { + updatedOn: new Date(), + author: fb.currentUser.uid, + author_name: fb.currentUser.displayName, + author_image: fb.currentUser.photoURL, + name: setting, + value + }; + try { + return usersCollection + .doc(fb.currentUser.uid) + .collection("settings") + .doc(setting) + .set(st); + } catch (e) { + return console.error("error updating", st, e); + } + }, + writeHistory: async entry => { + const hs = entry; + try { + return usersCollection + .doc(fb.currentUser.uid) + .collection("history") + .add(hs); + } catch (e) { + return console.error("error inserting", hs, e); + } + }, + deleteHistory: entry => { + usersCollection + .doc(fb.currentUser.uid) + .collection("history") + .doc(entry.id) + .delete() + .catch(e => console.error("error deleting", entry, e)); + }, + clearHistory: () => { + usersCollection + .doc(fb.currentUser.uid) + .collection("history") + .get() + .then(({ docs }) => { + docs.forEach(e => fb.deleteHistory(e)); + }); + }, + toggleStar: (entry, value) => { + usersCollection + .doc(fb.currentUser.uid) + .collection("history") + .doc(entry.id) + .update({ star: value }) + .catch(e => console.error("error deleting", entry, e)); + } +}; + +// When a user logs in or out, save that in the store +firebase.auth().onAuthStateChanged(user => { + if (user) { + fb.currentUser = user; + fb.currentUser.providerData.forEach(profile => { + let us = { + updatedOn: new Date(), + provider: profile.providerId, + name: profile.displayName, + email: profile.email, + photoUrl: profile.photoURL, + uid: profile.uid + }; + try { + usersCollection.doc(fb.currentUser.uid).set(us); + } catch (e) { + console.error("error updating", us, e); + } + }); + + usersCollection + .doc(fb.currentUser.uid) + .collection("feeds") + .orderBy("createdOn", "desc") + .onSnapshot(feedsRef => { + const feeds = []; + feedsRef.forEach(doc => { + const feed = doc.data(); + feed.id = doc.id; + feeds.push(feed); + }); + fb.currentFeeds = feeds; + }); + + usersCollection + .doc(fb.currentUser.uid) + .collection("settings") + .onSnapshot(settingsRef => { + const settings = []; + settingsRef.forEach(doc => { + const setting = doc.data(); + setting.id = doc.id; + settings.push(setting); + }); + fb.currentSettings = settings; + }); + + usersCollection + .doc(fb.currentUser.uid) + .collection("history") + .onSnapshot(historyRef => { + const history = []; + historyRef.forEach(doc => { + const entry = doc.data(); + entry.id = doc.id; + history.push(entry); + }); + fb.currentHistory = history; + }); + } else { + fb.currentUser = null; + } +}); diff --git a/functions/network.js b/functions/network.js index 7ed233cf8..f56c52676 100644 --- a/functions/network.js +++ b/functions/network.js @@ -1,19 +1,19 @@ import AxiosStrategy from "./strategies/AxiosStrategy"; -import ProxyStrategy from "./strategies/ProxyStrategy"; import FirefoxStrategy from "./strategies/FirefoxStrategy"; - +import ChromeStrategy, { hasChromeExtensionInstalled } from "./strategies/ChromeStrategy"; const runAppropriateStrategy = (req, store) => { + // Chrome Provides a chrome object for scripts to access + // Check its availability to say whether you are in Google Chrome + if (window.chrome && hasChromeExtensionInstalled()) { + return ChromeStrategy(req, store); + } // The firefox plugin injects a function to send requests through it // If that is available, then we can use the FirefoxStrategy if (window.firefoxExtSendRequest) { return FirefoxStrategy(req, store); } - if (store.state.postwoman.settings.PROXY_ENABLED) { - return ProxyStrategy(req, store); - } - return AxiosStrategy(req, store); } diff --git a/functions/strategies/AxiosStrategy.js b/functions/strategies/AxiosStrategy.js index 9bef30aef..4b3be7263 100644 --- a/functions/strategies/AxiosStrategy.js +++ b/functions/strategies/AxiosStrategy.js @@ -1,8 +1,23 @@ import axios from "axios"; -const axiosStrategy = async (req, _store) => { +const axiosWithProxy = async (req, { state }) => { + const { data } = await axios.post( + state.postwoman.settings.PROXY_URL || "https://postwoman.apollotv.xyz/", + req + ); + return data; +}; + +const axiosWithoutProxy = async (req, _store) => { const res = await axios(req); return res; }; +const axiosStrategy = (req, store) => { + if (store.state.postwoman.settings.PROXY_ENABLED) { + return axiosWithProxy(req, store); + } + return axiosWithoutProxy(req, store); +}; + export default axiosStrategy; diff --git a/functions/strategies/ChromeStrategy.js b/functions/strategies/ChromeStrategy.js new file mode 100644 index 000000000..03c05ea0c --- /dev/null +++ b/functions/strategies/ChromeStrategy.js @@ -0,0 +1,56 @@ +const EXTENSION_ID = "amknoiejhlmhancpahfcfcfhllgkpbld"; + +// Check if the Chrome Extension is present +// The Chrome extension injects an empty span to help detection. +// Also check for the presence of window.chrome object to confirm smooth operations +export const hasChromeExtensionInstalled = () => { + return document.getElementById("chromePWExtensionDetect") !== null; +} + +const chromeWithoutProxy = (req, _store) => new Promise((resolve, reject) => { + chrome.runtime.sendMessage( + EXTENSION_ID, { + messageType: "send-req", + data: { + config: req + } + }, (message) => { + if (message.data.error) { + reject(message.data.error); + } else { + resolve(message.data.response); + } + } + ); +}); + +const chromeWithProxy = (req, { state }) => new Promise((resolve, reject) => { + chrome.runtime.sendMessage( + EXTENSION_ID, { + messageType: "send-req", + data: { + config: { + method: "post", + url: state.postwoman.settings.PROXY_URL || "https://postwoman.apollotv.xyz/", + data: req + } + } + }, (message) => { + if (message.data.error) { + reject(error); + } else { + resolve(message.data.response.data); + } + } + ) +}); + +const chromeStrategy = (req, store) => { + if (store.state.postwoman.settings.PROXY_ENABLED) { + return chromeWithProxy(req, store); + } else { + return chromeWithoutProxy(req, store); + } +} + +export default chromeStrategy; diff --git a/functions/strategies/FirefoxStrategy.js b/functions/strategies/FirefoxStrategy.js index 77b15f453..9160a27d3 100644 --- a/functions/strategies/FirefoxStrategy.js +++ b/functions/strategies/FirefoxStrategy.js @@ -1,15 +1,50 @@ -const firefoxStrategy = (req, _store) => new Promise((resolve, reject) => { +const firefoxWithProxy = (req, { state }) => + new Promise((resolve, reject) => { + const eventListener = event => { + window.removeEventListener("firefoxExtSendRequestComplete", event); - const eventListener = (event) => { - window.removeEventListener("firefoxExtSendRequestComplete", eventListener); + if (event.detail.error) { + reject(JSON.parse(event.detail.error)); + } else { + resolve(JSON.parse(event.detail.response).data); + } + }; - if (event.detail.error) reject(JSON.parse(event.detail.error)); - else resolve(JSON.parse(event.detail.response)); - }; + window.addEventListener("firefoxExtSendRequestComplete", eventListener); - window.addEventListener("firefoxExtSendRequestComplete", eventListener); + window.firefoxExtSendRequest({ + method: "post", + url: + state.postwoman.settings.PROXY_URL || "https://postwoman.apollotv.xyz/", + data: req + }); + }); - window.firefoxExtSendRequest(req); -}); +const firefoxWithoutProxy = (req, _store) => + new Promise((resolve, reject) => { + const eventListener = ({ detail }) => { + window.removeEventListener( + "firefoxExtSendRequestComplete", + eventListener + ); + + if (detail.error) { + reject(JSON.parse(detail.error)); + } else { + resolve(JSON.parse(detail.response)); + } + }; + + window.addEventListener("firefoxExtSendRequestComplete", eventListener); + + window.firefoxExtSendRequest(req); + }); + +const firefoxStrategy = (req, store) => { + if (store.state.postwoman.settings.PROXY_ENABLED) { + return firefoxWithProxy(req, store); + } + return firefoxWithoutProxy(req, store); +}; export default firefoxStrategy; diff --git a/functions/strategies/ProxyStrategy.js b/functions/strategies/ProxyStrategy.js deleted file mode 100644 index 0b6f50b0f..000000000 --- a/functions/strategies/ProxyStrategy.js +++ /dev/null @@ -1,12 +0,0 @@ -import axios from "axios"; - -const proxyStrategy = async (req, store) => { - const { data } = await axios.post( - store.state.postwoman.settings.PROXY_URL || - "https://postwoman.apollotv.xyz/", - req - ); - return data; -}; - -export default proxyStrategy; diff --git a/functions/utils/debounce.js b/functions/utils/debounce.js new file mode 100644 index 000000000..09acda07d --- /dev/null +++ b/functions/utils/debounce.js @@ -0,0 +1,15 @@ +// Debounce is a higher order function which makes its enclosed function be executed +// only if the function wasn't called again till 'delay' time has passed, this helps reduce impact of heavy working +// functions which might be called frequently +// NOTE : Don't use lambda functions as this doesn't get bound properly in them, use the 'function (args) {}' format +const debounce = (func, delay) => { + let inDebounce + return function() { + const context = this + const args = arguments + clearTimeout(inDebounce) + inDebounce = setTimeout(() => func.apply(context, args), delay) + } +} + +export default debounce; diff --git a/lang/en-US.js b/lang/en-US.js index 906fb0602..2d63fd63a 100644 --- a/lang/en-US.js +++ b/lang/en-US.js @@ -249,5 +249,15 @@ export default { extensions: "Extensions", extensions_info1: "Browser extension that simplifies access to Postwoman", extensions_info2: "Get Postwoman browser extension!", - installed: "Installed" + installed: "Installed", + login_with: "Login with", + logged_out: "Logged out", + logout: "Logout", + account: "Account", + sync: "Sync", + syncHistory: "History", + syncCollections: "Collections", + turn_on: "Turn on", + login_first: "Login first", + paste_a_collection: "Paste a Collection" }; diff --git a/layouts/default.vue b/layouts/default.vue index 447e58ae4..15626a247 100644 --- a/layouts/default.vue +++ b/layouts/default.vue @@ -237,6 +237,11 @@ + + + person + + brush @@ -288,6 +293,46 @@ > offline_bolt + + + + + + account_circle + + + + + + settings + + {{ $t("settings") }} + + + + + + + exit_to_app + {{ $t("logout") }} + + + + + more_vert @@ -352,15 +397,7 @@ - - 🦄 - - - + Powered by Netlify + + + + 🦄 + + - - - - - Chrome (coming soon) - - done - - + + + + + + Chrome + + done + + + @@ -588,16 +648,31 @@ - + diff --git a/pages/settings.vue b/pages/settings.vue index 8e0d851a1..0bba79853 100644 --- a/pages/settings.vue +++ b/pages/settings.vue @@ -1,5 +1,88 @@ + + + + + + + account_circle + + {{ fb.currentUser.displayName || "Name not found" }} + + + + + email + + {{ fb.currentUser.email || "Email not found" }} + + + + + exit_to_app + {{ $t("logout") }} + + + + + {{ $t(setting.name) + " " + $t("sync") }} + {{ setting.value ? $t("enabled") : $t("disabled") }} + + + + + sync + {{ $t("turn_on") + " " + $t("sync") }} + + + + + {{ $t("login_with") }} + + + + + + Google + + + + + + + GitHub + + + + + + + @@ -137,6 +220,9 @@
+ + {{ $t(setting.name) + " " + $t("sync") }} + {{ setting.value ? $t("enabled") : $t("disabled") }} + +
+ + sync + {{ $t("turn_on") + " " + $t("sync") }} + +
+ + + + + Google + + + + + + + GitHub + +