├── .babelrc ├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .flowconfig ├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── database-rules.json ├── firebase.json ├── flow-typed ├── firebaseTypes.js └── types.js ├── frontend ├── App.jsx ├── ConditionalRedirect.jsx ├── README.md ├── Routes.jsx ├── actions.jsx ├── asyncActions.jsx ├── components │ ├── About.jsx │ ├── DrawerContent.jsx │ ├── FourOhFour.jsx │ ├── HomeFeed.jsx │ ├── Layout.jsx │ ├── MoreMenu.jsx │ ├── NavigationBar.jsx │ ├── NewPost.jsx │ ├── RecentPostsFeed.jsx │ ├── SearchBar.jsx │ ├── SinglePost.jsx │ ├── SplashPage.jsx │ ├── UserProfile.jsx │ ├── UserStatus.jsx │ ├── app.global.css │ ├── firebaseui-overrides.global.css │ └── splash-page.css ├── firebaseTools.jsx └── reducers.jsx ├── index.js ├── microservices ├── README.md ├── blurOffensiveImages.js ├── firebase-express-middleware.js ├── index.html ├── renderTemplate.js └── sendFollowerNotification.js ├── package-lock.json ├── package.json ├── public ├── firebase-messaging-sw.js ├── images │ ├── favicon.png │ ├── silhouette.jpg │ └── touch │ │ ├── touch-icon-120x120.png │ │ ├── touch-icon-128x128.png │ │ ├── touch-icon-144x144.png │ │ ├── touch-icon-180x180.png │ │ └── touch-icon-192x192.png ├── manifest.json └── old_scripts │ ├── auth.js │ ├── feed.js │ ├── firebase.js │ ├── messaging.js │ ├── post.js │ ├── routing.js │ ├── search.js │ ├── uploader.js │ ├── userpage.js │ └── utils.js ├── storage.rules ├── webpack.config.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "react", 4 | ["env", { 5 | "targets": { "node": "6.11" }, 6 | "loose": true, 7 | "modules": "commonjs" 8 | }] 9 | ], 10 | "plugins": [ 11 | "transform-decorators", 12 | "transform-class-properties", 13 | "transform-object-rest-spread", 14 | ["css-modules-transform", {"generateScopedName": "[name]__[local]___[hash:base64:5]"}] 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | public/ 2 | node_modules/ 3 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "plugin:flowtype/recommended", 4 | "google", 5 | "prettier", 6 | "prettier/react", 7 | "plugin:react/recommended" 8 | ], 9 | "plugins": [ 10 | "flowtype", 11 | "prettier" 12 | ], 13 | "parser": "babel-eslint", 14 | "parserOptions": { 15 | "ecmaVersion": 2016, 16 | "sourceType": "module", 17 | "ecmaFeatures": { 18 | "jsx": true 19 | } 20 | }, 21 | "env": { 22 | "es6": true, 23 | "browser": true, 24 | "node": true, 25 | "jest": true 26 | }, 27 | "globals": { 28 | "firebaseUi": true, 29 | "uiConfig": true, 30 | "jQuery": true, 31 | "swal": true, 32 | "latinize": true, 33 | "componentHandler": true, 34 | "friendlyPix": true, 35 | "page": true, 36 | "ga": true, 37 | "firebase": true, 38 | "gapi": true 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | /node_modules/styled-components/* 3 | 4 | [include] 5 | 6 | [libs] 7 | 8 | [options] 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /*.pyc 2 | /node_modules 3 | /firebase-debug.log 4 | /microservices/service-account-credentials.json 5 | /frontend/firebase-config.json 6 | /.firebaserc 7 | /public/bundle.js 8 | *.map 9 | frontend/actionCreators.js 10 | frontend/actions.js 11 | frontend/asyncActions.js 12 | frontend/ClientApp.js 13 | frontend/reducers.js 14 | frontend/Routes.js 15 | frontend/ServerApp.js 16 | /public/bundle.css 17 | /public/fonts 18 | frontend/App.js 19 | frontend/firebaseTools.js 20 | frontend/components/About.js 21 | frontend/components/FirebaseAuth.js 22 | frontend/components/HomeFeed.js 23 | frontend/components/Layout.js 24 | frontend/components/NewPost.js 25 | frontend/components/RecentPostsFeed.js 26 | frontend/components/SinglePost.js 27 | frontend/components/SplashPage.js 28 | UserProfile.js 29 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Friendly Pix 2 | 3 | We'd love for you to contribute to our source code and to make the Friendly Pix even better than it is today! Here are the guidelines we'd like you to follow: 4 | 5 | - [Code of Conduct](#coc) 6 | - [Question or Problem?](#question) 7 | - [Issues and Bugs](#issue) 8 | - [Feature Requests](#feature) 9 | - [Submission Guidelines](#submit) 10 | - [Coding Rules](#rules) 11 | - [Signing the CLA](#cla) 12 | 13 | ## Code of Conduct 14 | 15 | As contributors and maintainers of the Friendly Pix project, we pledge to respect everyone who contributes by posting issues, updating documentation, submitting pull requests, providing feedback in comments, and any other activities. 16 | 17 | Communication through any of Friendly Pix's channels (GitHub, StackOverflow, Google+, Twitter, etc.) must be constructive and never resort to personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct. 18 | 19 | We promise to extend courtesy and respect to everyone involved in this project regardless of gender, gender identity, sexual orientation, disability, age, race, ethnicity, religion, or level of experience. We expect anyone contributing to the project to do the same. 20 | 21 | If any member of the community violates this code of conduct, the maintainers of the Friendly Pix project may take action, removing issues, comments, and PRs or blocking accounts as deemed appropriate. 22 | 23 | If you are subject to or witness unacceptable behavior, or have any other concerns, please drop us a line at nivco@google.com. 24 | 25 | ## Got a Question or Problem? 26 | 27 | If you have technical questions about Friendly Pix, please direct these to [StackOverflow][stackoverflow] and use the `friendly-pix` tag. We are also available on GitHub issues. 28 | 29 | ## Found an Issue? 30 | If you find a bug in the source code or a mistake in the documentation, you can help us by 31 | submitting an issue to our [GitHub Repository][github]. Even better you can submit a Pull Request 32 | with a fix. 33 | 34 | See [below](#submit) for some guidelines. 35 | 36 | ## Want a Feature? 37 | You can request a new feature by submitting an issue to our [GitHub Repository][github]. 38 | 39 | If you would like to implement a new feature then consider what kind of change it is: 40 | 41 | * **Major Changes** that you wish to contribute to the project should be discussed first on our 42 | [issue tracker][github] so that we can better coordinate our efforts, prevent 43 | duplication of work, and help you to craft the change so that it is successfully accepted into the 44 | project. 45 | * **Small Changes** can be crafted and submitted to the [GitHub Repository][github] as a Pull Request directly. 46 | 47 | ## Submission Guidelines 48 | 49 | ### Submitting an Issue 50 | Before you submit your issue search the archive, maybe your question was already answered. 51 | 52 | If your issue appears to be a bug, and hasn't been reported, open a new issue. 53 | Help us to maximize the effort we can spend fixing issues and adding new 54 | features, by not reporting duplicate issues. Providing the following information will increase the 55 | chances of your issue being dealt with quickly: 56 | 57 | * **Overview of the Issue** - if an error is being thrown a non-minified stack trace helps 58 | * **Motivation for or Use Case** - explain why this is a bug for you 59 | * **Friendly Pix Version(s)** - is it a regression? 60 | * **Browsers and Operating System** - is this a problem with all browsers or only some browsers? 61 | * **Reproduce the Error** - provide an unambiguous set of steps. 62 | * **Related Issues** - has a similar issue been reported before? 63 | * **Suggest a Fix** - if you can't fix the bug yourself, perhaps you can point to what might be 64 | causing the problem (line of code or commit) 65 | 66 | **If you get help, help others. Good karma rulez!** 67 | 68 | Here's a template to get you started: 69 | 70 | ``` 71 | Browser: 72 | Browser version: 73 | Operating system: 74 | Operating system version: 75 | URL, if applicable: 76 | 77 | What steps will reproduce the problem: 78 | 1. 79 | 2. 80 | 3. 81 | 82 | What is the expected result? 83 | 84 | What happens instead of that? 85 | 86 | Please provide any other information below, and attach a screenshot if possible. 87 | ``` 88 | 89 | ### Submitting a Pull Request 90 | Before you submit your pull request consider the following guidelines: 91 | 92 | * Search [GitHub](https://github.com/firebase/friendlypix/pulls) for an open or closed Pull Request 93 | that relates to your submission. You don't want to duplicate effort. 94 | * Please sign our [Contributor License Agreement (CLA)](#cla) before sending pull 95 | requests. We cannot accept code without this. 96 | * Make your changes in a new git branch: 97 | 98 | ```shell 99 | git checkout -b my-fix-branch master 100 | ``` 101 | 102 | * Create your patch, **including appropriate test cases**. 103 | * Follow our [Coding Rules](#rules). 104 | * Avoid checking in files that shouldn't be tracked (e.g `node_modules`, `gulp-cache`, `.tmp`, `.idea`). We recommend using a [global](#global-gitignore) gitignore for this. 105 | * Make sure **not** to include a recompiled version of the files as part of your PR. We will generate these automatically. 106 | * Commit your changes using a descriptive commit message. 107 | 108 | ```shell 109 | git commit -a 110 | ``` 111 | Note: the optional commit `-a` command line option will automatically "add" and "rm" edited files. 112 | 113 | * Build your changes locally to ensure all the tests pass: 114 | 115 | ```shell 116 | gulp 117 | ``` 118 | 119 | * Push your branch to GitHub: 120 | 121 | ```shell 122 | git push origin my-fix-branch 123 | ``` 124 | 125 | * In GitHub, send a pull request to `friendlypix:master`. 126 | * If we suggest changes then: 127 | * Make the required updates. 128 | * Re-run the Friendly Pix test suite to ensure tests are still passing. 129 | * Rebase your branch and force push to your GitHub repository (this will update your Pull Request): 130 | 131 | ```shell 132 | git rebase master -i 133 | git push origin my-fix-branch -f 134 | ``` 135 | 136 | That's it! Thank you for your contribution! 137 | 138 | #### After your pull request is merged 139 | 140 | After your pull request is merged, you can safely delete your branch and pull the changes 141 | from the main (upstream) repository: 142 | 143 | * Delete the remote branch on GitHub either through the GitHub web UI or your local shell as follows: 144 | 145 | ```shell 146 | git push origin --delete my-fix-branch 147 | ``` 148 | 149 | * Check out the master branch: 150 | 151 | ```shell 152 | git checkout master -f 153 | ``` 154 | 155 | * Delete the local branch: 156 | 157 | ```shell 158 | git branch -D my-fix-branch 159 | ``` 160 | 161 | * Update your master with the latest upstream version: 162 | 163 | ```shell 164 | git pull --ff upstream master 165 | ``` 166 | 167 | ## Coding Rules 168 | 169 | We generally follow the [Google JavaScript style guide][js-style-guide] for the Web version. 170 | 171 | ## Signing the CLA 172 | 173 | Please sign our [Contributor License Agreement][google-cla] (CLA) before sending pull requests. For any code 174 | changes to be accepted, the CLA must be signed. It's a quick process, we promise! 175 | 176 | *This guide was inspired by the [AngularJS contribution guidelines](https://github.com/angular/angular.js/blob/master/CONTRIBUTING.md).* 177 | 178 | [github]: https://github.com/firebase/friendlypix 179 | [google-cla]: https://cla.developers.google.com 180 | [js-style-guide]: http://google.github.io/styleguide/javascriptguide.xml 181 | [jsbin]: http://jsbin.com/ 182 | [stackoverflow]: http://stackoverflow.com/questions/tagged/friendly-pix 183 | [global-gitignore]: https://help.github.com/articles/ignoring-files/#create-a-global-gitignore 184 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright 2015 Google Inc 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | 204 | All code in any directories or sub-directories that end with *.html or 205 | *.css is licensed under the Creative Commons Attribution International 206 | 4.0 License, which full text can be found here: 207 | https://creativecommons.org/licenses/by/4.0/legalcode. 208 | 209 | As an exception to this license, all html or css that is generated by 210 | the software at the direction of the user is copyright the user. The 211 | user has full ownership and control over such content, including 212 | whether and how they wish to license it. 213 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Friendly Pix Web - React edition 2 | 3 | Friendly Pix Web is a sample app demonstrating how to build a isomorphic React Web app with the Firebase Platform. 4 | 5 | Friendly Pix is a place where you can share photos, follow friends, comment on photos... 6 | 7 | The following React techs are being used: 8 | - Isomorphism with `ReactDOMServer` hosted on Cloud Functions 9 | - React Router v4 10 | - CSS Modules 11 | - Redux 12 | - Firebase UI 13 | - react-redux-firebase v2 14 | 15 | 16 | ## Initial setup, build tools and dependencies 17 | 18 | Friendly Pix is built using Javascript, [Firebase](https://firebase.google.com/docs/web/setup) and [React](https://facebook.github.io/react/). The Auth flow is built using [Firebase-UI](https://github.com/firebase/firebaseui-web). 19 | 20 | FriendlyPix is an Isomorphic app, the first render of the app is generated server side using [Cloud Functions for Firebase](https://firebase.google.com/docs/functions). 21 | 22 | Additionally server-side micro-services are built on [Cloud Functions for Firebase](https://firebase.google.com/docs/functions) such as an automatic image moderation and notifications sending. 23 | 24 | Install all JavaScript/Build/Deploy tools dependencies by running: 25 | 26 | ```bash 27 | $ npm install 28 | ``` 29 | 30 | 31 | ## Create and configure your Firebase Project 32 | 33 | 1. Create a Firebase project using the [Firebase Console](https://firebase.google.com/console). 34 | 2. Generate a Service accounts file from **⚙ > Project Settings > Service Accounts > GENERATE NEW PRIVATE KEY > GENERATE KEY** and save it as `./microservices/service-account-credentials.json` 35 | 2. Enable **Google** as a Sign in provider in **Firebase Console > Authentication > Sign in Method** tab. 36 | 3. Enable **Facebook** as a Sign in provider in **Firebase Console > Authentication > Sign in Method** tab. You'll need to provide your Facebook app's credentials. If you haven't yet you'll need to have created a Facebook app on [Facebook for Developers](https://developers.facebook.com) 37 | 4. At the root of the site run `firebase use --add`. When prompted select the Firebase Project you have just created. This will make sure the Firebase CLI is configured to use your particular project. 38 | 39 | 40 | ## Start a local development server 41 | 42 | You can start a local development server by running: 43 | 44 | ```bash 45 | npm run serve 46 | ``` 47 | 48 | This will start `firebase serve` and make sure your Javascript files are transpiled and packed automatically. 49 | 50 | Then open [http://localhost:5000](http://localhost:5000) in your browser. 51 | 52 | 53 | ## Deploy the app 54 | 55 | Before deploying your code you need to build it for production. Run: 56 | 57 | ```bash 58 | npm run build 59 | ``` 60 | 61 | This will install all runtime dependencies and transpile and pack Javascript code to ES5, install Cloud Functions dependencies. 62 | Then run: 63 | 64 | ```bash 65 | firebase deploy 66 | ``` 67 | 68 | Then this deploys a new version of your code that will be served from `https://.firebaseapp.com` 69 | 70 | 71 | ## Contributing 72 | 73 | We'd love that you contribute to the project. Before doing so please read our [Contributor guide](CONTRIBUTING.md). 74 | 75 | 76 | ## License 77 | 78 | © Google, 2011. Licensed under an [Apache-2](LICENSE) license. 79 | -------------------------------------------------------------------------------- /database-rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "feed": { 4 | "$uid": { 5 | ".read": "auth.uid === $uid", 6 | ".write": "auth.uid === $uid", 7 | "$postId": { 8 | ".validate": "newData.val() === true && newData.parent().parent().parent().child('posts').child($postId).exists()" 9 | } 10 | } 11 | }, 12 | "posts": { 13 | ".read": true, 14 | "$postId": { 15 | ".write": "!data.exists() || data.exists() && auth.uid === data.child('author').child('uid').val()", // Allow new writes and allow updates and deletes to own posts. 16 | "author": { 17 | "uid": { 18 | ".validate": "auth.uid === newData.val()" 19 | } 20 | } 21 | } 22 | }, 23 | "comments": { 24 | ".read": true, 25 | "$postId": { 26 | ".write": "!newData.exists() && auth.uid === root.child('posts').child($postId).child('author').child('uid').val() && !newData.parent().parent().child('posts').child($postId).exists()", // Allow deletes from the post owner 27 | ".validate": "root.child('posts').child($postId).exists()", // Check that the post exists 28 | "$commentId": { 29 | ".write": "!data.exists() || data.exists() && auth.uid === data.child('author').child('uid').val()", // Can write new comments and edit/delete particular comment if you are the author. 30 | "author": { 31 | "uid": { 32 | ".validate": "auth.uid === newData.val()" 33 | } 34 | } 35 | } 36 | } 37 | }, 38 | "likes": { 39 | ".read": true, 40 | "$postId": { 41 | ".write": "!newData.exists() && auth.uid === root.child('posts').child($postId).child('author').child('uid').val() && !newData.parent().parent().child('posts').child($postId).exists()", // Allow deletes from the post owner 42 | ".validate": "root.child('posts').child($postId).exists()", // Check that the post exists 43 | "$uid": { 44 | ".write": "auth.uid === $uid", 45 | ".validate": "newData.val() === now" 46 | } 47 | } 48 | }, 49 | "followers": { 50 | ".read": true, 51 | "$followedUid": { 52 | "$followerUid": { 53 | ".write": "auth.uid === $followerUid", // Can only add yourself as a follower 54 | ".validate": "newData.val() === true && newData.parent().parent().parent().child('people').child($followerUid).child('following').child($followedUid).exists()" // Makes sure /people/.../following is in sync 55 | } 56 | } 57 | }, 58 | "people": { 59 | ".indexOn": ["_search_index/full_name", "_search_index/reversed_full_name"], 60 | ".read": true, 61 | "$uid": { 62 | ".write": "auth.uid === $uid", 63 | "full_name": { 64 | ".validate": "newData.isString()" 65 | }, 66 | "profile_picture": { 67 | ".validate": "newData.isString()" 68 | }, 69 | "posts": { 70 | "$postId": { 71 | ".validate": "newData.val() === true && newData.parent().parent().parent().parent().child('posts').child($postId).exists()" 72 | } 73 | }, 74 | "_search_index": { 75 | "full_name": { 76 | ".validate": "newData.isString()" 77 | }, 78 | "reversed_full_name": { 79 | ".validate": "newData.isString()" 80 | } 81 | }, 82 | "following": { 83 | "$followedUid": { 84 | ".validate": "newData.parent().parent().parent().parent().child('followers').child($followedUid).child($uid).val() === true" // Makes sure /followers is in sync 85 | } 86 | } 87 | } 88 | }, 89 | "$other": { 90 | ".validate": false 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "database": { 3 | "rules": "database-rules.json" 4 | }, 5 | "storage": { 6 | "rules": "storage.rules" 7 | }, 8 | "functions": { 9 | "source": "./", 10 | "exclude": ["public", "node_modules"] 11 | }, 12 | "hosting": { 13 | "public": "public", 14 | "rewrites": [ 15 | { 16 | "source": "**", 17 | "function": "renderTemplate" 18 | } 19 | ] 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /flow-typed/firebaseTypes.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for t`he specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | // @flow 17 | 18 | declare type FirebaseActionType = 'SET_CURRENT_USER' | 'SET_AUTH_READY' | 'SET_ID_TOKEN' | 'SET_FIREBASE_INSTANCE'; 19 | 20 | declare type FirebaseActionT = {| 21 | type: A, 22 | payload: P 23 | |}; 24 | 25 | export type FirebaseAction = FirebaseActionT<'SET_CURRENT_USER', Object> 26 | | FirebaseActionT<'SET_AUTH_READY', boolean> 27 | | FirebaseActionT<'SET_ID_TOKEN', string> 28 | | FirebaseActionT<'SET_FIREBASE_INSTANCE', Object>; 29 | -------------------------------------------------------------------------------- /flow-typed/types.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for t`he specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | // @flow 17 | 18 | declare type ActionType = 'SET_PROFILE_SEARCH_TERM' | 'SET_FIREBASE_CUSTOM_AUTH_TOKEN'; 19 | 20 | declare type ActionT = {| 21 | type: A, 22 | payload: P 23 | |}; 24 | 25 | export type Action = ActionT<'SET_PROFILE_SEARCH_TERM', string> | ActionT<'SET_FIREBASE_CUSTOM_AUTH_TOKEN', string>; 26 | -------------------------------------------------------------------------------- /frontend/App.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | // @flow 17 | 18 | // React core. 19 | import React from 'react'; 20 | import ReactDOM from 'react-dom'; 21 | 22 | // Firebase. 23 | import firebase from 'firebase/app'; 24 | import 'firebase/auth'; 25 | 26 | // Redux. 27 | import { Provider } from 'react-redux'; 28 | import { createStore, compose, combineReducers, applyMiddleware } from 'redux'; 29 | import thunk from 'redux-thunk'; 30 | import { reactReduxFirebase, getFirebase, firebaseStateReducer } from 'react-redux-firebase'; 31 | 32 | // Router. 33 | import { createBrowserHistory } from 'history'; 34 | import { ConnectedRouter, routerReducer, routerMiddleware } from 'react-router-redux'; 35 | 36 | // JSS. 37 | import { SheetsRegistry } from 'react-jss/lib/jss'; 38 | import JssProvider from 'react-jss/lib/JssProvider'; 39 | import { create } from 'jss'; 40 | import preset from 'jss-preset-default'; 41 | 42 | // Theme. 43 | import { MuiThemeProvider, createMuiTheme } from 'material-ui/styles'; 44 | import createGenerateClassName from 'material-ui/styles/createGenerateClassName'; 45 | import { lightBlue, amber } from 'material-ui/colors'; 46 | 47 | // Other. 48 | import { canUseDOM } from 'exenv'; 49 | 50 | // Local. 51 | import { keepIdTokenInCookie } from './firebaseTools'; 52 | import * as reducers from './reducers'; 53 | import Routes from './Routes'; 54 | 55 | /** 56 | * Loads the App. 57 | * 58 | * This takes care of setting up JSS, the Theme, Redux and the Router. 59 | */ 60 | export class App extends React.Component { 61 | 62 | /** 63 | * @inheritDoc 64 | */ 65 | constructor(props) { 66 | super(props); 67 | 68 | // Create a theme instance. 69 | this.theme = createMuiTheme({ 70 | palette: { 71 | primary: lightBlue, 72 | secondary: amber, 73 | type: 'light', 74 | }, 75 | }); 76 | 77 | // Configure JSS 78 | this.jss = create(preset()); 79 | this.jss.options.createGenerateClassName = createGenerateClassName; 80 | } 81 | 82 | /** 83 | * Properties types. 84 | */ 85 | props: { 86 | store: Object, 87 | history: Object, 88 | registry: Object 89 | }; 90 | 91 | /** 92 | * @inheritDoc 93 | */ 94 | componentDidMount() { 95 | // Remove the server-side injected CSS. 96 | const jssStyles = document.getElementById('jss-server-side'); 97 | if (jssStyles && jssStyles.parentNode) { 98 | jssStyles.parentNode.removeChild(jssStyles); 99 | } 100 | } 101 | 102 | /** 103 | * @inheritDoc 104 | */ 105 | render() { 106 | return ( 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | ); 117 | } 118 | } 119 | 120 | /** 121 | * Create a redux store. 122 | * 123 | * @param {Object} history - The History manager to use. 124 | * @param {Object} firebaseApp - The Firebase App instance to use. 125 | * @param {Object} initialState - The initial state of the Redux store. 126 | * @return {Object} - The store. 127 | */ 128 | export function makeStore(history, firebaseApp, initialState = {}) { 129 | const historyMiddleware = routerMiddleware(history); 130 | 131 | return createStore( 132 | combineReducers({ 133 | ...reducers, 134 | router: routerReducer, 135 | firebaseState: firebaseStateReducer 136 | }), 137 | initialState, 138 | compose( 139 | applyMiddleware(thunk.withExtraArgument(getFirebase)), 140 | applyMiddleware(historyMiddleware), 141 | reactReduxFirebase(firebaseApp, { 142 | enableRedirectHandling: false, 143 | attachAuthIsReady: true, 144 | firebaseStateName: 'firebaseState', 145 | authIsReady: () => Promise.resolve() // Remove when prescottprue/redux-firebase#2 is fixed. 146 | }) 147 | )); 148 | } 149 | 150 | /** 151 | * Create a Jss Registry. 152 | * 153 | * @return {Object} - The Jss Registry. 154 | */ 155 | export function makeRegistry() { 156 | return new SheetsRegistry(); 157 | } 158 | 159 | // On the client, display the app right away. 160 | if (canUseDOM) { 161 | // Get the Firebase config from the auto generated file. 162 | const firebaseConfig = require('./firebase-config.json').result; 163 | 164 | // Instantiate a Firebase app. 165 | const firebaseApp = firebase.initializeApp(firebaseConfig); 166 | 167 | // Keep the Firebase ID Token and the __session cookie in sync. 168 | keepIdTokenInCookie(firebaseApp, '__session'); 169 | 170 | const registry = makeRegistry(); 171 | const history = createBrowserHistory(); 172 | const store = makeStore(history, firebaseApp, window.__REDUX_STATE__); 173 | 174 | store.subscribe(() => { 175 | const firebaseState = store.getState().firebaseState; 176 | console.log('state: ',firebaseState.auth); 177 | }); 178 | 179 | // When Firebase Auth is ready we'll display the app. 180 | store.firebaseAuthIsReady.then(() => { 181 | // Render the app. 182 | ReactDOM.render(, document.getElementById('app')); 183 | }); 184 | } 185 | -------------------------------------------------------------------------------- /frontend/ConditionalRedirect.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | // @flow 17 | 18 | import React from 'react'; 19 | import { connect } from 'react-redux'; 20 | import { push } from 'react-router-redux'; 21 | 22 | /** 23 | * A react-router-redux compatible Redirect with additional conditional. 24 | */ 25 | class ConditionalRedirect extends React.Component { 26 | 27 | /** 28 | * Properties types. 29 | */ 30 | props: { 31 | from: string, 32 | to?: string | Object, 33 | exact?: boolean, 34 | redirect?: Function 35 | }; 36 | 37 | /** 38 | * @inheritDoc 39 | */ 40 | componentWillReceiveProps(props) { 41 | this.onRouting(props); 42 | } 43 | 44 | /** 45 | * @inheritDoc 46 | */ 47 | componentWillMount() { 48 | this.onRouting(this.props); 49 | } 50 | 51 | /** 52 | * Executing every time new routing should occur. 53 | * 54 | * @param {Object} props 55 | */ 56 | onRouting(props) { 57 | if (props.if) { 58 | this.props.redirect(props.to); 59 | } 60 | } 61 | 62 | /** 63 | * @inheritDoc 64 | */ 65 | render() { 66 | return null; 67 | } 68 | } 69 | 70 | const mapDispatchToProps = (dispatch: Function) => ({ 71 | redirect(to) { 72 | dispatch(push(to)); 73 | } 74 | }); 75 | 76 | export default connect(state => state, mapDispatchToProps)(ConditionalRedirect); 77 | -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | All the Frontend code. 2 | -------------------------------------------------------------------------------- /frontend/Routes.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | // @flow 17 | 18 | import React from 'react'; 19 | import { Route, Switch } from 'react-router'; 20 | import FriendlyPixLayout from './components/Layout'; 21 | import { connect } from 'react-redux'; 22 | import ConditionalRedirect from './ConditionalRedirect'; 23 | import SplashPage from './components/SplashPage'; 24 | import HomeFeed from './components/HomeFeed'; 25 | import RecentPostsFeed from './components/RecentPostsFeed'; 26 | import SinglePost from './components/SinglePost'; 27 | import About from './components/About'; 28 | import NewPost from './components/NewPost'; 29 | import UserProfile from './components/UserProfile'; 30 | import FourOhFour from './components/FourOhFour'; 31 | 32 | /** 33 | * All the routes. 34 | */ 35 | class Routes extends React.Component { 36 | 37 | /** 38 | * Properties types. 39 | */ 40 | props: { 41 | firebaseState: { 42 | auth: { 43 | isEmpty: boolean 44 | } 45 | } 46 | }; 47 | 48 | /** 49 | * @inheritDoc 50 | */ 51 | render() { 52 | const isAuth = !this.props.firebaseState.auth.isEmpty; 53 | 54 | return ( 55 |
56 | {/* Redirects */} 57 | 58 | 59 | 60 | 61 | {/* Content */} 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 |
79 | ) 80 | } 81 | } 82 | 83 | export default connect(state => state)(Routes); 84 | -------------------------------------------------------------------------------- /frontend/actions.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | // @flow 17 | 18 | export const SET_PROFILE_SEARCH_TERM = 'SET_PROFILE_SEARCH_TERM'; 19 | 20 | 21 | /** 22 | * Sets the profile search term. 23 | * 24 | * @param {string} searchTerm - The new search term to set. 25 | * @return {{type, payload: string}} 26 | */ 27 | export function setProfileSearchTerm(searchTerm: string) { 28 | return { type: SET_PROFILE_SEARCH_TERM, payload: searchTerm }; 29 | } 30 | -------------------------------------------------------------------------------- /frontend/asyncActions.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | // @flow 17 | 18 | import axios from 'axios'; 19 | import { addAPIData } from './actionCreators'; 20 | 21 | /** 22 | * Fetches the details of the movie with the given IMDB ID. 23 | * 24 | * @param {string} imdbID - The IMDB ID of the movie. 25 | * @return {function(Function)} 26 | */ 27 | export default function getAPIDetails(imdbID: string) { 28 | return (dispatch: Function) => { 29 | axios 30 | .get(`http://localhost:3000/${imdbID}`) 31 | .then(response => { 32 | dispatch(addAPIData(response.data)); 33 | }) 34 | .catch(error => { 35 | console.error('axios error', error); // eslint-disable-line no-console 36 | }); 37 | }; 38 | } 39 | -------------------------------------------------------------------------------- /frontend/components/About.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | // @flow 17 | 18 | import React from 'react'; 19 | import { connect } from 'react-redux'; 20 | import 'firebase/auth'; 21 | 22 | /** 23 | * Entry point to the FriendlyPix app. 24 | */ 25 | class About extends React.Component { 26 | 27 | /** 28 | * Constructor for the FriendlyPix app. 29 | * 30 | * @param {Object} props - Additional object properties. 31 | * @constructor 32 | */ 33 | constructor(props) { 34 | super(props); 35 | } 36 | 37 | /** 38 | * Properties types. 39 | */ 40 | props: { 41 | }; 42 | 43 | /** 44 | * @inheritDoc 45 | */ 46 | render() { 47 | return ( 48 |
49 |

ABOUT

50 |
51 | ) 52 | } 53 | } 54 | 55 | const mapStateToProps = (state) => { 56 | return state; 57 | }; 58 | 59 | const mapDispatchToProps = (dispatch: Function, ownProps) => ({ 60 | }); 61 | 62 | export default connect(mapStateToProps, mapDispatchToProps)(About); 63 | -------------------------------------------------------------------------------- /frontend/components/DrawerContent.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | // @flow 17 | 18 | import React from 'react'; 19 | import { connect } from 'react-redux'; 20 | import { Link } from 'react-router-dom'; 21 | import Avatar from 'material-ui/Avatar'; 22 | import { withStyles } from 'material-ui/styles'; 23 | import List, { ListItem, ListItemText } from 'material-ui/List'; 24 | import Divider from 'material-ui/Divider'; 25 | import UserStatus from './UserStatus'; 26 | import { compose } from 'redux'; 27 | import { firebaseConnect } from 'react-redux-firebase'; 28 | import TrendingUpIcon from 'material-ui-icons/TrendingUp'; 29 | import HomeIcon from 'material-ui-icons/Home'; 30 | import PermContactCalendarIcon from 'material-ui-icons/PermContactCalendar'; 31 | import ExitToAppIcon from 'material-ui-icons/ExitToApp'; 32 | import { lightBlue, common } from 'material-ui/colors'; 33 | 34 | const styles = { 35 | signInContainer: { 36 | backgroundColor: lightBlue['700'], 37 | color: common.black, 38 | marginTop: '-10px' 39 | }, 40 | noStyleLink: { 41 | textDecoration: 'none', 42 | }, 43 | button: { 44 | color: common.white 45 | }, 46 | avatar: { 47 | marginRight: '-20px', 48 | borderRadius: '20px', 49 | border: '2px white solid', 50 | zIndex: 3, 51 | width: '38px', 52 | height: '38px' 53 | }, 54 | label: { 55 | color: common.white, 56 | textTransform: 'none', 57 | fontSize: '16px' 58 | } 59 | }; 60 | 61 | /** 62 | * The Dropdown Menu. 63 | */ 64 | class DrawerContent extends React.Component { 65 | 66 | /** 67 | * Properties types. 68 | */ 69 | props: { 70 | firebase: { 71 | logout: Function 72 | }, 73 | isSignedIn: boolean, 74 | classes: Object 75 | }; 76 | 77 | /** 78 | * Logs out the user from Firebase. 79 | */ 80 | logout() { 81 | this.props.firebase.logout(); 82 | } 83 | 84 | /** 85 | * @inheritDoc 86 | */ 87 | render() { 88 | const classes = this.props.classes; 89 | 90 | return ( 91 | 92 | 93 | 94 | 95 | 96 | {this.props.isSignedIn && 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | } 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | this.logout()}> 125 | 126 | 127 | 128 | 129 | 130 | 131 | ); 132 | } 133 | } 134 | const mapStateToProps = state => ({ 135 | isSignedIn: !state.firebaseState.auth.isEmpty 136 | }); 137 | 138 | 139 | export default compose(withStyles(styles), firebaseConnect(), connect(mapStateToProps))(DrawerContent); 140 | -------------------------------------------------------------------------------- /frontend/components/FourOhFour.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | // @flow 17 | 18 | import React from 'react'; 19 | 20 | /** 21 | * Entry point to the FriendlyPix app. 22 | */ 23 | export default class FourOhFour extends React.Component { 24 | 25 | /** 26 | * @inheritDoc 27 | */ 28 | render() { 29 | return ( 30 |
31 |

ERROR 404: Not Found

32 |
33 | ) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /frontend/components/HomeFeed.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | // @flow 17 | 18 | import React from 'react'; 19 | import { connect } from 'react-redux'; 20 | import 'firebase/auth'; 21 | 22 | /** 23 | * Entry point to the Home Feed. 24 | */ 25 | class HomeFeed extends React.Component { 26 | 27 | /** 28 | * Constructor for the Home Feed. 29 | * 30 | * @param {Object} props - Additional object properties. 31 | * @constructor 32 | */ 33 | constructor(props) { 34 | super(props); 35 | } 36 | 37 | /** 38 | * Properties types. 39 | */ 40 | props: { 41 | }; 42 | 43 | /** 44 | * @inheritDoc 45 | */ 46 | render() { 47 | return ( 48 |
49 |

HOME FEED

50 |
51 | ) 52 | } 53 | } 54 | 55 | const mapStateToProps = state => { 56 | return state; 57 | }; 58 | 59 | const mapDispatchToProps = (dispatch: Function, ownProps) => ({ 60 | }); 61 | 62 | export default connect(mapStateToProps, mapDispatchToProps)(HomeFeed); 63 | -------------------------------------------------------------------------------- /frontend/components/Layout.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | // @flow 17 | 18 | import React from 'react'; 19 | import { connect } from 'react-redux'; 20 | import { Link } from 'react-router-dom'; 21 | import Button from 'material-ui/Button'; 22 | import DrawerContent from './DrawerContent'; 23 | import { withStyles } from 'material-ui/styles'; 24 | import Hidden from 'material-ui/Hidden'; 25 | import AppBar from 'material-ui/AppBar'; 26 | import Toolbar from 'material-ui/Toolbar'; 27 | import IconButton from 'material-ui/IconButton'; 28 | import MenuIcon from 'material-ui-icons/Menu'; 29 | import PhotoIcon from 'material-ui-icons/Photo'; 30 | import Drawer from 'material-ui/Drawer'; 31 | import SearchBar from './SearchBar'; 32 | import { compose } from 'redux'; 33 | import { firebaseConnect } from 'react-redux-firebase'; 34 | import DropdownMenu from './MoreMenu'; 35 | import NavigationBar from './NavigationBar'; 36 | import UserStatus from './UserStatus'; 37 | import './app.global.css'; 38 | import { lightBlue, common } from 'material-ui/colors'; 39 | 40 | 41 | const styles = theme => ({ 42 | appBar: { 43 | backgroundColor: lightBlue['700'] 44 | }, 45 | sideBarButton: { 46 | color: common.white 47 | }, 48 | spacer: { 49 | flexGrow: '1' 50 | }, 51 | toolBar: { 52 | margin: 'auto', 53 | maxWidth: '1024px', 54 | padding: '10px 20px', 55 | width: '100%', 56 | justifyContent: 'space-between', 57 | boxSizing: 'border-box', 58 | minHeight: '90px', 59 | [theme.breakpoints.down('md')]: { 60 | minHeight: '60px', 61 | padding: '5px 0 5px 10px' 62 | }, 63 | [theme.breakpoints.down('sm')]: { 64 | padding: '0' 65 | } 66 | }, 67 | subToolBar: { 68 | padding: '0', 69 | minHeight: '48px' 70 | }, 71 | subAppBar: { 72 | backgroundColor: lightBlue['600'] 73 | }, 74 | logo: { 75 | display: 'flex', 76 | color: common.white, 77 | textDecoration: 'none', 78 | fontSize: '34px', 79 | fontFamily: '"Amaranth", sans-serif', 80 | whiteSpace: 'nowrap', 81 | marginRight: '20px', 82 | [theme.breakpoints.down('md')]: { 83 | fontSize: '28px', 84 | } 85 | }, 86 | logoIcon: { 87 | margin: '5px 7px 0 0', 88 | width: '32px', 89 | height: '32px', 90 | [theme.breakpoints.down('md')]: { 91 | width: '26px', 92 | height: '26px', 93 | marginTop: '4px' 94 | } 95 | }, 96 | takePicButton: { 97 | position: 'fixed', 98 | bottom: '10px', 99 | right: '10px' 100 | }, 101 | uploadPicButton: { 102 | position: 'absolute', 103 | right: '10px', 104 | bottom: '-25px' 105 | }, 106 | mediaCapture: { 107 | display: 'none' 108 | } 109 | }); 110 | 111 | /** 112 | * Entry point to the FriendlyPix app. 113 | */ 114 | class FriendlyPixLayout extends React.Component { 115 | 116 | /** 117 | * Properties types. 118 | */ 119 | props: { 120 | children: any, 121 | firebaseCustomAuthToken: string | void, 122 | firebase: { 123 | logout: Function 124 | }, 125 | classes: Object 126 | }; 127 | 128 | state = { 129 | drawerOpen: false 130 | }; 131 | 132 | /** 133 | * Closes the Drawer. 134 | */ 135 | closeDrawer() { 136 | this.setState({drawerOpen: false}) 137 | } 138 | 139 | /** 140 | * @inheritDoc 141 | */ 142 | render() { 143 | const classes = this.props.classes; 144 | return ( 145 |
146 | {/* Header section containing logo, menus... */} 147 | 148 | 149 | {/* SideBar button */} 150 | 151 | this.setState({drawerOpen: true})}> 155 | 156 | 157 | 158 | 159 | {/* Logo */} 160 | 161 | Friendly Pix 162 | 163 | 164 |
165 | 166 | {/* Search bar */} 167 | 168 | 169 | {/* Signed-in User Info */} 170 | 171 | 172 | 173 | 174 | {/* Drop Down Menu */} 175 | 176 | 177 | 178 | 179 | 180 | 181 |
182 | 183 | {/* Navigation bar */} 184 | 185 | 186 | {/* Floating Take Picture Button */} 187 | 188 | 191 | 192 |
193 |
194 | 195 | {/* Floating Take Picture Button */} 196 | 197 | 200 | 201 | 202 | 203 | 204 | 205 | {/* Drawer menu */} 206 | this.closeDrawer()}> 207 |
this.closeDrawer()}> 208 | 209 |
210 |
211 | 212 |
213 | {this.props.children} 214 |
215 |
216 | ) 217 | } 218 | } 219 | 220 | export default compose(withStyles(styles), firebaseConnect(), connect(state => state))(FriendlyPixLayout); 221 | -------------------------------------------------------------------------------- /frontend/components/MoreMenu.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | // @flow 17 | 18 | import React from 'react'; 19 | import { Link } from 'react-router-dom'; 20 | import IconButton from 'material-ui/IconButton'; 21 | import Avatar from 'material-ui/Avatar'; 22 | import { withStyles } from 'material-ui/styles'; 23 | import MoreVertIcon from 'material-ui-icons/MoreVert'; 24 | import List, { ListItem, ListItemText } from 'material-ui/List'; 25 | import Divider from 'material-ui/Divider'; 26 | import Popover from 'material-ui/Popover'; 27 | import { compose } from 'redux'; 28 | import { firebaseConnect } from 'react-redux-firebase'; 29 | import { common } from 'material-ui/colors'; 30 | import PermContactCalendarIcon from 'material-ui-icons/PermContactCalendar'; 31 | import ExitToAppIcon from 'material-ui-icons/ExitToApp'; 32 | 33 | const styles = { 34 | noStyleLink: { 35 | textDecoration: 'none', 36 | }, 37 | button: { 38 | color: common.white 39 | } 40 | }; 41 | 42 | /** 43 | * The Dropdown Menu. 44 | */ 45 | class DropdownMenu extends React.Component { 46 | 47 | /** 48 | * Properties types. 49 | */ 50 | props: { 51 | firebase: { 52 | logout: Function 53 | }, 54 | classes: Object 55 | }; 56 | 57 | state = { 58 | anchorEl: null, 59 | open: false, 60 | }; 61 | 62 | /** 63 | * Opens the menu next to the given element. 64 | * 65 | * @param {Object} element - The element which to anchor the menu. 66 | */ 67 | openMenu(element) { 68 | this.setState({ open: true, anchorEl: element }); 69 | } 70 | 71 | /** 72 | * Closes the menu. 73 | */ 74 | closeMenu() { 75 | this.setState({ open: false }); 76 | } 77 | 78 | /** 79 | * Log sout the user from Firebase. 80 | */ 81 | logout() { 82 | this.props.firebase.logout(); 83 | } 84 | 85 | /** 86 | * @inheritDoc 87 | */ 88 | render() { 89 | const classes = this.props.classes; 90 | return ( 91 |
92 | this.openMenu(e.currentTarget)}> 98 | 99 | 100 | this.closeMenu()}> 113 | this.closeMenu()}> 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | this.logout()}> 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 |
132 | ) 133 | } 134 | } 135 | 136 | export default compose(withStyles(styles), firebaseConnect())(DropdownMenu); 137 | -------------------------------------------------------------------------------- /frontend/components/NavigationBar.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | // @flow 17 | 18 | import React from 'react'; 19 | import { connect } from 'react-redux'; 20 | import Link from 'react-router-dom/Link'; 21 | import { withStyles } from 'material-ui/styles'; 22 | import TrendingUpIcon from 'material-ui-icons/TrendingUp'; 23 | import Tabs, { Tab } from 'material-ui/Tabs'; 24 | import HomeIcon from 'material-ui-icons/Home'; 25 | import { common } from 'material-ui/colors'; 26 | import { compose } from 'redux'; 27 | 28 | const styles = theme => ({ 29 | tabWrapper: { 30 | flexDirection: 'row', 31 | color: common.white, 32 | [theme.breakpoints.down('md')]: { 33 | padding: '0 4px 0 12px' 34 | } 35 | }, 36 | tabLabel: { 37 | padding: '0 8px' 38 | }, 39 | tabRoot: { 40 | height: '48px' 41 | }, 42 | link: { 43 | textDecoration: 'none' 44 | }, 45 | root: { 46 | marginLeft: '-20px', 47 | '@media (max-width: 1044px)': { 48 | marginLeft: 0 49 | } 50 | } 51 | }); 52 | 53 | /** 54 | * This is a Tab warped in a Link. 55 | */ 56 | class LinkTab extends React.Component { 57 | 58 | static muiName = 'Tab'; 59 | 60 | /** 61 | * Properties types. 62 | */ 63 | props: { 64 | value: String, 65 | classes?: Object 66 | }; 67 | 68 | /** 69 | * @inheritDoc 70 | */ 71 | render() { 72 | const classes = this.props.classes; 73 | const tabStyles = { 74 | root: classes.tabRoot, 75 | wrapper: classes.tabWrapper, 76 | labelContainer: classes.tabLabel 77 | }; 78 | return ( 79 | 80 | Hello 81 | 82 | ) 83 | } 84 | } 85 | LinkTab = withStyles(styles)(LinkTab); 86 | 87 | /** 88 | * A Navigation Bar implemented as a ReactRouter-aware material-ui Tabs. 89 | */ 90 | class NavigationBar extends React.Component { 91 | 92 | /** 93 | * Properties types. 94 | */ 95 | props: { 96 | pathname: String, 97 | isSignedIn: boolean, 98 | classes?: { 99 | root: Object 100 | } 101 | }; 102 | 103 | /** 104 | * @inheritDoc 105 | */ 106 | render() { 107 | if (this.props.isSignedIn) { 108 | return ( 109 | {}}> 110 | } label="HOME"/> 111 | } label="RECENT"/> 112 | 113 | ); 114 | } 115 | 116 | return ( 117 | {}}> 118 | } label="RECENT"/> 119 | 120 | ); 121 | } 122 | } 123 | 124 | const mapStateToProps = state => ({ 125 | pathname: state.router.location.pathname, 126 | isSignedIn: !state.firebaseState.auth.isEmpty 127 | }); 128 | 129 | export default compose(withStyles(styles), connect(mapStateToProps))(NavigationBar); 130 | -------------------------------------------------------------------------------- /frontend/components/NewPost.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | // @flow 17 | 18 | import React from 'react'; 19 | import { connect } from 'react-redux'; 20 | 21 | /** 22 | * Entry point to the FriendlyPix app. 23 | */ 24 | class NewPost extends React.Component { 25 | 26 | /** 27 | * Constructor for the FriendlyPix app. 28 | * 29 | * @param {Object} props - Additional object properties. 30 | * @constructor 31 | */ 32 | constructor(props) { 33 | super(props); 34 | } 35 | 36 | /** 37 | * Properties types. 38 | */ 39 | props: { 40 | }; 41 | 42 | /** 43 | * @inheritDoc 44 | */ 45 | render() { 46 | return ( 47 |
48 |

NEW POST

49 |
50 | ) 51 | } 52 | } 53 | 54 | const mapStateToProps = state => { 55 | return state; 56 | }; 57 | 58 | const mapDispatchToProps = (dispatch: Function, ownProps) => ({ 59 | }); 60 | 61 | export default connect(mapStateToProps, mapDispatchToProps)(NewPost); 62 | -------------------------------------------------------------------------------- /frontend/components/RecentPostsFeed.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | // @flow 17 | 18 | import React from 'react'; 19 | import { connect } from 'react-redux'; 20 | import 'firebase/auth'; 21 | 22 | 23 | /** 24 | * Entry point to the Recent Posts Feed. 25 | */ 26 | class RecentPostsFeed extends React.Component { 27 | 28 | /** 29 | * Constructor for the Recent Posts Feed. 30 | * 31 | * @param {Object} props - Additional object properties. 32 | * @constructor 33 | */ 34 | constructor(props) { 35 | super(props); 36 | } 37 | 38 | /** 39 | * Properties types. 40 | */ 41 | props: { 42 | }; 43 | 44 | /** 45 | * @inheritDoc 46 | */ 47 | render() { 48 | return ( 49 |
50 |

RECENT POSTS FEED

51 |
52 | ) 53 | } 54 | } 55 | 56 | const mapStateToProps = (state) => { 57 | return state; 58 | }; 59 | 60 | const mapDispatchToProps = (dispatch: Function, ownProps) => ({ 61 | }); 62 | 63 | export default connect(mapStateToProps, mapDispatchToProps)(RecentPostsFeed); 64 | -------------------------------------------------------------------------------- /frontend/components/SearchBar.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | // @flow 17 | 18 | import React from 'react'; 19 | import { connect } from 'react-redux'; 20 | import { withStyles } from 'material-ui/styles'; 21 | import SearchIcon from 'material-ui-icons/Search'; 22 | import TextField from 'material-ui/TextField'; 23 | import { compose } from 'redux'; 24 | import { firebaseConnect } from 'react-redux-firebase'; 25 | import { common, lightBlue } from 'material-ui/colors'; 26 | 27 | const styles = theme => ({ 28 | root: { 29 | display: 'block', 30 | color: common.white, 31 | backgroundColor: lightBlue['600'], 32 | borderRadius: '3px', 33 | padding: '5px 10px 5px 5px', 34 | marginRight: '20px', 35 | whiteSpace: 'nowrap', 36 | [theme.breakpoints.down('sm')]: { 37 | marginRight : '6px' 38 | } 39 | }, 40 | icon: { 41 | position: 'relative', 42 | top: '7px', 43 | margin: '0 5px' 44 | }, 45 | textField: { 46 | color: common.white, 47 | '&:before': { 48 | display: 'none' 49 | }, 50 | '&:after': { 51 | display: 'none' 52 | } 53 | } 54 | }); 55 | 56 | /** 57 | * The Search Bar. 58 | */ 59 | class SearchBar extends React.Component { 60 | 61 | /** 62 | * Properties types. 63 | */ 64 | props: { 65 | classes: Object 66 | }; 67 | 68 | /** 69 | * @inheritDoc 70 | */ 71 | render() { 72 | const classes = this.props.classes; 73 | return ( 74 |
75 | this.textInput.focus()} 78 | /> 79 | this.textInput = element} 81 | type="search" 82 | InputClassName={classes.textField} 83 | margin="none" 84 | /> 85 |
86 | ) 87 | } 88 | } 89 | 90 | export default compose(withStyles(styles), firebaseConnect(), connect())(SearchBar); 91 | -------------------------------------------------------------------------------- /frontend/components/SinglePost.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | // @flow 17 | 18 | import React from 'react'; 19 | import { connect } from 'react-redux'; 20 | 21 | /** 22 | * Entry point to the FriendlyPix app. 23 | */ 24 | class SinglePost extends React.Component { 25 | 26 | /** 27 | * Constructor for the FriendlyPix app. 28 | * 29 | * @param {Object} props - Additional object properties. 30 | * @constructor 31 | */ 32 | constructor(props) { 33 | super(props); 34 | } 35 | 36 | /** 37 | * Properties types. 38 | */ 39 | props: { 40 | }; 41 | 42 | /** 43 | * @inheritDoc 44 | */ 45 | render() { 46 | return ( 47 |
48 |

SINGLE POST

49 |
50 | ) 51 | } 52 | } 53 | 54 | const mapStateToProps = (state, ownProps) => { 55 | // const apiData = state.apiData[ownProps.show.imdbID] ? state.apiData[ownProps.show.imdbID] : {}; 56 | return state; 57 | }; 58 | 59 | const mapDispatchToProps = (dispatch: Function, ownProps) => ({ 60 | }); 61 | 62 | export default connect(mapStateToProps, mapDispatchToProps)(SinglePost); 63 | -------------------------------------------------------------------------------- /frontend/components/SplashPage.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | // @flow 17 | 18 | import React from 'react'; 19 | import { connect } from 'react-redux'; 20 | import PhotoIcon from 'material-ui-icons/Photo'; 21 | import { FirebaseAuth } from 'react-firebaseui'; 22 | import firebase from 'firebase/app'; 23 | import 'firebase/auth'; 24 | import { Link } from 'react-router-dom'; 25 | import { firebaseConnect } from 'react-redux-firebase'; 26 | import styles from './splash-page.css'; 27 | import { push } from 'react-router-redux'; 28 | import { compose } from 'redux' 29 | import './firebaseui-overrides.global.css'; // Import globally. 30 | 31 | 32 | /** 33 | * The Splash Page containing the login UI. 34 | */ 35 | class SplashPage extends React.Component { 36 | 37 | /** 38 | * Constructor for the Splash Page. 39 | * 40 | * @param {Object} props - Additional object properties. 41 | * @constructor 42 | */ 43 | constructor(props) { 44 | super(props); 45 | 46 | this.uiConfig = { 47 | signInFlow: 'popup', 48 | signInOptions: [ 49 | firebase.auth.GoogleAuthProvider.PROVIDER_ID, 50 | firebase.auth.FacebookAuthProvider.PROVIDER_ID 51 | ], 52 | callbacks: { 53 | signInSuccess: () => { 54 | this.props.redirectHome(); 55 | return false; 56 | } 57 | } 58 | }; 59 | } 60 | 61 | /** 62 | * Properties types. 63 | */ 64 | props: { 65 | redirectHome: Function, 66 | firebase: { 67 | auth: Function 68 | } 69 | }; 70 | 71 | /** 72 | * @inheritDoc 73 | */ 74 | render() { 75 | return ( 76 |
77 |
Friendly Pix
78 |
The friendliest way to share your pics
79 |
80 | 81 | skip sign in 82 |
83 |
84 | ) 85 | } 86 | } 87 | 88 | const mapStateToProps = state => { 89 | return state; 90 | }; 91 | 92 | const mapDispatchToProps = (dispatch: Function, ownProps) => ({ 93 | redirectHome() { 94 | dispatch(push('/home')); 95 | } 96 | }); 97 | 98 | export default compose(firebaseConnect(), connect(mapStateToProps, mapDispatchToProps))(SplashPage); 99 | -------------------------------------------------------------------------------- /frontend/components/UserProfile.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | // @flow 17 | 18 | import React from 'react'; 19 | import { connect } from 'react-redux'; 20 | 21 | /** 22 | * Entry point to the FriendlyPix app. 23 | */ 24 | class UserProfile extends React.Component { 25 | 26 | /** 27 | * Constructor for the FriendlyPix app. 28 | * 29 | * @param {Object} props - Additional object properties. 30 | * @constructor 31 | */ 32 | constructor(props) { 33 | super(props); 34 | } 35 | 36 | /** 37 | * Properties types. 38 | */ 39 | props: { 40 | }; 41 | 42 | /** 43 | * @inheritDoc 44 | */ 45 | render() { 46 | return ( 47 |
48 |

USER PROFILE

49 |
50 | ) 51 | } 52 | } 53 | 54 | const mapStateToProps = (state, ownProps) => { 55 | // const apiData = state.apiData[ownProps.show.imdbID] ? state.apiData[ownProps.show.imdbID] : {}; 56 | return state; 57 | }; 58 | 59 | const mapDispatchToProps = (dispatch: Function, ownProps) => ({ 60 | }); 61 | 62 | export default connect(mapStateToProps, mapDispatchToProps)(UserProfile); 63 | -------------------------------------------------------------------------------- /frontend/components/UserStatus.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | // @flow 17 | 18 | import React from 'react'; 19 | import { connect } from 'react-redux'; 20 | import { Link } from 'react-router-dom'; 21 | import Button from 'material-ui/Button'; 22 | import Avatar from 'material-ui/Avatar'; 23 | import { withStyles } from 'material-ui/styles'; 24 | import { compose } from 'redux'; 25 | import { common } from 'material-ui/colors'; 26 | 27 | 28 | const styles = theme => ({ 29 | root: { 30 | display: 'flex', 31 | textDecoration: 'none', 32 | whiteSpace: 'nowrap' 33 | }, 34 | avatar: { 35 | marginRight: '-20px', 36 | borderRadius: '20px', 37 | border: '2px white solid', 38 | zIndex: 3, 39 | [theme.breakpoints.down('md')]: { 40 | width: '32px', 41 | height: '32px' 42 | } 43 | }, 44 | button: { 45 | paddingLeft: '35px', 46 | zIndex: 2 47 | }, 48 | label: { 49 | color: common.white, 50 | textTransform: 'none' 51 | } 52 | }); 53 | 54 | /** 55 | * Widget showing the user status. 56 | */ 57 | class UserStatus extends React.Component { 58 | 59 | /** 60 | * Properties types. 61 | */ 62 | props: { 63 | auth: { 64 | isEmpty: boolean, 65 | uid: String, 66 | displayName: String, 67 | photoURL: String 68 | }, 69 | classes: Object 70 | }; 71 | 72 | /** 73 | * @inheritDoc 74 | */ 75 | render() { 76 | const isUserSignedIn = !this.props.auth.isEmpty; 77 | const classes = this.props.classes; 78 | 79 | if (isUserSignedIn) { 80 | return ( 81 | 82 | 85 | 86 | 87 | ); 88 | } 89 | return ( 90 | 91 | 92 | 93 | ); 94 | } 95 | } 96 | 97 | const mapStateToProps = state => { 98 | return {auth: state.firebaseState.auth}; 99 | }; 100 | 101 | export default compose(withStyles(styles), connect(mapStateToProps))(UserStatus); 102 | -------------------------------------------------------------------------------- /frontend/components/app.global.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | html, body { 18 | font-family: 'Roboto', 'Helvetica', sans-serif; 19 | padding:0; 20 | margin:0; 21 | } 22 | -------------------------------------------------------------------------------- /frontend/components/firebaseui-overrides.global.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | .firebaseui-container { 18 | background: rgba(0, 0, 0, 0.05); 19 | margin-bottom: 15px; 20 | min-height: 150px; 21 | padding-top: 10px; 22 | border-radius: 20px; 23 | box-shadow: none; 24 | } 25 | .firebaseui-container.firebaseui-page-provider-sign-in { 26 | background: transparent; 27 | box-shadow: none; 28 | min-height: 0; 29 | margin-bottom: 0; 30 | padding-top: 0; 31 | } 32 | .firebaseui-container.firebaseui-id-page-callback { 33 | background: transparent; 34 | box-shadow: none; 35 | margin-top: 40px; 36 | height: 84px; 37 | min-height: 0; 38 | margin-bottom: 0; 39 | padding-top: 0; 40 | } 41 | .firebaseui-card-header { 42 | display: none; 43 | } 44 | .firebaseui-subtitle, .firebaseui-text { 45 | color: rgba(255, 255, 255, 0.87); 46 | } 47 | .firebaseui-form-actions .mdl-button--raised.mdl-button--colored.firebaseui-button { 48 | background: rgba(0, 0, 0, 0.1); 49 | } 50 | .firebaseui-id-dismiss-info-bar { 51 | display: block; 52 | } 53 | .firebaseui-info-bar { 54 | border: 0; 55 | border-radius: 10px; 56 | left: 5%; 57 | right: 5%; 58 | top: 10%; 59 | bottom: 10%; 60 | } 61 | -------------------------------------------------------------------------------- /frontend/components/splash-page.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | .container { 18 | position: fixed; 19 | top: 0; 20 | left: 0; 21 | width: 100%; 22 | height: 100%; 23 | background-color: #0288D1; 24 | background: radial-gradient(circle, #039BE5, #01579b); 25 | z-index: 10000; 26 | display: flex; 27 | justify-content: center; 28 | align-items: center; 29 | color: white; 30 | flex-direction: column; 31 | } 32 | .logo { 33 | font-family: 'Amaranth', sans-serif; 34 | font-size: 200%; 35 | } 36 | .logoIcon { 37 | top: 4px; 38 | font-size: 32px; 39 | margin-right: -2px; 40 | position: relative; 41 | } 42 | .caption { 43 | margin: 20px 0 40px 0; 44 | font-family: 'Amaranth', sans-serif; 45 | font-size: 18px; 46 | opacity: 0.8; 47 | } 48 | .skip { 49 | font-weight:lighter; 50 | color:white; 51 | opacity: 0.7; 52 | width: 100%; 53 | display: block; 54 | text-align: center; 55 | text-decoration: none; 56 | cursor: pointer; 57 | } 58 | .skip:HOVER { 59 | text-decoration: underline; 60 | } 61 | .firebaseUi { 62 | min-width: 250px; 63 | } 64 | -------------------------------------------------------------------------------- /frontend/firebaseTools.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import * as Cookies from 'js-cookie'; 18 | 19 | /** 20 | * Keeps the ID token in sync in a cookie. While this is active, the current ID Token of the 21 | * signed-in Firebase user is synced in a cookie of the given name. 22 | * 23 | * You can call the returned Function to stop the sync. 24 | * 25 | * @param {Object} firebaseApp - The Firebase instance that will be used. 26 | * @param {string} [cookieName] - The name of the cookie that will hold the ID Token. This defaults 27 | * to '__session'. 28 | * @return {Function} - The unsubscribe function. Call this function to stop the process. 29 | */ 30 | export function keepIdTokenInCookie(firebaseApp, cookieName = '__session') { 31 | if (document instanceof Object) { 32 | // Make sure the Firebase ID Token is always passed as a cookie. 33 | return firebaseApp.auth().onIdTokenChanged(user => { 34 | if (user) { 35 | user.getIdToken().then(idToken => { 36 | console.log('User signed-in or new ID Token for user!'); 37 | const existingCookieValue = Cookies.get(cookieName); 38 | // If the token is not in the cookies yet we add it. 39 | if (existingCookieValue !== idToken) { 40 | console.log('Saving new ID Token in', cookieName, 'cookie.'); 41 | // Set a Cookie valid for 1h. 42 | Cookies.set(cookieName, idToken, {expires: 0.04166}); 43 | } 44 | }); 45 | } else { 46 | console.log('User signed-out!'); 47 | Cookies.set(cookieName, '', {expires: 0}); 48 | } 49 | }); 50 | } else { 51 | console.warn('You can only use cookies in a DOM environment. This is a NoOp'); 52 | return () => {}; 53 | } 54 | } 55 | 56 | /** 57 | * Returns a promise that completes when Firebase Auth is ready in the given store using 58 | * react-redux-firebase. 59 | * 60 | * @param {Object} store - The Redux store on which we want to detect if Firebase auth is ready. 61 | * @param {string} [firebaseReducerAttributeName] - The attribute name of the react-redux-firebase 62 | * reducer. 'firebaseState' by default. 63 | * @return {Promise} - A promise that completes when Firebase auth is ready in the store. 64 | */ 65 | export function whenAuthReady(store, firebaseReducerAttributeName = 'firebaseState') { 66 | const isAuthReady = store => { 67 | const state = store.getState(); 68 | const firebaseState = firebaseReducerAttributeName ? 69 | state[firebaseReducerAttributeName] : state; 70 | return firebaseState && firebaseState.auth && firebaseState.auth.isLoaded; 71 | }; 72 | 73 | return new Promise(accept => { 74 | if (isAuthReady(store)) { 75 | console.log('Redux store Firebase auth state is ready!'); 76 | return accept(); 77 | } 78 | let unsubscribe = store.subscribe(() => { 79 | if (isAuthReady(store)) { 80 | console.log('Redux store Firebase auth state is ready!'); 81 | unsubscribe(); 82 | accept(); 83 | } 84 | }); 85 | }); 86 | } 87 | -------------------------------------------------------------------------------- /frontend/reducers.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | // @flow 17 | 18 | import { SET_PROFILE_SEARCH_TERM } from './actions'; 19 | 20 | export const profileSearchTerm = (state = '', action: Action) => { 21 | if (action.type === SET_PROFILE_SEARCH_TERM) { 22 | return action.payload; 23 | } 24 | return state; 25 | }; 26 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 'use strict'; 17 | 18 | // This will allow to require the .jsx files directly since babel will automatically compile them on the fly. 19 | // We only using during development for performance reasons. 20 | if (!process.env.FUNCTION_NAME || process.env.NODE_ENV === 'devserver') { 21 | require('babel-register'); 22 | } 23 | 24 | /** 25 | * Triggers when a user gets a new follower and sends notifications if the user has enabled them. 26 | * Also avoids sending multiple notifications for the same user by keeping a timestamp of sent notifications. 27 | */ 28 | if (!process.env.FUNCTION_NAME || process.env.FUNCTION_NAME === 'sendFollowerNotification') { 29 | exports.sendFollowerNotification = require('./microservices/sendFollowerNotification'); 30 | } 31 | 32 | /** 33 | * When an image is uploaded we check if it is flagged as Adult or Violence by the Cloud Vision 34 | * API and if it is we blur it using ImageMagick. 35 | */ 36 | if (!process.env.FUNCTION_NAME || process.env.FUNCTION_NAME === 'blurOffensiveImages') { 37 | exports.blurOffensiveImages = require('./microservices/blurOffensiveImages'); 38 | } 39 | 40 | /** 41 | * Helper function to get the markup from React, inject the initial state, and 42 | * send the server-side markup to the client 43 | */ 44 | if (!process.env.FUNCTION_NAME || process.env.FUNCTION_NAME === 'renderTemplate') { 45 | exports.renderTemplate = require('./microservices/renderTemplate'); 46 | } 47 | -------------------------------------------------------------------------------- /microservices/README.md: -------------------------------------------------------------------------------- 1 | The Google Cloud Functions. 2 | -------------------------------------------------------------------------------- /microservices/blurOffensiveImages.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2016 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 'use strict'; 17 | 18 | const functions = require('firebase-functions'); 19 | const admin = require('firebase-admin'); 20 | try {admin.initializeApp(functions.config().firebase);} catch(e) {} 21 | const mkdirp = require('mkdirp-promise'); 22 | const gcs = require('@google-cloud/storage')(); 23 | const vision = require('@google-cloud/vision')(); 24 | const spawn = require('child-process-promise').spawn; 25 | const path = require('path'); 26 | const os = require('os'); 27 | const fs = require('fs'); 28 | 29 | /** 30 | * When an image is uploaded we check if it is flagged as Adult or Violence by the Cloud Vision 31 | * API and if it is we blur it using ImageMagick. 32 | */ 33 | exports = module.exports = functions.storage.object().onChange(event => { 34 | const object = event.data; 35 | 36 | // Exit if this is a move or deletion event. 37 | if (object.resourceState === 'not_exists') { 38 | console.log('This is a deletion event.'); 39 | return; 40 | } 41 | 42 | const image = { 43 | source: {imageUri: `gs://${object.bucket}/${object.name}`} 44 | }; 45 | 46 | // Check the image content using the Cloud Vision API. 47 | return vision.safeSearchDetection(image).then(batchAnnotateImagesResponse => { 48 | console.log('SafeSearch results on image', batchAnnotateImagesResponse); 49 | const safeSearchResult = batchAnnotateImagesResponse[0].safeSearchAnnotation; 50 | 51 | if (safeSearchResult.adult === 'LIKELY' || 52 | safeSearchResult.adult === 'VERY_LIKELY' || 53 | safeSearchResult.violence === 'LIKELY' || 54 | safeSearchResult.violence === 'VERY_LIKELY') { 55 | return blurImage(object.name, object.bucket, object.metadata).then(() => { 56 | const filePathSplit = object.name.split(path.sep); 57 | const uid = filePathSplit[0]; 58 | const size = filePathSplit[1]; // 'thumb' or 'full' 59 | const postId = filePathSplit[2]; 60 | 61 | return refreshImages(uid, postId, size); 62 | }); 63 | } 64 | }); 65 | }); 66 | 67 | /** 68 | * Blurs the given image located in the given bucket using ImageMagick. 69 | */ 70 | function blurImage(filePath, bucketName, metadata) { 71 | const tempLocalFile = path.join(os.tmpdir(), filePath); 72 | const tempLocalDir = path.dirname(tempLocalFile); 73 | const bucket = gcs.bucket(bucketName); 74 | 75 | // Create the temp directory where the storage file will be downloaded. 76 | return mkdirp(tempLocalDir).then(() => { 77 | // Download file from bucket. 78 | return bucket.file(filePath).download({destination: tempLocalFile}); 79 | }).then(() => { 80 | console.log('The file has been downloaded to', tempLocalFile); 81 | // Blur the image using ImageMagick. 82 | return spawn('convert', [tempLocalFile, '-channel', 'RGBA', '-blur', '0x18', tempLocalFile]); 83 | }).then(() => { 84 | console.log('Blurred image created at', tempLocalFile); 85 | // Uploading the Blurred image. 86 | return bucket.upload(tempLocalFile, { 87 | destination: filePath, 88 | metadata: {metadata: metadata} // Keeping custom metadata. 89 | }); 90 | }).then(() => { 91 | console.log('Blurred image uploaded to Storage at', filePath); 92 | fs.unlinkSync(tempLocalFile); 93 | console.log('Deleted local file', tempLocalFile); 94 | }); 95 | } 96 | 97 | /** 98 | * Changes the image URL slightly (add a `&blurred` query parameter) to force a refresh. 99 | */ 100 | function refreshImages(uid, postId, size) { 101 | let app; 102 | try { 103 | // Create a Firebase app that will honor security rules for a specific user. 104 | const config = { 105 | credential: functions.config().firebase.credential, 106 | databaseURL: functions.config().firebase.databaseURL, 107 | databaseAuthVariableOverride: { 108 | uid: uid 109 | } 110 | }; 111 | app = admin.initializeApp(config, uid); 112 | } catch (e) { 113 | if (e.code !== 'app/duplicate-app') { 114 | console.error('There was an error initializing Firebase Admin', e); 115 | throw e; 116 | } 117 | // An app for that UID was already created so we re-use it. 118 | console.log('Re-using existing app.'); 119 | app = admin.app(uid); 120 | } 121 | 122 | const imageUrlRef = app.database().ref(`/posts/${postId}/${size}_url`); 123 | return imageUrlRef.once('value').then(snap => { 124 | const picUrl = snap.val(); 125 | return imageUrlRef.set(`${picUrl}&blurred`).then(() => { 126 | console.log('Blurred image URL updated.'); 127 | }); 128 | }); 129 | } 130 | -------------------------------------------------------------------------------- /microservices/firebase-express-middleware.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 'use strict'; 17 | 18 | const cookie = require('cookie'); 19 | const admin = require('firebase-admin'); 20 | 21 | /** 22 | * Creates a Firebase admin instance using the app default credentials. 23 | * This is only possible in a Google Cloud environment. 24 | * 25 | * @return {admin.app.App} - A Firebase Admin App instance. 26 | */ 27 | function getDefaultFirebaseAdminApp() { 28 | let firebaseAdminApp; 29 | let appName; 30 | try { 31 | // Configure Firebase Admin. 32 | appName = `__service_account_${process.env.GCLOUD_PROJECT}`; 33 | firebaseAdminApp = admin.initializeApp({credential: admin.credential.applicationDefault()}, appName); 34 | } catch (e) { 35 | if (e.code !== 'app/duplicate-app') { 36 | console.error('There was an error initializing Firebase Admin with default credentials', e); 37 | return; 38 | } 39 | // An app for that UID was already created so we re-use it. 40 | console.log('Re-using existing app.'); 41 | firebaseAdminApp = admin.app(appName); 42 | } 43 | return firebaseAdminApp; 44 | } 45 | 46 | // The default configuration values. They can be overridden individually when calling `auth`. 47 | const DEFAULT_CONFIG = { 48 | checkHeader: true, 49 | checkCookie: false, 50 | cookieName: '__session', 51 | generateCustomToken: false, 52 | firebaseAdminApp: null 53 | }; 54 | 55 | /** 56 | * Express middleware that checks if a Firebase ID Token is passed in the `Authorization` HTTP 57 | * header or a cookie and decodes it. 58 | * 59 | * The Firebase ID token needs to be passed as a Bearer token in the Authorization HTTP header 60 | * like this: `Authorization: Bearer `. 61 | * 62 | * You can also pass the ID Token in an HTTP cookie. To do this you need to configure the 63 | * middleware with `config.checkCookie` as `true` and pass the name of the cookie to check in 64 | * `config.cookieName`. 65 | * 66 | * By default the middleware will try to use a Firebase Admin instance using default app 67 | * credentials which is only possible when running in a Google Cloud environment. 68 | * 69 | * When decoded successfully, the ID Token content will be added as `req.user`. 70 | * 71 | * Typical usage is: 72 | * 73 | * ```js 74 | * const express = require('express'); 75 | * const router = new express.Router(); 76 | * const firebaseMiddleware = require('express-firebase-middleware'); 77 | * 78 | * // Create a Firebase Admin app 79 | * const admin = require('firebase-admin'); 80 | * const serviceAccount = require('./service-account-credentials.json'); 81 | * const firebaseAdminApp = admin.initializeApp({ 82 | * credential: admin.credential.cert(serviceAccount) 83 | * }); 84 | * 85 | * // Add the Firebase auth middleware. 86 | * router.use(firebaseMiddleware.auth({ 87 | * firebaseAdminApp: firebaseAdminApp 88 | * })); 89 | * 90 | * router.get('*', (req, res) => { 91 | * if (req.user) { 92 | * // Firebase User is authorized. 93 | * console.log('Request is authorized for Firebase user with UID:', req.uid); 94 | * } 95 | * }); 96 | * ``` 97 | * 98 | * @param {Object} [config] - The configuration object for the middleware. 99 | * @param {boolean} [config.checkHeader] - If `true` the middleware will check if an ID Token is 100 | * passed in the `Authorization` HTTP header as Bearer token. This defaults as `true`. 101 | * @param {boolean} [config.checkCookie] - If `true` the middleware will check if an ID Token is 102 | * passed in the a cookie. The name of the cookie to check can be configured using 103 | * `config.cookieName`. This defaults as `false`. 104 | * @param {boolean} [config.cookieName] - The name of the cookie to check if `config.checkCookie` 105 | * is `true`. This defaults as `"__session"`. 106 | * @param {boolean} [config.generateCustomToken] - If `true` the middleware will generate a 107 | * Firebase Custom Auth token in `req.user.token` if a valid ID token is found. This defaults 108 | * as `false`. 109 | * @param {boolean} [config.firebaseAdminApp] - A Firebase Admin app instance that will be used to 110 | * decode the ID token and generate the Custom Auth token if `config.generateCustomToken` is 111 | * `true`. If none is provided a Firebae Admin app will be instanciated wuing the app default 112 | * credentials which his only possible in a Google Cloud environment (Google App Engine, Google 113 | * Compute Engine, Firebase Functions...) and does not allow generating Custom auth tokens. 114 | * @return {Object} - The configured Firebase Auth Express Middleware. 115 | */ 116 | exports.auth = config => { 117 | if (!config) { 118 | config = {}; 119 | } 120 | // Merge provided config with the default config. 121 | config = Object.assign(Object.assign({}, DEFAULT_CONFIG), config); 122 | 123 | return (req, res, next) => { 124 | console.log('Check if request is authorized with Firebase ID token'); 125 | 126 | let idToken; 127 | 128 | // Check auth header for an ID Token. 129 | if (config.checkHeader) { 130 | idToken = getIdTokenFromRequestHeader(req); 131 | } 132 | 133 | // Check cookies for an ID Token. The ID Token from a request header has priority if both exist. 134 | if (!idToken && config.checkCookie) { 135 | idToken = getIdTokenFromCookie(req, config.cookieName); 136 | } 137 | 138 | if (idToken) { 139 | // If no Firebase Admin app was provided in the config we use one with default credentials. 140 | const firebaseAdminApp = config.firebaseAdminApp || getDefaultFirebaseAdminApp(); 141 | 142 | // Make sure we have a usable Firebase Admin app instance. 143 | if (!firebaseAdminApp instanceof Object && firebaseAdminApp.auth instanceof Function) { 144 | console.error('We could not create a Firebase Admin app with default credentials.', 145 | 'You can pass a Firebase Admin app instance to the Firebase express middleware using', 146 | '`config.firebaseAdminApp`.'); 147 | next(); 148 | return; 149 | } 150 | // We found an ID token, let's try to decode it. 151 | decodeIdToken(idToken, firebaseAdminApp).then(user => { 152 | req.user = user; 153 | if (req.user && config.generateCustomToken) { 154 | // We successfully decoded the ID token and we want to generate a custom ID token. 155 | return generateCustomAuthToken(req.user.uid, firebaseAdminApp) 156 | .then(customAuthToken => req.user.token = customAuthToken); 157 | } 158 | }).then(() => next()); 159 | } else { 160 | console.log('Found no Firebase ID token in request.'); 161 | next(); 162 | } 163 | }; 164 | } 165 | 166 | /** 167 | * Returns a Promise with the Firebase ID Token if found in the Authorization header. 168 | * 169 | * @param {Object} req - The request object. 170 | * @return {String} - The encoded ID token. 171 | */ 172 | function getIdTokenFromRequestHeader(req) { 173 | if (req.headers.authorization && req.headers.authorization.startsWith('Bearer ')) { 174 | console.log('Found Bearer token in "Authorization" header.'); 175 | // Return the ID Token from the Authorization header. 176 | return req.headers.authorization.split('Bearer ')[1]; 177 | } 178 | } 179 | 180 | /** 181 | * Returns a Promise with the Firebase ID Token if found in a cookie of the given name. 182 | * 183 | * @param {Object} req - The request object. 184 | * @param {String} cookieName - The name of the cookie to check. 185 | * @return {Promise} - The encoded ID token. 186 | */ 187 | function getIdTokenFromCookie(req, cookieName) { 188 | if (req.headers.cookie) { 189 | const cookies = cookie.parse(req.headers.cookie); 190 | if (cookies && cookies[cookieName]) { 191 | console.log('Found "', cookieName, '" cookie.'); 192 | // Read the ID Token from cookie. 193 | return cookies[cookieName]; 194 | } 195 | } 196 | } 197 | 198 | /** 199 | * Returns a Promise with the Decoded ID Token. 200 | * 201 | * @param {String} idToken - The encoded ID token to decode. 202 | * @param {admin.app.App} firebaseAdminApp - A Firebase Admin App instance. 203 | * @return {Object} - The decoded ID token. 204 | */ 205 | function decodeIdToken(idToken, firebaseAdminApp) { 206 | return firebaseAdminApp.auth().verifyIdToken(idToken).then(decodedIdToken => { 207 | console.log('Firebase ID Token correctly decoded.'); 208 | return decodedIdToken; 209 | }).catch(error => { 210 | console.error('Error while verifying Firebase ID token:', error); 211 | }); 212 | } 213 | 214 | /** 215 | * Adds a Custom Auth token to the request. 216 | * 217 | * @param {String} uid - The UID the generate a custom auth token for. 218 | * @param {admin.app.App} firebaseAdminApp - A Firebase Admin App instance. 219 | * @return {String} - The custom auth token for he provided UID. 220 | */ 221 | function generateCustomAuthToken(uid, firebaseAdminApp) { 222 | return firebaseAdminApp.auth().createCustomToken(uid).then(token => { 223 | console.log('Created Custom token for UID:', uid); 224 | return token; 225 | }).catch(error => { 226 | console.error('Error while generating a Custom Auth token for UID:', uid, 227 | 'Make sure you have passed a Firebase Admin app instance authorized with appropriate ' 228 | + 'Service Accounts credentials.', error); 229 | }); 230 | } 231 | -------------------------------------------------------------------------------- /microservices/index.html: -------------------------------------------------------------------------------- 1 | 2 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | Friendly Pix - React Edition 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 60 |
<%= body %>
61 | 62 | 63 | 64 | 223 | -------------------------------------------------------------------------------- /microservices/renderTemplate.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 'use strict'; 17 | 18 | // needed to fix "Error: The XMLHttpRequest compatibility library was not found." in Firebase client SDK. 19 | global.XMLHttpRequest = require('xmlhttprequest').XMLHttpRequest; 20 | const functions = require('firebase-functions'); 21 | const path = require('path'); 22 | const fs = require('fs'); 23 | const React = require('react'); 24 | const ReactDOMServer = require('react-dom/server'); 25 | const _ = require('lodash'); 26 | const baseTemplate = fs.readFileSync(path.resolve(__dirname, './index.html')); 27 | const template = _.template(baseTemplate); 28 | const app = require('../frontend/App'); 29 | const express = require('express'); 30 | const router = new express.Router(); 31 | const firebaseMiddleware = require('./firebase-express-middleware'); 32 | const createMemoryHistory = require('history').createMemoryHistory; 33 | const firebase = require('firebase'); 34 | // Get the Firebase config from the auto generated file. 35 | const firebaseConfig = require('../frontend/firebase-config.json').result; 36 | // Create a Firebase Admin app 37 | const admin = require('firebase-admin'); 38 | const serviceAccount = require('./service-account-credentials.json'); 39 | const firebaseAdminApp = admin.initializeApp({ 40 | credential: admin.credential.cert(serviceAccount) 41 | }, '__service_account'); 42 | 43 | 44 | const cacheControlHeaderValues = {}; 45 | 46 | // This Express middleware will check ig there is a Firebase ID token and inject 47 | router.use(firebaseMiddleware.auth({ 48 | checkCookie: true, 49 | generateCustomToken: true, 50 | firebaseAdminApp: firebaseAdminApp 51 | })); 52 | 53 | router.get('*', (req, res) => { 54 | const user = req.user || {}; 55 | createFirebaseAppWithSignedInUser(user.uid, user.token).then(firebaseApp => { 56 | // We make sure that the firebase auth state listeners are triggered again. 57 | // Create the redux store. 58 | const history = createMemoryHistory(); 59 | // Set the new URL. 60 | history.replace(req.url); 61 | const store = app.makeStore(history, firebaseApp); 62 | const registry = app.makeRegistry(); 63 | // Wait for auth to be ready. 64 | console.log('Waiting for Auth state to be ready in Redux store'); 65 | store.firebaseAuthIsReady.then(() => { 66 | console.log('Auth state ready in Redux store'); 67 | // Render the App. 68 | const body = ReactDOMServer.renderToString( 69 | React.createElement(app.App, {registry: registry, store: store, history: history}) 70 | ); 71 | 72 | // Get the state of the redux store. 73 | const initialState = store.getState(); 74 | 75 | // Grab the CSS from our sheetsRegistry. 76 | const css = registry.toString(); 77 | 78 | // Check if there has been a redirect. 79 | const lastUrl = initialState.router.location.pathname; 80 | if (lastUrl !== req.url) { 81 | // If there has been a redirect we redirect server side. 82 | console.log('Server side redirect to', lastUrl); 83 | res.redirect(lastUrl); 84 | } else { 85 | // res.set('Cache-Control', 'public, max-age=60, s-maxage=180'); // TODO: make this change dependent on each URL. with a map maybe?? 86 | // If there was no redirect we send the rendered app as well as the redux state. 87 | res.send(template({body, initialState, css, node_env: process.env.NODE_ENV})); 88 | } 89 | }); 90 | }).catch(error => { 91 | console.log('There was an error', error); 92 | res.status(500).send(error); 93 | }); 94 | }); 95 | 96 | /** 97 | * Helper function to get the markup from React, inject the initial state, and 98 | * send the server-side markup to the client 99 | */ 100 | exports = module.exports = functions.https.onRequest(router); 101 | 102 | /** 103 | * Returns a Firebase App instance 104 | * 105 | * @param {String} uid - The UID of the user to sign in the app. 106 | * @param {String} customToken - A custom token to sign the user in the app. 107 | * @return {Promise} - A Firebase App instance specific to the given user with the user already signed-in. 108 | */ 109 | function createFirebaseAppWithSignedInUser(uid = undefined, customToken) { 110 | // Instantiate a Firebase app. 111 | let firebaseApp; 112 | // Try to re-use cached firebase App. 113 | try { 114 | firebaseApp = firebase.app(/* uid */); // Uncomment. aka create named apps whe this bug is fixed: https://github.com/prescottprue/react-redux-firebase/issues/250 115 | console.log('Re-used a cached app for UID', uid); 116 | } catch(e) { 117 | firebaseApp = firebase.initializeApp(firebaseConfig/* , uid */); // Uncomment. aka create named apps when this bug is fixed: https://github.com/prescottprue/react-redux-firebase/issues/250 118 | console.log('Created a new Firebase App instance for UID', uid); 119 | } 120 | 121 | // Check if a Firebase user was signed in and a custom auth token was generated. 122 | let signInPromise; 123 | const firebaseAppUid = firebaseApp.auth().currentUser ? firebaseApp.auth().currentUser.uid : undefined; 124 | if (uid === firebaseAppUid) { 125 | signInPromise = Promise.resolve(); 126 | console.log('Firebase App instance auth state is already correct.'); 127 | } else if (uid && customToken) { 128 | console.log('Need to sign in user in Firebase App instance.'); 129 | signInPromise = firebaseApp.auth().signInWithCustomToken(customToken).then(user => { 130 | console.log('User now signed-in! uid:', user.uid); 131 | }); 132 | } else { 133 | console.log('Need to sign out user in Firebase App instance.'); 134 | signInPromise = firebaseApp.auth().signOut().then(() => { 135 | console.log('User now signed-out!'); 136 | }); 137 | } 138 | 139 | return signInPromise.then(() => firebaseApp); 140 | } 141 | -------------------------------------------------------------------------------- /microservices/sendFollowerNotification.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2016 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 'use strict'; 17 | 18 | const functions = require('firebase-functions'); 19 | const admin = require('firebase-admin'); 20 | try {admin.initializeApp(functions.config().firebase);} catch(e) {} 21 | 22 | /** 23 | * Triggers when a user gets a new follower and sends notifications if the user has enabled them. 24 | * Also avoids sending multiple notifications for the same user by keeping a timestamp of sent notifications. 25 | */ 26 | exports = module.exports = functions.database.ref('/followers/{followedUid}/{followerUid}').onWrite(event => { 27 | const followerUid = event.params.followerUid; 28 | const followedUid = event.params.followedUid; 29 | // If un-follow we exit the function. 30 | if (!event.data.val()) { 31 | return console.log('User ', followerUid, 'un-followed user', followedUid); 32 | } 33 | const followedUserRef = admin.database().ref(`people/${followedUid}`); 34 | console.log('We have a new follower UID:', followerUid, 'for user:', followerUid); 35 | 36 | // Check if the user has notifications enabled. 37 | return followedUserRef.child('/notificationEnabled').once('value').then(enabledSnap => { 38 | const notificationsEnabled = enabledSnap.val(); 39 | if (!notificationsEnabled) { 40 | return console.log('The user has not enabled notifications.'); 41 | } 42 | console.log('User has notifications enabled.'); 43 | 44 | // Check if we already sent that notification. 45 | return followedUserRef.child(`/notificationsSent/${followerUid}`).once('value').then(snap => { 46 | if (snap.val()) { 47 | return console.log('Already sent a notification to', followedUid, 'for this follower.'); 48 | } 49 | console.log('Not yet sent a notification to', followedUid, 'for this follower.'); 50 | 51 | // Get the list of device notification tokens. 52 | const getNotificationTokensPromise = followedUserRef.child('notificationTokens').once('value'); 53 | 54 | // Get the follower profile. 55 | const getFollowerProfilePromise = admin.auth().getUser(followerUid); 56 | 57 | return Promise.all([getNotificationTokensPromise, getFollowerProfilePromise]).then(results => { 58 | const tokensSnapshot = results[0]; 59 | const follower = results[1]; 60 | 61 | // Check if there are any device tokens. 62 | if (!tokensSnapshot.hasChildren()) { 63 | return console.log('There are no notification tokens to send to.'); 64 | } 65 | console.log('There are', tokensSnapshot.numChildren(), 'tokens to send notifications to.'); 66 | console.log('Fetched follower profile', follower); 67 | const displayName = follower.displayName; 68 | const profilePic = follower.photoURL; 69 | 70 | // Notification details. 71 | const payload = { 72 | notification: { 73 | title: 'You have a new follower!', 74 | body: `${displayName} is now following you.`, 75 | icon: profilePic || '/images/silhouette.jpg', 76 | click_action: `https://friendly-pix.com/user/${followerUid}` 77 | } 78 | }; 79 | 80 | // Listing all device tokens of the user to notify. 81 | const tokens = Object.keys(tokensSnapshot.val()); 82 | 83 | // Saves the flag that this notification has been sent. 84 | const setNotificationsSentTask = followedUserRef.child(`/notificationsSent/${followerUid}`) 85 | .set(admin.database.ServerValue.TIMESTAMP).then(() => { 86 | console.log('Marked notification as sent.'); 87 | }); 88 | 89 | // Send notifications to all tokens. 90 | const notificationPromise = admin.messaging().sendToDevice(tokens, payload).then(response => { 91 | // For each message check if there was an error. 92 | const tokensToRemove = {}; 93 | response.results.forEach((result, index) => { 94 | const error = result.error; 95 | if (error) { 96 | // Cleanup the tokens who are not registered anymore. 97 | if (error.code === 'messaging/invalid-registration-token' || 98 | error.code === 'messaging/registration-token-not-registered') { 99 | console.log('The following token is not registered anymore', tokens[index]); 100 | tokensToRemove[`/people/${followedUid}/notificationTokens/${tokens[index]}`] = null; 101 | } else { 102 | console.error('Failure sending notification to', tokens[index], error); 103 | } 104 | } 105 | }); 106 | // If there are tokens to cleanup. 107 | const nbTokensToCleanup = Object.keys(tokensToRemove).length; 108 | if (nbTokensToCleanup > 0) { 109 | return admin.database().ref('/').update(tokensToRemove).then(() => { 110 | console.log(`Removed ${nbTokensToCleanup} unregistered tokens.`); 111 | }); 112 | } 113 | console.log(`Successfully sent ${tokens.length - nbTokensToCleanup} notifications.`); 114 | }); 115 | 116 | return Promise.all([notificationPromise, setNotificationsSentTask]); 117 | }); 118 | }); 119 | }); 120 | }); 121 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "cleanup": "for file in $(find frontend public -type f -iname *.map 2>/dev/null); do rm $file ${file%.*}; done", 4 | "build": "npm run createfirebaseconf; npm run cleanup; NODE_ENV=production webpack -p; babel $(find frontend -type f -iname *.jsx) -s --retain-lines -d ./", 5 | "build:dev": "npm run createfirebaseconf; npm run cleanup; webpack -d", 6 | "build:watch": "npm run createfirebaseconf; npm run cleanup; webpack --watch", 7 | "createfirebaseconf": "firebase setup:web --json > ./frontend/firebase-config.json", 8 | "serve": "GCLOUD_PROJECT=- NODE_ENV=devserver npm run build:watch", 9 | "format": "prettier --write --single-quote --print-width=120 --parser=flow --tab-width=2 \"js/**/*.{js,jsx}\"", 10 | "lint": "eslint **/*.{js,jsx} --quiet", 11 | "test": "jest", 12 | "test:coverage": "jest --coverage", 13 | "test:update": "jest -u" 14 | }, 15 | "dependencies": { 16 | "@google-cloud/storage": "^1.2.1", 17 | "@google-cloud/vision": "^0.12.0", 18 | "axios": "0.16.1", 19 | "child-process-promise": "^2.2.0", 20 | "compression": "1.6.2", 21 | "cookie": "^0.3.1", 22 | "eslint-plugin-prettier": "^2.3.1", 23 | "exenv": "^1.2.2", 24 | "express": "4.15.3", 25 | "firebase": "^4.1.5", 26 | "firebase-admin": "~5.2.1", 27 | "firebase-functions": "^0.6.2", 28 | "firebaseui": "^2.3.0", 29 | "history": "^4.6.3", 30 | "js-cookie": "^2.1.4", 31 | "jss": "^8.1.0", 32 | "jss-preset-default": "^3.0.0", 33 | "latinize": "^0.4.0", 34 | "lodash": "4.17.4", 35 | "material-ui": "^1.0.0-beta.12", 36 | "material-ui-icons": "^1.0.0-beta.10", 37 | "mkdirp": "^0.5.1", 38 | "mkdirp-promise": "^4.0.0", 39 | "preact": "8.1.0", 40 | "preact-compat": "3.16.0", 41 | "prop-types": "15.5.10", 42 | "react": "15.6.1", 43 | "react-addons-perf": "15.4.2", 44 | "react-dom": "15.6.1", 45 | "react-firebaseui": "^1.0.5", 46 | "react-jss": "^7.1.0", 47 | "react-promise": "~1.1.1", 48 | "react-redux": "5.0.5", 49 | "react-redux-firebase": "2.0.0-beta.9", 50 | "react-router": "^4.1.2", 51 | "react-router-dom": "4.1.1", 52 | "react-router-redux": "^5.0.0-alpha.6", 53 | "redux": "3.6.0", 54 | "redux-thunk": "2.2.0", 55 | "styled-components": "^2.0.0", 56 | "xmlhttprequest": "^1.8.0" 57 | }, 58 | "devDependencies": { 59 | "autoprefixer": "^7.1.2", 60 | "babel-cli": "6.24.1", 61 | "babel-core": "6.24.1", 62 | "babel-eslint": "7.2.3", 63 | "babel-loader": "7.1.1", 64 | "babel-register": "6.24.1", 65 | "babel-plugin-css-modules-transform": "^1.2.7", 66 | "babel-plugin-dynamic-import-node": "1.0.2", 67 | "babel-plugin-syntax-dynamic-import": "6.18.0", 68 | "babel-plugin-transform-class-properties": "6.24.1", 69 | "babel-plugin-transform-decorators": "^6.24.1", 70 | "babel-plugin-transform-es2015-modules-commonjs": "6.24.1", 71 | "babel-plugin-transform-object-rest-spread": "^6.26.0", 72 | "babel-plugin-transform-postcss": "^0.3.0", 73 | "babel-preset-env": "1.5.1", 74 | "babel-preset-react": "6.24.1", 75 | "css-loader": "^0.28.5", 76 | "enzyme": "2.8.2", 77 | "eslint": "^4.2.0", 78 | "eslint-config-google": "0.9.1", 79 | "eslint-config-prettier": "2.3.0", 80 | "eslint-config-react": "1.1.7", 81 | "eslint-config-standard": "^10.2.1", 82 | "eslint-config-standard-react": "^5.0.0", 83 | "eslint-loader": "1.9.0", 84 | "eslint-plugin-babel": "^4.1.2", 85 | "eslint-plugin-flow": "2.29.1", 86 | "eslint-plugin-flowtype": "2.35.0", 87 | "eslint-plugin-import": "2.7.0", 88 | "eslint-plugin-jsx-a11y": "6.0.2", 89 | "eslint-plugin-node": "^5.1.1", 90 | "eslint-plugin-promise": "^3.5.0", 91 | "eslint-plugin-react": "^7.1.0", 92 | "eslint-plugin-standard": "^3.0.1", 93 | "extract-text-webpack-plugin": "^3.0.0", 94 | "firebase-tools": "^3.12.0", 95 | "flow-bin": "0.50.0", 96 | "html-webpack-plugin": "^2.30.1", 97 | "jest": "20.0.4", 98 | "jest-serializer-enzyme": "1.0.0", 99 | "moxios": "0.4.0", 100 | "nodemon": "1.11.0", 101 | "postcss": "^6.0.9", 102 | "postcss-import": "^10.0.0", 103 | "postcss-loader": "^2.0.6", 104 | "postcss-modules": "^0.8.0", 105 | "prettier": "1.5.2", 106 | "react-hot-loader": "3.0.0-beta.7", 107 | "react-test-renderer": "15.5.4", 108 | "string-replace-loader": "^1.3.0", 109 | "style-loader": "^0.18.2", 110 | "webpack": "^3.5.5", 111 | "webpack-shell-plugin": "^0.5.0" 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /public/firebase-messaging-sw.js: -------------------------------------------------------------------------------- 1 | // Import and configure the Firebase SDK 2 | // These scripts are made available when the app is served or deployed on Firebase Hosting 3 | // If you do not serve/host your project using Firebase Hosting see https://firebase.google.com/docs/web/setup 4 | importScripts('/__/firebase/4.1.3/firebase-app.js'); 5 | importScripts('/__/firebase/4.1.3/firebase-messaging.js'); 6 | importScripts('/__/firebase/init.js'); 7 | 8 | firebase.messaging(); 9 | -------------------------------------------------------------------------------- /public/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicolasgarnier/friendlypix-web-react/8b6550167004b9788d85f6e8a732ea2bb8ae9a7c/public/images/favicon.png -------------------------------------------------------------------------------- /public/images/silhouette.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicolasgarnier/friendlypix-web-react/8b6550167004b9788d85f6e8a732ea2bb8ae9a7c/public/images/silhouette.jpg -------------------------------------------------------------------------------- /public/images/touch/touch-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicolasgarnier/friendlypix-web-react/8b6550167004b9788d85f6e8a732ea2bb8ae9a7c/public/images/touch/touch-icon-120x120.png -------------------------------------------------------------------------------- /public/images/touch/touch-icon-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicolasgarnier/friendlypix-web-react/8b6550167004b9788d85f6e8a732ea2bb8ae9a7c/public/images/touch/touch-icon-128x128.png -------------------------------------------------------------------------------- /public/images/touch/touch-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicolasgarnier/friendlypix-web-react/8b6550167004b9788d85f6e8a732ea2bb8ae9a7c/public/images/touch/touch-icon-144x144.png -------------------------------------------------------------------------------- /public/images/touch/touch-icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicolasgarnier/friendlypix-web-react/8b6550167004b9788d85f6e8a732ea2bb8ae9a7c/public/images/touch/touch-icon-180x180.png -------------------------------------------------------------------------------- /public/images/touch/touch-icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicolasgarnier/friendlypix-web-react/8b6550167004b9788d85f6e8a732ea2bb8ae9a7c/public/images/touch/touch-icon-192x192.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Friendly Pix", 3 | "short_name": "FriendlyPix", 4 | "icons": [{ 5 | "src": "/images/touch/touch-icon-128x128.png", 6 | "sizes": "128x128", 7 | "type": "image/png" 8 | }, { 9 | "src": "/images/touch/touch-icon-180x180.png", 10 | "sizes": "180x180", 11 | "type": "image/png" 12 | }, { 13 | "src": "/images/touch/touch-icon-144x144.png", 14 | "sizes": "144x144", 15 | "type": "image/png" 16 | }, { 17 | "src": "/images/touch/touch-icon-192x192.png", 18 | "sizes": "192x192", 19 | "type": "image/png" 20 | }], 21 | "start_url": "/index.html", 22 | "display": "standalone", 23 | "orientation": "portrait", 24 | "gcm_sender_id": "103953800507" 25 | } 26 | -------------------------------------------------------------------------------- /public/old_scripts/auth.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2015 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 'use strict'; 17 | 18 | window.friendlyPix = window.friendlyPix || {}; 19 | 20 | /** 21 | * Handles the user auth flows and updating the UI depending on the auth state. 22 | */ 23 | friendlyPix.Auth = class { 24 | 25 | /** 26 | * Returns a Promise that completes when auth is ready. 27 | * @return Promise 28 | */ 29 | get waitForAuth() { 30 | return this._waitForAuthPromiseResolver.promise(); 31 | } 32 | 33 | /** 34 | * Initializes Friendly Pix's auth. 35 | * Binds the auth related UI components and handles the auth flow. 36 | * @constructor 37 | */ 38 | constructor() { 39 | // Firebase SDK 40 | this.database = firebase.database(); 41 | this.auth = firebase.auth(); 42 | this._waitForAuthPromiseResolver = new $.Deferred(); 43 | 44 | $(document).ready(() => { 45 | // Pointers to DOM Elements 46 | const signedInUserContainer = $('.fp-signed-in-user-container'); 47 | this.signedInUserAvatar = $('.fp-avatar', signedInUserContainer); 48 | this.signedInUsername = $('.fp-username', signedInUserContainer); 49 | this.signOutButton = $('.fp-sign-out'); 50 | this.signedOutOnlyElements = $('.fp-signed-out-only'); 51 | this.signedInOnlyElements = $('.fp-signed-in-only'); 52 | this.usernameLink = $('.fp-usernamelink'); 53 | 54 | // Event bindings 55 | this.signOutButton.click(() => this.auth.signOut()); 56 | this.signedInOnlyElements.hide(); 57 | }); 58 | 59 | this.auth.onAuthStateChanged(user => this.onAuthStateChanged(user)); 60 | } 61 | 62 | /** 63 | * Displays the signed-in user information in the UI or hides it and displays the 64 | * "Sign-In" button if the user isn't signed-in. 65 | */ 66 | onAuthStateChanged(user) { 67 | if (window.friendlyPix.router) { 68 | window.friendlyPix.router.reloadPage(); 69 | } 70 | this._waitForAuthPromiseResolver.resolve(); 71 | $(document).ready(() => { 72 | if (!user) { 73 | this.signedOutOnlyElements.show(); 74 | this.signedInOnlyElements.hide(); 75 | this.userId = null; 76 | this.signedInUserAvatar.css('background-image', ''); 77 | firebaseUi.start('#firebaseui-auth-container', uiConfig); 78 | } else { 79 | this.signedOutOnlyElements.hide(); 80 | this.signedInOnlyElements.show(); 81 | this.userId = user.uid; 82 | this.signedInUserAvatar.css('background-image', 83 | `url("${user.photoURL || '/images/silhouette.jpg'}")`); 84 | this.signedInUsername.text(user.displayName || 'Anonymous'); 85 | this.usernameLink.attr('href', `/user/${user.uid}`); 86 | friendlyPix.firebase.saveUserData(user.photoURL, user.displayName); 87 | } 88 | }); 89 | } 90 | }; 91 | 92 | friendlyPix.auth = new friendlyPix.Auth(); 93 | -------------------------------------------------------------------------------- /public/old_scripts/feed.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2015 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 'use strict'; 17 | 18 | window.friendlyPix = window.friendlyPix || {}; 19 | 20 | /** 21 | * Handles the Home and Feed UI. 22 | */ 23 | friendlyPix.Feed = class { 24 | 25 | /** 26 | * Initializes the Friendly Pix feeds. 27 | * @constructor 28 | */ 29 | constructor() { 30 | // List of all posts on the page. 31 | this.posts = []; 32 | // Map of posts that can be displayed. 33 | this.newPosts = {}; 34 | 35 | // Firebase SDK. 36 | this.auth = firebase.auth(); 37 | 38 | $(document).ready(() => { 39 | // Pointers to DOM elements. 40 | this.pageFeed = $('#page-feed'); 41 | this.feedImageContainer = $('.fp-image-container', this.pageFeed); 42 | this.noPostsMessage = $('.fp-no-posts', this.pageFeed); 43 | this.nextPageButton = $('.fp-next-page-button button'); 44 | this.newPostsButton = $('.fp-new-posts-button button'); 45 | 46 | // Event bindings. 47 | this.newPostsButton.click(() => this.showNewPosts()); 48 | }); 49 | } 50 | 51 | /** 52 | * Appends the given list of `posts`. 53 | */ 54 | addPosts(posts) { 55 | // Displays the list of posts 56 | const postIds = Object.keys(posts); 57 | for (let i = postIds.length - 1; i >= 0; i--) { 58 | this.noPostsMessage.hide(); 59 | const postData = posts[postIds[i]]; 60 | const post = new friendlyPix.Post(); 61 | this.posts.push(post); 62 | const postElement = post.fillPostData(postIds[i], postData.thumb_url || postData.url, 63 | postData.text, postData.author, postData.timestamp, null, null, postData.full_url); 64 | // If a post with similar ID is already in the feed we replace it instead of appending. 65 | const existingPostElement = $(`.fp-post-${postIds[i]}`, this.feedImageContainer); 66 | if (existingPostElement.length) { 67 | existingPostElement.replaceWith(postElement); 68 | } else { 69 | this.feedImageContainer.append(postElement.addClass(`fp-post-${postIds[i]}`)); 70 | } 71 | } 72 | } 73 | 74 | /** 75 | * Shows the "load next page" button and binds it the `nextPage` callback. If `nextPage` is `null` 76 | * then the button is hidden. 77 | */ 78 | toggleNextPageButton(nextPage) { 79 | this.nextPageButton.unbind('click'); 80 | if (nextPage) { 81 | const loadMorePosts = () => { 82 | this.nextPageButton.prop('disabled', true); 83 | console.log('Loading next page of posts.'); 84 | nextPage().then(data => { 85 | this.addPosts(data.entries); 86 | this.toggleNextPageButton(data.nextPage); 87 | }); 88 | }; 89 | this.nextPageButton.show(); 90 | // Enable infinite Scroll. 91 | friendlyPix.MaterialUtils.onEndScroll(100).then(loadMorePosts); 92 | this.nextPageButton.prop('disabled', false); 93 | this.nextPageButton.click(loadMorePosts); 94 | } else { 95 | this.nextPageButton.hide(); 96 | } 97 | } 98 | 99 | /** 100 | * Prepends the list of new posts stored in `this.newPosts`. This happens when the user clicks on 101 | * the "Show new posts" button. 102 | */ 103 | showNewPosts() { 104 | const newPosts = this.newPosts; 105 | this.newPosts = {}; 106 | this.newPostsButton.hide(); 107 | const postKeys = Object.keys(newPosts); 108 | 109 | for (let i = 0; i < postKeys.length; i++) { 110 | this.noPostsMessage.hide(); 111 | const post = newPosts[postKeys[i]]; 112 | const postElement = new friendlyPix.Post(); 113 | this.posts.push(postElement); 114 | this.feedImageContainer.prepend(postElement.fillPostData(postKeys[i], post.thumb_url || 115 | post.url, post.text, post.author, post.timestamp, null, null, post.full_url)); 116 | } 117 | } 118 | 119 | /** 120 | * Displays the general posts feed. 121 | */ 122 | showGeneralFeed() { 123 | // Clear previously displayed posts if any. 124 | this.clear(); 125 | 126 | // Load initial batch of posts. 127 | friendlyPix.firebase.getPosts().then(data => { 128 | // Listen for new posts. 129 | const latestPostId = Object.keys(data.entries)[Object.keys(data.entries).length - 1]; 130 | friendlyPix.firebase.subscribeToGeneralFeed( 131 | (postId, postValue) => this.addNewPost(postId, postValue), latestPostId); 132 | 133 | // Adds fetched posts and next page button if necessary. 134 | this.addPosts(data.entries); 135 | this.toggleNextPageButton(data.nextPage); 136 | }); 137 | 138 | // Listen for posts deletions. 139 | friendlyPix.firebase.registerForPostsDeletion(postId => this.onPostDeleted(postId)); 140 | } 141 | 142 | /** 143 | * Shows the feed showing all followed users. 144 | */ 145 | showHomeFeed() { 146 | // Clear previously displayed posts if any. 147 | this.clear(); 148 | 149 | if (this.auth.currentUser) { 150 | // Make sure the home feed is updated with followed users's new posts. 151 | friendlyPix.firebase.updateHomeFeeds().then(() => { 152 | // Load initial batch of posts. 153 | friendlyPix.firebase.getHomeFeedPosts().then(data => { 154 | const postIds = Object.keys(data.entries); 155 | if (postIds.length === 0) { 156 | this.noPostsMessage.fadeIn(); 157 | } 158 | // Listen for new posts. 159 | const latestPostId = postIds[postIds.length - 1]; 160 | friendlyPix.firebase.subscribeToHomeFeed( 161 | (postId, postValue) => { 162 | this.addNewPost(postId, postValue); 163 | }, latestPostId); 164 | 165 | // Adds fetched posts and next page button if necessary. 166 | this.addPosts(data.entries); 167 | this.toggleNextPageButton(data.nextPage); 168 | }); 169 | 170 | // Add new posts from followers live. 171 | friendlyPix.firebase.startHomeFeedLiveUpdaters(); 172 | 173 | // Listen for posts deletions. 174 | friendlyPix.firebase.registerForPostsDeletion(postId => this.onPostDeleted(postId)); 175 | }); 176 | } 177 | } 178 | 179 | /** 180 | * Triggered when a post has been deleted. 181 | */ 182 | onPostDeleted(postId) { 183 | // Potentially remove post from in-memory new post list. 184 | if (this.newPosts[postId]) { 185 | delete this.newPosts[postId]; 186 | const nbNewPosts = Object.keys(this.newPosts).length; 187 | this.newPostsButton.text(`Display ${nbNewPosts} new posts`); 188 | if (nbNewPosts === 0) { 189 | this.newPostsButton.hide(); 190 | } 191 | } 192 | 193 | // Potentially delete from the UI. 194 | $(`.fp-post-${postId}`, this.pageFeed).remove(); 195 | } 196 | 197 | /** 198 | * Adds a new post to display in the queue. 199 | */ 200 | addNewPost(postId, postValue) { 201 | this.newPosts[postId] = postValue; 202 | this.newPostsButton.text(`Display ${Object.keys(this.newPosts).length} new posts`); 203 | this.newPostsButton.show(); 204 | } 205 | 206 | /** 207 | * Clears the UI. 208 | */ 209 | clear() { 210 | // Delete the existing posts if any. 211 | $('.fp-post', this.feedImageContainer).remove(); 212 | 213 | // Hides the "next page" and "new posts" buttons. 214 | this.nextPageButton.hide(); 215 | this.newPostsButton.hide(); 216 | 217 | // Remove any click listener on the next page button. 218 | this.nextPageButton.unbind('click'); 219 | 220 | // Stops then infinite scrolling listeners. 221 | friendlyPix.MaterialUtils.stopOnEndScrolls(); 222 | 223 | // Clears the list of upcoming posts to display. 224 | this.newPosts = {}; 225 | 226 | // Displays the help message for empty feeds. 227 | this.noPostsMessage.hide(); 228 | 229 | // Remove Firebase listeners. 230 | friendlyPix.firebase.cancelAllSubscriptions(); 231 | 232 | // Stops all timers if any. 233 | this.posts.forEach(post => post.clear()); 234 | this.posts = []; 235 | } 236 | }; 237 | 238 | friendlyPix.feed = new friendlyPix.Feed(); 239 | -------------------------------------------------------------------------------- /public/old_scripts/messaging.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2015 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 'use strict'; 17 | 18 | window.friendlyPix = window.friendlyPix || {}; 19 | 20 | /** 21 | * Handles notifications. 22 | */ 23 | friendlyPix.Messaging = class { 24 | 25 | /** 26 | * Inititializes the notifications utility. 27 | * @constructor 28 | */ 29 | constructor() { 30 | // Firebase SDK 31 | this.database = firebase.database(); 32 | this.auth = firebase.auth(); 33 | this.storage = firebase.storage(); 34 | this.messaging = firebase.messaging(); 35 | 36 | $(document).ready(() => { 37 | // DOM Elements 38 | this.enableNotificationsContainer = $('.fp-notifications'); 39 | this.enableNotificationsCheckbox = $('#notifications'); 40 | this.enableNotificationsLabel = $('.mdl-switch__label', this.enableNotificationsContainer); 41 | 42 | this.toast = $('.mdl-js-snackbar'); 43 | 44 | // Event bindings 45 | this.enableNotificationsCheckbox.change(() => this.onEnableNotificationsChange()); 46 | this.auth.onAuthStateChanged(() => this.trackNotificationsEnabledStatus()); 47 | this.messaging.onTokenRefresh(() => this.saveToken()); 48 | this.messaging.onMessage(payload => this.onMessage(payload)); 49 | }); 50 | } 51 | 52 | /** 53 | * Saves the token to the database if available. If not request permissions. 54 | */ 55 | saveToken() { 56 | this.messaging.getToken().then(currentToken => { 57 | if (currentToken) { 58 | friendlyPix.firebase.saveNotificationToken(currentToken).then(() => { 59 | console.log('Notification Token saved to database'); 60 | }); 61 | } else { 62 | this.requestPermission(); 63 | } 64 | }).catch(err => { 65 | console.error('Unable to get messaging token.', err); 66 | }); 67 | } 68 | 69 | /** 70 | * Requests permission to send notifications on this browser. 71 | */ 72 | requestPermission() { 73 | console.log('Requesting permission...'); 74 | this.messaging.requestPermission().then(() => { 75 | console.log('Notification permission granted.'); 76 | this.saveToken(); 77 | }).catch(err => { 78 | console.error('Unable to get permission to notify.', err); 79 | }); 80 | } 81 | 82 | /** 83 | * Called when the app is in focus. 84 | */ 85 | onMessage(payload) { 86 | console.log('Notifications received.', payload); 87 | 88 | // If we get a notification while focus on the app 89 | if (payload.notification) { 90 | const userId = payload.notification.click_action.split('/user/')[1]; 91 | 92 | let data = { 93 | message: payload.notification.body, 94 | actionHandler: () => page(`/user/${userId}`), 95 | actionText: 'Profile', 96 | timeout: 10000 97 | }; 98 | this.toast[0].MaterialSnackbar.showSnackbar(data); 99 | } 100 | } 101 | 102 | /** 103 | * Triggered when the user changes the "Notifications Enabled" checkbox. 104 | */ 105 | onEnableNotificationsChange() { 106 | const checked = this.enableNotificationsCheckbox.prop('checked'); 107 | this.enableNotificationsCheckbox.prop('disabled', true); 108 | 109 | return friendlyPix.firebase.toggleNotificationEnabled(checked); 110 | } 111 | 112 | /** 113 | * Starts tracking the "Notifications Enabled" checkbox status. 114 | */ 115 | trackNotificationsEnabledStatus() { 116 | if (this.auth.currentUser) { 117 | friendlyPix.firebase.registerToNotificationEnabledStatusUpdate(data => { 118 | this.enableNotificationsCheckbox.prop('checked', data.val() !== null); 119 | this.enableNotificationsCheckbox.prop('disabled', false); 120 | this.enableNotificationsLabel.text(data.val() ? 'Notifications Enabled' : 'Enable Notifications'); 121 | friendlyPix.MaterialUtils.refreshSwitchState(this.enableNotificationsContainer); 122 | 123 | if (data.val()) { 124 | this.saveToken(); 125 | } 126 | }); 127 | } 128 | } 129 | }; 130 | 131 | friendlyPix.messaging = new friendlyPix.Messaging(); 132 | -------------------------------------------------------------------------------- /public/old_scripts/post.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2015 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 'use strict'; 17 | 18 | window.friendlyPix = window.friendlyPix || {}; 19 | 20 | /** 21 | * Handles the single post UI. 22 | */ 23 | friendlyPix.Post = class { 24 | /** 25 | * Initializes the single post's UI. 26 | * @constructor 27 | */ 28 | constructor() { 29 | // List of all times running on the page. 30 | this.timers = []; 31 | 32 | // Firebase SDK. 33 | this.database = firebase.database(); 34 | this.storage = firebase.storage(); 35 | this.auth = firebase.auth(); 36 | 37 | $(document).ready(() => { 38 | this.postPage = $('#page-post'); 39 | // Pointers to DOM elements. 40 | this.postElement = $(friendlyPix.Post.createPostHtml()); 41 | friendlyPix.MaterialUtils.upgradeTextFields(this.postElement); 42 | this.toast = $('.mdl-js-snackbar'); 43 | this.theatre = $('.fp-theatre'); 44 | }); 45 | } 46 | 47 | /** 48 | * Loads the given post's details. 49 | */ 50 | loadPost(postId) { 51 | // Load the posts information. 52 | friendlyPix.firebase.getPostData(postId).then(snapshot => { 53 | const post = snapshot.val(); 54 | // Clear listeners and previous post data. 55 | this.clear(); 56 | if (!post) { 57 | var data = { 58 | message: 'This post does not exists.', 59 | timeout: 5000 60 | }; 61 | this.toast[0].MaterialSnackbar.showSnackbar(data); 62 | if (this.auth.currentUser) { 63 | page(`/user/${this.auth.currentUser.uid}`); 64 | } else { 65 | page(`/feed`); 66 | } 67 | } else { 68 | this.fillPostData(snapshot.key, post.thumb_url || post.url, post.text, post.author, 69 | post.timestamp, post.thumb_storage_uri, post.full_storage_uri, post.full_url); 70 | } 71 | }); 72 | } 73 | 74 | /** 75 | * Clears all listeners and timers in the given element. 76 | */ 77 | clear() { 78 | // Stops all timers if any. 79 | this.timers.forEach(timer => clearInterval(timer)); 80 | this.timers = []; 81 | 82 | // Remove Firebase listeners. 83 | friendlyPix.firebase.cancelAllSubscriptions(); 84 | } 85 | 86 | /** 87 | * Displays the given list of `comments` in the post. 88 | */ 89 | displayComments(comments) { 90 | const commentsIds = Object.keys(comments); 91 | for (let i = commentsIds.length - 1; i >= 0; i--) { 92 | $('.fp-comments', this.postElement).prepend( 93 | friendlyPix.Post.createCommentHtml(comments[commentsIds[i]].author, 94 | comments[commentsIds[i]].text)); 95 | } 96 | } 97 | 98 | /** 99 | * Shows the "show more comments" button and binds it the `nextPage` callback. If `nextPage` is 100 | * `null` then the button is hidden. 101 | */ 102 | displayNextPageButton(nextPage) { 103 | const nextPageButton = $('.fp-morecomments', this.postElement); 104 | if (nextPage) { 105 | nextPageButton.show(); 106 | nextPageButton.unbind('click'); 107 | nextPageButton.prop('disabled', false); 108 | nextPageButton.click(() => nextPage().then(data => { 109 | nextPageButton.prop('disabled', true); 110 | this.displayComments(data.entries); 111 | this.displayNextPageButton(data.nextPage); 112 | })); 113 | } else { 114 | nextPageButton.hide(); 115 | } 116 | } 117 | 118 | /** 119 | * Fills the post's Card with the given details. 120 | * Also sets all auto updates and listeners on the UI elements of the post. 121 | */ 122 | fillPostData(postId, thumbUrl, imageText, author, timestamp, thumbStorageUri, picStorageUri, picUrl) { 123 | const post = this.postElement; 124 | 125 | // Fills element's author profile. 126 | $('.fp-usernamelink', post).attr('href', `/user/${author.uid}`); 127 | $('.fp-avatar', post).css('background-image', 128 | `url(${author.profile_picture || '/images/silhouette.jpg'})`); 129 | $('.fp-username', post).text(author.full_name || 'Anonymous'); 130 | 131 | // Shows the pic's thumbnail. 132 | this._setupThumb(thumbUrl, picUrl); 133 | 134 | // Make sure we update if the thumb or pic URL changes. 135 | friendlyPix.firebase.registerForThumbChanges(postId, thumbUrl => { 136 | this._setupThumb(thumbUrl, picUrl); 137 | }); 138 | 139 | this._setupDate(postId, timestamp); 140 | this._setupDeleteButton(postId, author, picStorageUri, thumbStorageUri); 141 | this._setupLikeCountAndStatus(postId); 142 | this._setupComments(postId, author, imageText); 143 | return post; 144 | } 145 | 146 | /** 147 | * Leaves the theatre mode. 148 | */ 149 | leaveTheatreMode() { 150 | this.theatre.hide(); 151 | this.theatre.off('click'); 152 | $(document).off('keydown'); 153 | } 154 | 155 | /** 156 | * Leaves the theatre mode. 157 | */ 158 | enterTheatreMode(picUrl) { 159 | $('.fp-fullpic', this.theatre).prop('src', picUrl); 160 | this.theatre.css('display', 'flex'); 161 | // Leave theatre mode if click or ESC key down. 162 | this.theatre.off('click'); 163 | this.theatre.click(() => this.leaveTheatreMode()) 164 | $(document).off('keydown'); 165 | $(document).keydown(e => { 166 | if (e.which === 27) { 167 | this.leaveTheatreMode(); 168 | } 169 | }); 170 | } 171 | 172 | /** 173 | * Shows the thumbnail and sets up the click to see the full size image. 174 | * @private 175 | */ 176 | _setupThumb(thumbUrl, picUrl) { 177 | const post = this.postElement; 178 | 179 | $('.fp-image', post).css('background-image', `url("${thumbUrl ? thumbUrl.replace(/"/g, '\\"') : ''}")`); 180 | $('.fp-image', post).unbind('click'); 181 | $('.fp-image', post).click(() => this.enterTheatreMode(picUrl || thumbUrl)); 182 | } 183 | 184 | /** 185 | * Shows the publishing date of the post and updates this date live. 186 | * @private 187 | */ 188 | _setupDate(postId, timestamp) { 189 | const post = this.postElement; 190 | 191 | $('.fp-time', post).attr('href', `/post/${postId}`); 192 | $('.fp-time', post).text(friendlyPix.Post.getTimeText(timestamp)); 193 | // Update the time counter every minutes. 194 | this.timers.push(setInterval( 195 | () => $('.fp-time', post).text(friendlyPix.Post.getTimeText(timestamp)), 60000)); 196 | } 197 | 198 | /** 199 | * Shows comments and binds actions to the comments form. 200 | * @private 201 | */ 202 | _setupComments(postId, author, imageText) { 203 | const post = this.postElement; 204 | 205 | // Creates the initial comment with the post's text. 206 | $('.fp-first-comment', post).empty(); 207 | $('.fp-first-comment', post).append(friendlyPix.Post.createCommentHtml(author, imageText)); 208 | 209 | // Load first page of comments and listen to new comments. 210 | $('.fp-comments', post).empty(); 211 | friendlyPix.firebase.getComments(postId).then(data => { 212 | this.displayComments(data.entries); 213 | this.displayNextPageButton(data.nextPage); 214 | 215 | // Display any new comments. 216 | const commentIds = Object.keys(data.entries); 217 | friendlyPix.firebase.subscribeToComments(postId, (commentId, commentData) => { 218 | $('.fp-comments', post).append( 219 | friendlyPix.Post.createCommentHtml(commentData.author, commentData.text)); 220 | }, commentIds ? commentIds[commentIds.length - 1] : 0); 221 | }); 222 | 223 | if (this.auth.currentUser) { 224 | // Bind comments form posting. 225 | $('.fp-add-comment', post).off('submit'); 226 | $('.fp-add-comment', post).submit(e => { 227 | e.preventDefault(); 228 | const commentText = $(`.mdl-textfield__input`, post).val(); 229 | if (!commentText || commentText.length === 0) { 230 | return; 231 | } 232 | friendlyPix.firebase.addComment(postId, commentText); 233 | $(`.mdl-textfield__input`, post).val(''); 234 | }); 235 | const ran = Math.floor(Math.random() * 10000000); 236 | $('.mdl-textfield__input', post).attr('id', `${postId}-${ran}-comment`); 237 | $('.mdl-textfield__label', post).attr('for', `${postId}-${ran}-comment`); 238 | // Show comments form. 239 | $('.fp-action', post).css('display', 'flex'); 240 | } 241 | } 242 | 243 | /** 244 | * Shows/Hode and binds actions to the Delete button. 245 | * @private 246 | */ 247 | _setupDeleteButton(postId, author, picStorageUri, thumbStorageUri) { 248 | const post = this.postElement; 249 | 250 | if (this.auth.currentUser && this.auth.currentUser.uid === author.uid && picStorageUri) { 251 | $('.fp-delete-post', post).show(); 252 | $('.fp-delete-post', post).off('click'); 253 | $('.fp-delete-post', post).click(() => { 254 | swal({ 255 | title: 'Are you sure?', 256 | text: 'You will not be able to recover this post!', 257 | type: 'warning', 258 | showCancelButton: true, 259 | confirmButtonColor: '#DD6B55', 260 | confirmButtonText: 'Yes, delete it!', 261 | closeOnConfirm: false, 262 | showLoaderOnConfirm: true, 263 | allowEscapeKey: true 264 | }, () => { 265 | $('.fp-delete-post', post).prop('disabled', true); 266 | friendlyPix.firebase.deletePost(postId, picStorageUri, thumbStorageUri).then(() => { 267 | swal({ 268 | title: 'Deleted!', 269 | text: 'Your post has been deleted.', 270 | type: 'success', 271 | timer: 2000 272 | }); 273 | $('.fp-delete-post', post).prop('disabled', false); 274 | page(`/user/${this.auth.currentUser.uid}`); 275 | }).catch(error => { 276 | swal.close(); 277 | $('.fp-delete-post', post).prop('disabled', false); 278 | const data = { 279 | message: `There was an error deleting your post: ${error}`, 280 | timeout: 5000 281 | }; 282 | this.toast[0].MaterialSnackbar.showSnackbar(data); 283 | }); 284 | }); 285 | }); 286 | } else { 287 | $('.fp-delete-post', post).hide(); 288 | } 289 | } 290 | 291 | /** 292 | * Starts Likes count listener and on/off like status. 293 | * @private 294 | */ 295 | _setupLikeCountAndStatus(postId) { 296 | const post = this.postElement; 297 | 298 | if (this.auth.currentUser) { 299 | // Listen to like status. 300 | friendlyPix.firebase.registerToUserLike(postId, isliked => { 301 | if (isliked) { 302 | $('.fp-liked', post).show(); 303 | $('.fp-not-liked', post).hide(); 304 | } else { 305 | $('.fp-liked', post).hide(); 306 | $('.fp-not-liked', post).show(); 307 | } 308 | }); 309 | 310 | // Add event listeners. 311 | $('.fp-liked', post).off('click'); 312 | $('.fp-liked', post).click(() => friendlyPix.firebase.updateLike(postId, false)); 313 | $('.fp-not-liked', post).off('click'); 314 | $('.fp-not-liked', post).click(() => friendlyPix.firebase.updateLike(postId, true)); 315 | } else { 316 | $('.fp-liked', post).hide(); 317 | $('.fp-not-liked', post).hide(); 318 | $('.fp-action', post).hide(); 319 | } 320 | 321 | // Listen to number of Likes. 322 | friendlyPix.firebase.registerForLikesCount(postId, nbLikes => { 323 | if (nbLikes > 0) { 324 | $('.fp-likes', post).show(); 325 | $('.fp-likes', post).text(nbLikes + ' like' + (nbLikes === 1 ? '' : 's')); 326 | } else { 327 | $('.fp-likes', post).hide(); 328 | } 329 | }); 330 | } 331 | 332 | /** 333 | * Returns the HTML for a post's comment. 334 | */ 335 | static createPostHtml() { 336 | return ` 337 |
339 |
341 |
342 | 343 |
344 |
345 |
346 | 347 | 350 | now 351 |
352 |
353 |
0 likes
354 |
355 |
View more comments...
356 |
357 |
358 | 359 |
favorite_border
360 |
favorite
361 |
362 |
363 |
364 | 365 | 366 |
367 |
368 |
369 |
370 |
`; 371 | } 372 | 373 | /** 374 | * Returns the HTML for a post's comment. 375 | */ 376 | static createCommentHtml(author, text) { 377 | return ` 378 |
379 | ${author.full_name || 'Anonymous'}: 380 | ${text} 381 |
`; 382 | } 383 | 384 | /** 385 | * Given the time of creation of a post returns how long since the creation of the post in text 386 | * format. e.g. 5d, 10h, now... 387 | */ 388 | static getTimeText(postCreationTimestamp) { 389 | let millis = Date.now() - postCreationTimestamp; 390 | const ms = millis % 1000; 391 | millis = (millis - ms) / 1000; 392 | const secs = millis % 60; 393 | millis = (millis - secs) / 60; 394 | const mins = millis % 60; 395 | millis = (millis - mins) / 60; 396 | const hrs = millis % 24; 397 | const days = (millis - hrs) / 24; 398 | var timeSinceCreation = [days, hrs, mins, secs, ms]; 399 | 400 | let timeText = 'Now'; 401 | if (timeSinceCreation[0] !== 0) { 402 | timeText = timeSinceCreation[0] + 'd'; 403 | } else if (timeSinceCreation[1] !== 0) { 404 | timeText = timeSinceCreation[1] + 'h'; 405 | } else if (timeSinceCreation[2] !== 0) { 406 | timeText = timeSinceCreation[2] + 'm'; 407 | } 408 | return timeText; 409 | } 410 | }; 411 | 412 | friendlyPix.post = new friendlyPix.Post(); 413 | 414 | $(document).ready(() => { 415 | // We add the Post element to the single post page. 416 | $('.fp-image-container', friendlyPix.post.postPage).append(friendlyPix.post.postElement); 417 | }); 418 | -------------------------------------------------------------------------------- /public/old_scripts/routing.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2015 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 'use strict'; 17 | 18 | window.friendlyPix = window.friendlyPix || {}; 19 | 20 | /** 21 | * Handles the pages/routing. 22 | */ 23 | friendlyPix.Router = class { 24 | 25 | /** 26 | * Initializes the Friendly Pix controller/router. 27 | * @constructor 28 | */ 29 | constructor() { 30 | $(document).ready(() => { 31 | friendlyPix.auth.waitForAuth.then(() => { 32 | // Dom elements. 33 | this.pagesElements = $('[id^=page-]'); 34 | this.splashLogin = $('#login', '#page-splash'); 35 | 36 | // Make sure /add is never opened on website load. 37 | if (window.location.pathname === '/add') { 38 | page('/'); 39 | } 40 | 41 | // Configuring routes. 42 | const pipe = friendlyPix.Router.pipe; 43 | const displayPage = this.displayPage.bind(this); 44 | const loadUser = userId => friendlyPix.userPage.loadUser(userId); 45 | const showHomeFeed = () => friendlyPix.feed.showHomeFeed(); 46 | const showGeneralFeed = () => friendlyPix.feed.showGeneralFeed(); 47 | const clearFeed = () => friendlyPix.feed.clear(); 48 | const showPost = postId => friendlyPix.post.loadPost(postId); 49 | 50 | page('/', pipe(showHomeFeed, null, true), 51 | pipe(displayPage, {pageId: 'feed', onlyAuthed: true})); 52 | page('/feed', pipe(showGeneralFeed, null, true), pipe(displayPage, {pageId: 'feed'})); 53 | page('/post/:postId', pipe(showPost, null, true), pipe(displayPage, {pageId: 'post'})); 54 | page('/user/:userId', pipe(loadUser, null, true), pipe(displayPage, {pageId: 'user-info'})); 55 | page('/about', pipe(clearFeed, null, true), pipe(displayPage, {pageId: 'about'})); 56 | page('/add', pipe(displayPage, {pageId: 'add', onlyAuthed: true})); 57 | page('*', () => page('/')); 58 | 59 | // Start routing. 60 | page(); 61 | }); 62 | }); 63 | } 64 | 65 | /** 66 | * Returns a function that displays the given page and hides the other ones. 67 | * if `onlyAuthed` is set to true then the splash page will be displayed instead of the page if 68 | * the user is not signed-in. 69 | */ 70 | displayPage(attributes, context) { 71 | const onlyAuthed = attributes.onlyAuthed; 72 | let pageId = attributes.pageId; 73 | 74 | if (onlyAuthed && !firebase.auth().currentUser) { 75 | pageId = 'splash'; 76 | this.splashLogin.show(); 77 | } 78 | friendlyPix.Router.setLinkAsActive(context.canonicalPath); 79 | this.pagesElements.each(function(index, element) { 80 | if (element.id === 'page-' + pageId) { 81 | $(element).show(); 82 | } else if (element.id === 'page-splash') { 83 | $(element).fadeOut(1000); 84 | } else { 85 | $(element).hide(); 86 | } 87 | }); 88 | friendlyPix.MaterialUtils.closeDrawer(); 89 | friendlyPix.Router.scrollToTop(); 90 | } 91 | 92 | /** 93 | * Reloads the current page. 94 | */ 95 | reloadPage() { 96 | let path = window.location.pathname; 97 | if (path === '') { 98 | path = '/'; 99 | } 100 | page(path); 101 | } 102 | 103 | /** 104 | * Scrolls the page to top. 105 | */ 106 | static scrollToTop() { 107 | $('html,body').animate({scrollTop: 0}, 0); 108 | } 109 | 110 | /** 111 | * Pipes the given function and passes the given attribute and Page.js context. 112 | * Set 'optContinue' to true if there are further functions to call. 113 | */ 114 | static pipe(funct, attribute, optContinue) { 115 | return (context, next) => { 116 | if (funct) { 117 | const params = Object.keys(context.params); 118 | if (!attribute && params.length > 0) { 119 | funct(context.params[params[0]], context); 120 | } else { 121 | funct(attribute, context); 122 | } 123 | } 124 | if (optContinue) { 125 | next(); 126 | } 127 | }; 128 | } 129 | 130 | /** 131 | * Highlights the correct menu item/link. 132 | */ 133 | static setLinkAsActive(canonicalPath) { 134 | if (canonicalPath === '') { 135 | canonicalPath = '/'; 136 | } 137 | $('.is-active').removeClass('is-active'); 138 | $(`[href="${canonicalPath}"]`).addClass('is-active'); 139 | } 140 | }; 141 | 142 | friendlyPix.router = new friendlyPix.Router(); 143 | -------------------------------------------------------------------------------- /public/old_scripts/search.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2015 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 'use strict'; 17 | 18 | window.friendlyPix = window.friendlyPix || {}; 19 | 20 | /** 21 | * Handles the Friendly Pix search feature. 22 | */ 23 | friendlyPix.Search = class { 24 | 25 | /** 26 | * The minimum number of characters to trigger a search. 27 | * @return {number} 28 | */ 29 | static get MIN_CHARACTERS() { 30 | return 3; 31 | } 32 | 33 | /** 34 | * The maximum number of search results to be displayed. 35 | * @return {number} 36 | */ 37 | static get NB_RESULTS_LIMIT() { 38 | return 10; 39 | } 40 | 41 | /** 42 | * Initializes the Friendly Pix search bar. 43 | */ 44 | constructor() { 45 | // Firebase SDK. 46 | this.database = firebase.database(); 47 | 48 | $(document).ready(() => { 49 | // DOM Elements pointers. 50 | this.searchField = $('#searchQuery'); 51 | this.searchResults = $('#fp-searchResults'); 52 | 53 | // Event bindings. 54 | this.searchField.keyup(() => this.displaySearchResults()); 55 | this.searchField.focus(() => this.displaySearchResults()); 56 | this.searchField.click(() => this.displaySearchResults()); 57 | }); 58 | } 59 | 60 | /** 61 | * Display search results. 62 | */ 63 | displaySearchResults() { 64 | const searchString = this.searchField.val().toLowerCase().trim(); 65 | if (searchString.length >= friendlyPix.Search.MIN_CHARACTERS) { 66 | friendlyPix.firebase.searchUsers(searchString, friendlyPix.Search.NB_RESULTS_LIMIT).then( 67 | results => { 68 | this.searchResults.empty(); 69 | const peopleIds = Object.keys(results); 70 | if (peopleIds.length > 0) { 71 | this.searchResults.fadeIn(); 72 | $('html').click(() => { 73 | $('html').unbind('click'); 74 | this.searchResults.fadeOut(); 75 | }); 76 | peopleIds.forEach(peopleId => { 77 | const profile = results[peopleId]; 78 | this.searchResults.append( 79 | friendlyPix.Search.createSearchResultHtml(peopleId, profile)); 80 | }); 81 | } else { 82 | this.searchResults.fadeOut(); 83 | } 84 | }); 85 | } else { 86 | this.searchResults.empty(); 87 | this.searchResults.fadeOut(); 88 | } 89 | } 90 | 91 | /** 92 | * Returns the HTML for a single search result 93 | */ 94 | static createSearchResultHtml(peopleId, peopleProfile) { 95 | return ` 96 | 97 |
99 |
${peopleProfile.full_name}
100 |
`; 101 | } 102 | }; 103 | 104 | friendlyPix.search = new friendlyPix.Search(); 105 | -------------------------------------------------------------------------------- /public/old_scripts/uploader.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2015 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 'use strict'; 17 | 18 | window.friendlyPix = window.friendlyPix || {}; 19 | 20 | /** 21 | * Handles uploads of new pics. 22 | */ 23 | friendlyPix.Uploader = class { 24 | 25 | /** 26 | * @return {number} 27 | */ 28 | static get FULL_IMAGE_SPECS() { 29 | return { 30 | maxDimension: 1280, 31 | quality: 0.9 32 | }; 33 | } 34 | 35 | /** 36 | * @return {number} 37 | */ 38 | static get THUMB_IMAGE_SPECS() { 39 | return { 40 | maxDimension: 640, 41 | quality: 0.7 42 | }; 43 | } 44 | 45 | /** 46 | * Inititializes the pics uploader/post creator. 47 | * @constructor 48 | */ 49 | constructor() { 50 | // Firebase SDK 51 | this.database = firebase.database(); 52 | this.auth = firebase.auth(); 53 | this.storage = firebase.storage(); 54 | 55 | this.addPolyfills(); 56 | 57 | $(document).ready(() => { 58 | // DOM Elements 59 | this.addButton = $('#add'); 60 | this.addButtonFloating = $('#add-floating'); 61 | this.imageInput = $('#fp-mediacapture'); 62 | this.overlay = $('.fp-overlay', '#page-add'); 63 | this.newPictureContainer = $('#newPictureContainer'); 64 | this.uploadButton = $('.fp-upload'); 65 | this.imageCaptionInput = $('#imageCaptionInput'); 66 | this.uploadPicForm = $('#uploadPicForm'); 67 | this.toast = $('.mdl-js-snackbar'); 68 | 69 | // Event bindings 70 | this.addButton.click(() => this.initiatePictureCapture()); 71 | this.addButtonFloating.click(() => this.initiatePictureCapture()); 72 | this.imageInput.change(e => this.readPicture(e)); 73 | this.uploadPicForm.submit(e => this.uploadPic(e)); 74 | this.imageCaptionInput.keyup(() => this.uploadButton.prop('disabled', !this.imageCaptionInput.val())); 75 | }); 76 | } 77 | 78 | // Adds polyfills required for the Uploader. 79 | addPolyfills() { 80 | // Polyfill for canvas.toBlob(). 81 | if (!HTMLCanvasElement.prototype.toBlob) { 82 | Object.defineProperty(HTMLCanvasElement.prototype, 'toBlob', { 83 | value: function(callback, type, quality) { 84 | var binStr = atob(this.toDataURL(type, quality).split(',')[1]); 85 | var len = binStr.length; 86 | var arr = new Uint8Array(len); 87 | 88 | for (var i = 0; i < len; i++) { 89 | arr[i] = binStr.charCodeAt(i); 90 | } 91 | 92 | callback(new Blob([arr], {type: type || 'image/png'})); 93 | } 94 | }); 95 | } 96 | } 97 | 98 | /** 99 | * Start taking a picture. 100 | */ 101 | initiatePictureCapture() { 102 | this.imageInput.trigger('click'); 103 | } 104 | 105 | /** 106 | * Displays the given pic in the New Pic Upload dialog. 107 | */ 108 | displayPicture(url) { 109 | this.newPictureContainer.attr('src', url); 110 | page('/add'); 111 | this.imageCaptionInput.focus(); 112 | this.uploadButton.prop('disabled', true); 113 | } 114 | 115 | /** 116 | * Enables or disables the UI. Typically while the image is uploading. 117 | */ 118 | disableUploadUi(disabled) { 119 | this.uploadButton.prop('disabled', disabled); 120 | this.addButton.prop('disabled', disabled); 121 | this.addButtonFloating.prop('disabled', disabled); 122 | this.imageCaptionInput.prop('disabled', disabled); 123 | this.overlay.toggle(disabled); 124 | } 125 | 126 | /** 127 | * Reads the picture the has been selected by the file picker. 128 | */ 129 | readPicture(event) { 130 | this.clear(); 131 | 132 | var file = event.target.files[0]; // FileList object 133 | this.currentFile = file; 134 | 135 | // Clear the selection in the file picker input. 136 | this.imageInput.wrap('
').closest('form').get(0).reset(); 137 | this.imageInput.unwrap(); 138 | 139 | // Only process image files. 140 | if (file.type.match('image.*')) { 141 | var reader = new FileReader(); 142 | reader.onload = e => this.displayPicture(e.target.result); 143 | // Read in the image file as a data URL. 144 | reader.readAsDataURL(file); 145 | this.disableUploadUi(false); 146 | } 147 | } 148 | 149 | /** 150 | * Returns a Canvas containing the given image scaled down to the given max dimension. 151 | * @private 152 | * @static 153 | */ 154 | static _getScaledCanvas(image, maxDimension) { 155 | const thumbCanvas = document.createElement('canvas'); 156 | if (image.width > maxDimension || 157 | image.height > maxDimension) { 158 | if (image.width > image.height) { 159 | thumbCanvas.width = maxDimension; 160 | thumbCanvas.height = maxDimension * image.height / image.width; 161 | } else { 162 | thumbCanvas.width = maxDimension * image.width / image.height; 163 | thumbCanvas.height = maxDimension; 164 | } 165 | } else { 166 | thumbCanvas.width = image.width; 167 | thumbCanvas.height = image.height; 168 | } 169 | thumbCanvas.getContext('2d').drawImage(image, 0, 0, image.width, image.height, 170 | 0, 0, thumbCanvas.width, thumbCanvas.height); 171 | return thumbCanvas; 172 | } 173 | 174 | /** 175 | * Generates the full size image and image thumb using canvas and returns them in a promise. 176 | */ 177 | generateImages() { 178 | const fullDeferred = new $.Deferred(); 179 | const thumbDeferred = new $.Deferred(); 180 | 181 | const resolveFullBlob = blob => fullDeferred.resolve(blob); 182 | const resolveThumbBlob = blob => thumbDeferred.resolve(blob); 183 | 184 | const displayPicture = url => { 185 | const image = new Image(); 186 | image.src = url; 187 | 188 | // Generate thumb. 189 | const maxThumbDimension = friendlyPix.Uploader.THUMB_IMAGE_SPECS.maxDimension; 190 | const thumbCanvas = friendlyPix.Uploader._getScaledCanvas(image, maxThumbDimension); 191 | thumbCanvas.toBlob(resolveThumbBlob, 'image/jpeg', friendlyPix.Uploader.THUMB_IMAGE_SPECS.quality); 192 | 193 | // Generate full sized image. 194 | const maxFullDimension = friendlyPix.Uploader.FULL_IMAGE_SPECS.maxDimension; 195 | const fullCanvas = friendlyPix.Uploader._getScaledCanvas(image, maxFullDimension); 196 | fullCanvas.toBlob(resolveFullBlob, 'image/jpeg', friendlyPix.Uploader.FULL_IMAGE_SPECS.quality); 197 | }; 198 | 199 | const reader = new FileReader(); 200 | reader.onload = e => displayPicture(e.target.result); 201 | reader.readAsDataURL(this.currentFile); 202 | 203 | return Promise.all([fullDeferred.promise(), thumbDeferred.promise()]).then(results => { 204 | return { 205 | full: results[0], 206 | thumb: results[1] 207 | }; 208 | }); 209 | } 210 | 211 | /** 212 | * Uploads the pic to Cloud Storage and add a new post into the Firebase Database. 213 | */ 214 | uploadPic(e) { 215 | e.preventDefault(); 216 | this.disableUploadUi(true); 217 | var imageCaption = this.imageCaptionInput.val(); 218 | 219 | this.generateImages().then(pics => { 220 | // Upload the File upload to Cloud Storage and create new post. 221 | friendlyPix.firebase.uploadNewPic(pics.full, pics.thumb, this.currentFile.name, imageCaption) 222 | .then(postId => { 223 | page(`/user/${this.auth.currentUser.uid}`); 224 | var data = { 225 | message: 'New pic has been posted!', 226 | actionHandler: () => page(`/post/${postId}`), 227 | actionText: 'View', 228 | timeout: 10000 229 | }; 230 | this.toast[0].MaterialSnackbar.showSnackbar(data); 231 | this.disableUploadUi(false); 232 | }, error => { 233 | console.error(error); 234 | var data = { 235 | message: `There was an error while posting your pic. Sorry!`, 236 | timeout: 5000 237 | }; 238 | this.toast[0].MaterialSnackbar.showSnackbar(data); 239 | this.disableUploadUi(false); 240 | }); 241 | }); 242 | } 243 | 244 | /** 245 | * Clear the uploader. 246 | */ 247 | clear() { 248 | this.currentFile = null; 249 | 250 | // Cancel all Firebase listeners. 251 | friendlyPix.firebase.cancelAllSubscriptions(); 252 | 253 | // Clear previously displayed pic. 254 | this.newPictureContainer.attr('src', ''); 255 | 256 | // Clear the text field. 257 | friendlyPix.MaterialUtils.clearTextField(this.imageCaptionInput[0]); 258 | 259 | // Make sure UI is not disabled. 260 | this.disableUploadUi(false); 261 | } 262 | }; 263 | 264 | friendlyPix.uploader = new friendlyPix.Uploader(); 265 | -------------------------------------------------------------------------------- /public/old_scripts/userpage.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2015 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 'use strict'; 17 | 18 | window.friendlyPix = window.friendlyPix || {}; 19 | 20 | /** 21 | * Handles the User Profile UI. 22 | */ 23 | friendlyPix.UserPage = class { 24 | 25 | /** 26 | * Initializes the user's profile UI. 27 | * @constructor 28 | */ 29 | constructor() { 30 | // Firebase SDK. 31 | this.database = firebase.database(); 32 | this.auth = firebase.auth(); 33 | 34 | $(document).ready(() => { 35 | // DOM Elements. 36 | this.userPage = $('#page-user-info'); 37 | this.userAvatar = $('.fp-user-avatar'); 38 | this.toast = $('.mdl-js-snackbar'); 39 | this.userUsername = $('.fp-user-username'); 40 | this.userInfoContainer = $('.fp-user-container'); 41 | this.followContainer = $('.fp-follow'); 42 | this.noPosts = $('.fp-no-posts', this.userPage); 43 | this.followLabel = $('.mdl-switch__label', this.followContainer); 44 | this.followCheckbox = $('#follow'); 45 | this.nbPostsContainer = $('.fp-user-nbposts', this.userPage); 46 | this.nbFollowers = $('.fp-user-nbfollowers', this.userPage); 47 | this.nbFollowing = $('.fp-user-nbfollowing', this.userPage); 48 | this.nbFollowingContainer = $('.fp-user-nbfollowing-container', this.userPage); 49 | this.followingContainer = $('.fp-user-following', this.userPage); 50 | this.nextPageButton = $('.fp-next-page-button button'); 51 | this.closeFollowingButton = $('.fp-close-following', this.userPage); 52 | this.userInfoPageImageContainer = $('.fp-image-container', this.userPage); 53 | 54 | // Event bindings. 55 | this.followCheckbox.change(() => this.onFollowChange()); 56 | this.auth.onAuthStateChanged(() => this.trackFollowStatus()); 57 | this.nbFollowingContainer.click(() => this.displayFollowing()); 58 | this.closeFollowingButton.click(() => { 59 | this.followingContainer.hide(); 60 | this.nbFollowingContainer.removeClass('is-active'); 61 | }); 62 | }); 63 | } 64 | 65 | /** 66 | * Triggered when the user changes the "Follow" checkbox. 67 | */ 68 | onFollowChange() { 69 | const checked = this.followCheckbox.prop('checked'); 70 | this.followCheckbox.prop('disabled', true); 71 | 72 | friendlyPix.firebase.toggleFollowUser(this.userId, checked); 73 | } 74 | 75 | /** 76 | * Starts tracking the "Follow" checkbox status. 77 | */ 78 | trackFollowStatus() { 79 | if (this.auth.currentUser) { 80 | friendlyPix.firebase.registerToFollowStatusUpdate(this.userId, data => { 81 | this.followCheckbox.prop('checked', data.val() !== null); 82 | this.followCheckbox.prop('disabled', false); 83 | this.followLabel.text(data.val() ? 'Following' : 'Follow'); 84 | friendlyPix.MaterialUtils.refreshSwitchState(this.followContainer); 85 | }); 86 | } 87 | } 88 | 89 | /** 90 | * Adds the list of posts to the UI. 91 | */ 92 | addPosts(posts) { 93 | const postIds = Object.keys(posts); 94 | for (let i = postIds.length - 1; i >= 0; i--) { 95 | this.userInfoPageImageContainer.append( 96 | friendlyPix.UserPage.createImageCard(postIds[i], 97 | posts[postIds[i]].thumb_url || posts[postIds[i]].url, posts[postIds[i]].text)); 98 | this.noPosts.hide(); 99 | } 100 | } 101 | 102 | /** 103 | * Shows the "load next page" button and binds it the `nextPage` callback. If `nextPage` is `null` 104 | * then the button is hidden. 105 | */ 106 | toggleNextPageButton(nextPage) { 107 | if (nextPage) { 108 | this.nextPageButton.show(); 109 | this.nextPageButton.unbind('click'); 110 | this.nextPageButton.prop('disabled', false); 111 | this.nextPageButton.click(() => { 112 | this.nextPageButton.prop('disabled', true); 113 | nextPage().then(data => { 114 | this.addPosts(data.entries); 115 | this.toggleNextPageButton(data.nextPage); 116 | }); 117 | }); 118 | } else { 119 | this.nextPageButton.hide(); 120 | } 121 | } 122 | 123 | /** 124 | * Displays the given user information in the UI. 125 | */ 126 | loadUser(userId) { 127 | this.userId = userId; 128 | 129 | // Reset the UI. 130 | this.clear(); 131 | 132 | // If users is the currently signed-in user we hide the "Follow" checkbox and the opposite for 133 | // the "Notifications" checkbox. 134 | if (this.auth.currentUser && userId === this.auth.currentUser.uid) { 135 | this.followContainer.hide(); 136 | friendlyPix.messaging.enableNotificationsContainer.show(); 137 | friendlyPix.messaging.enableNotificationsCheckbox.prop('disabled', true); 138 | friendlyPix.MaterialUtils.refreshSwitchState(friendlyPix.messaging.enableNotificationsContainer); 139 | friendlyPix.messaging.trackNotificationsEnabledStatus(); 140 | } else { 141 | friendlyPix.messaging.enableNotificationsContainer.hide(); 142 | this.followContainer.show(); 143 | this.followCheckbox.prop('disabled', true); 144 | friendlyPix.MaterialUtils.refreshSwitchState(this.followContainer); 145 | // Start live tracking the state of the "Follow" Checkbox. 146 | this.trackFollowStatus(); 147 | } 148 | 149 | // Load user's profile. 150 | friendlyPix.firebase.loadUserProfile(userId).then(snapshot => { 151 | const userInfo = snapshot.val(); 152 | if (userInfo) { 153 | this.userAvatar.css('background-image', 154 | `url("${userInfo.profile_picture || '/images/silhouette.jpg'}")`); 155 | this.userUsername.text(userInfo.full_name || 'Anonymous'); 156 | this.userInfoContainer.show(); 157 | } else { 158 | var data = { 159 | message: 'This user does not exists.', 160 | timeout: 5000 161 | }; 162 | this.toast[0].MaterialSnackbar.showSnackbar(data); 163 | page(`/feed`); 164 | } 165 | }); 166 | 167 | // Lod user's number of followers. 168 | friendlyPix.firebase.registerForFollowersCount(userId, 169 | nbFollowers => this.nbFollowers.text(nbFollowers)); 170 | 171 | // Lod user's number of followed users. 172 | friendlyPix.firebase.registerForFollowingCount(userId, 173 | nbFollowed => this.nbFollowing.text(nbFollowed)); 174 | 175 | // Lod user's number of posts. 176 | friendlyPix.firebase.registerForPostsCount(userId, 177 | nbPosts => this.nbPostsContainer.text(nbPosts)); 178 | 179 | // Display user's posts. 180 | friendlyPix.firebase.getUserFeedPosts(userId).then(data => { 181 | const postIds = Object.keys(data.entries); 182 | if (postIds.length === 0) { 183 | this.noPosts.show(); 184 | } 185 | friendlyPix.firebase.subscribeToUserFeed(userId, 186 | (postId, postValue) => { 187 | this.userInfoPageImageContainer.prepend( 188 | friendlyPix.UserPage.createImageCard(postId, 189 | postValue.thumb_url || postValue.url, postValue.text)); 190 | this.noPosts.hide(); 191 | }, postIds[postIds.length - 1]); 192 | 193 | // Adds fetched posts and next page button if necessary. 194 | this.addPosts(data.entries); 195 | this.toggleNextPageButton(data.nextPage); 196 | }); 197 | 198 | // Listen for posts deletions. 199 | friendlyPix.firebase.registerForPostsDeletion(postId => 200 | $(`.fp-post-${postId}`, this.userPage).remove()); 201 | } 202 | 203 | /** 204 | * Displays the list of followed people. 205 | */ 206 | displayFollowing() { 207 | friendlyPix.firebase.getFollowingProfiles(this.userId).then(profiles => { 208 | // Clear previous following list. 209 | $('.fp-usernamelink', this.followingContainer).remove(); 210 | // Display all following profile cards. 211 | Object.keys(profiles).forEach(uid => this.followingContainer.prepend( 212 | friendlyPix.UserPage.createProfileCardHtml( 213 | uid, profiles[uid].profile_picture, profiles[uid].full_name))); 214 | if (Object.keys(profiles).length > 0) { 215 | this.followingContainer.show(); 216 | // Mark submenu as active. 217 | this.nbFollowingContainer.addClass('is-active'); 218 | } 219 | }); 220 | } 221 | 222 | /** 223 | * Clears the UI and listeners. 224 | */ 225 | clear() { 226 | // Removes all pics. 227 | $('.fp-image', this.userInfoPageImageContainer).remove(); 228 | 229 | // Remove active states of sub menu selectors (like "Following"). 230 | $('.is-active', this.userInfoPageImageContainer).removeClass('is-active'); 231 | 232 | // Cancel all Firebase listeners. 233 | friendlyPix.firebase.cancelAllSubscriptions(); 234 | 235 | // Hides the "Load Next Page" button. 236 | this.nextPageButton.hide(); 237 | 238 | // Hides the user info box. 239 | this.userInfoContainer.hide(); 240 | 241 | // Hide and empty the list of Followed people. 242 | this.followingContainer.hide(); 243 | $('.fp-usernamelink', this.followingContainer).remove(); 244 | 245 | // Stops then infinite scrolling listeners. 246 | friendlyPix.MaterialUtils.stopOnEndScrolls(); 247 | 248 | // Hide the "No posts" message. 249 | this.noPosts.hide(); 250 | } 251 | 252 | /** 253 | * Returns an image Card element for the image with the given URL. 254 | */ 255 | static createImageCard(postId, thumbUrl, text) { 256 | const element = $(` 257 | 259 |
260 | favorite 261 | mode_comment0 262 |
${text}
263 |
264 |
266 |
`); 267 | // Display the thumbnail. 268 | $('.mdl-card', element).css('background-image', `url("${thumbUrl.replace(/"/g, '\\"')}")`); 269 | // Start listening for comments and likes counts. 270 | friendlyPix.firebase.registerForLikesCount(postId, 271 | nbLikes => $('.likes', element).text(nbLikes)); 272 | friendlyPix.firebase.registerForCommentsCount(postId, 273 | nbComments => $('.comments', element).text(nbComments)); 274 | 275 | return element; 276 | } 277 | 278 | /** 279 | * Returns an image Card element for the image with the given URL. 280 | */ 281 | static createProfileCardHtml(uid, profilePic = '/images/silhouette.jpg', fullName = 'Anonymous') { 282 | return ` 283 | 284 |
285 |
${fullName}
286 |
`; 287 | } 288 | }; 289 | 290 | friendlyPix.userPage = new friendlyPix.UserPage(); 291 | -------------------------------------------------------------------------------- /public/old_scripts/utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2015 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 'use strict'; 17 | 18 | window.friendlyPix = window.friendlyPix || {}; 19 | 20 | /** 21 | * Set of utilities to handle Material Design Lite elements. 22 | */ 23 | friendlyPix.MaterialUtils = class { 24 | 25 | /** 26 | * Refreshes the UI state of the given Material Design Checkbox / Switch element. 27 | */ 28 | static refreshSwitchState(element) { 29 | if (element instanceof jQuery) { 30 | element = element[0]; 31 | } 32 | if (element.MaterialSwitch) { 33 | element.MaterialSwitch.checkDisabled(); 34 | element.MaterialSwitch.checkToggleState(); 35 | } 36 | } 37 | 38 | /** 39 | * Closes the drawer if it is open. 40 | */ 41 | static closeDrawer() { 42 | const drawerObfuscator = $('.mdl-layout__obfuscator'); 43 | if (drawerObfuscator.hasClass('is-visible')) { 44 | drawerObfuscator.click(); 45 | } 46 | } 47 | 48 | /** 49 | * Clears the given Material Text Field. 50 | */ 51 | static clearTextField(element) { 52 | element.value = ''; 53 | element.parentElement.MaterialTextfield.boundUpdateClassesHandler(); 54 | } 55 | 56 | /** 57 | * Upgrades the text fields in the element. 58 | */ 59 | static upgradeTextFields(element) { 60 | componentHandler.upgradeElements($('.mdl-textfield', element).get()); 61 | } 62 | 63 | /** 64 | * Returns a Promise which resolves when the user has reached the bottom of the page while 65 | * scrolling. 66 | * If an `offset` is specified the promise will resolve before reaching the bottom of 67 | * the page by the given amount offset in pixels. 68 | */ 69 | static onEndScroll(offset = 0) { 70 | const resolver = new $.Deferred(); 71 | const mdlLayoutElement = $('.mdl-layout'); 72 | mdlLayoutElement.scroll(() => { 73 | if ((window.innerHeight + mdlLayoutElement.scrollTop() + offset) >= 74 | mdlLayoutElement.prop('scrollHeight')) { 75 | console.log('Scroll End Reached!'); 76 | mdlLayoutElement.unbind('scroll'); 77 | resolver.resolve(); 78 | } 79 | }); 80 | console.log('Now watching for Scroll End.'); 81 | return resolver.promise(); 82 | } 83 | 84 | /** 85 | * Stops scroll listeners. 86 | */ 87 | static stopOnEndScrolls() { 88 | const mdlLayoutElement = $('.mdl-layout'); 89 | mdlLayoutElement.unbind('scroll'); 90 | } 91 | }; 92 | -------------------------------------------------------------------------------- /storage.rules: -------------------------------------------------------------------------------- 1 | // Returns true if the given UID matches the signed in UID, 2 | // the uploaded file is an image and its size is below the given number of MB. 3 | // Also allow deletes. 4 | function isImageAndBelowMaxSize(uid, maxSizeMB) { 5 | return request.auth.uid == uid 6 | && (request.resource == null // Allow deletes 7 | || request.resource.size < maxSizeMB * 1024 * 1024 // Max size for the uploaded file 8 | && request.resource.contentType.matches('image/.*')) // The file is an image 9 | } 10 | 11 | service firebase.storage { 12 | match /b/{bucket}/o { 13 | match /{userId}/thumb/{postId}/{fileName} { 14 | allow read, write: if isImageAndBelowMaxSize(userId, 1); 15 | } 16 | match /{userId}/full/{postId}/{fileName} { 17 | allow read, write: if isImageAndBelowMaxSize(userId, 5); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for t`he specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 'use strict'; 17 | 18 | const path = require('path'); 19 | const WebpackShellPlugin = require('webpack-shell-plugin'); 20 | const ExtractTextPlugin = require('extract-text-webpack-plugin'); 21 | 22 | const config = { 23 | context: __dirname, 24 | 25 | entry: './frontend/App.jsx', 26 | output: { 27 | filename: 'bundle.js', 28 | path: path.resolve(__dirname, './public') 29 | }, 30 | devtool: 'cheap-module-source-map', 31 | resolve: { 32 | extensions: ['.js', '.jsx', '.json'], 33 | // alias: { 34 | // react: 'preact-compat', 35 | // 'react-dom': 'preact-compat' 36 | // } 37 | }, 38 | stats: { 39 | colors: true, 40 | reasons: true, 41 | chunks: true 42 | }, 43 | plugins: [new ExtractTextPlugin('./bundle.css')], 44 | module: { 45 | rules: [ 46 | { 47 | enforce: 'pre', 48 | test: /\.jsx?$/, 49 | loader: 'eslint-loader', 50 | exclude: /node_modules/ 51 | }, 52 | { 53 | test: /\.jsx?$/, 54 | loader: 'babel-loader', 55 | exclude: /node_modules/, 56 | include: [path.resolve('frontend'), path.resolve('node_modules/preact-compat/src')], 57 | query: { 58 | babelrc: false, 59 | presets: [ 60 | "react", 61 | ["env", { 62 | "targets": { 63 | "browsers": "last 2 versions" 64 | }, 65 | "loose": true, 66 | "modules": "commonjs" 67 | }] 68 | ], 69 | "plugins": [ 70 | "transform-decorators", 71 | "transform-class-properties", 72 | "transform-object-rest-spread" 73 | ] 74 | } 75 | }, 76 | { 77 | test: /\.css$/, 78 | exclude: [/\.global\./, /node_modules/], 79 | loader: ExtractTextPlugin.extract( 80 | { 81 | fallback: 'style-loader', 82 | use:[ 83 | { 84 | loader: 'css-loader', 85 | options: { 86 | importLoaders: 1, 87 | modules: true, 88 | autoprefixer: true, 89 | minimize: true, 90 | localIdentName: '[name]__[local]___[hash:base64:5]' 91 | } 92 | } 93 | ] 94 | }) 95 | }, 96 | { 97 | test: /\.css/, 98 | include: [/\.global\./, /node_modules/], 99 | loader: ExtractTextPlugin.extract( 100 | { 101 | fallback: 'style-loader', 102 | use:[ 103 | { 104 | loader: 'css-loader', 105 | options: { 106 | importLoaders: 1, 107 | modules: false, 108 | minimize: true 109 | } 110 | } 111 | ] 112 | }) 113 | } 114 | ] 115 | } 116 | }; 117 | 118 | console.log('Packing for', process.env.NODE_ENV || 'development'); 119 | 120 | if (process.env.NODE_ENV === 'production') { 121 | config.devtool = 'source-map'; 122 | } 123 | 124 | if (process.env.NODE_ENV === 'devserver') { 125 | config.plugins.push(new WebpackShellPlugin({ 126 | onBuildStart: ['echo "Starting to pack"'], 127 | onBuildEnd: ['firebase serve --only hosting,functions'] 128 | })); 129 | } 130 | 131 | module.exports = config; 132 | --------------------------------------------------------------------------------