├── .editorconfig ├── .gitignore ├── .jscsrc ├── .jshintignore ├── .jshintrc ├── LICENSE ├── README.md ├── app.yaml ├── bower.json ├── data └── threads.json ├── deploy.sh ├── elements ├── appstyles_sd.html ├── elements.html ├── mail-thread.html ├── material-search.html ├── profile-img.html └── swipeable-item.html ├── gulpfile.js ├── images ├── beach.jpg ├── gmail.png ├── mountains.jpg ├── screenshot.jpg └── user.png ├── index.html ├── manifest.json ├── package.json ├── scripts ├── app.js ├── googleapis.js └── polymer_metrics.js ├── styles └── app.css └── sw-import.js /.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 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | bower_components 3 | dist 4 | node_modules 5 | scripts/bundle.js 6 | vulcanized* 7 | -------------------------------------------------------------------------------- /.jscsrc: -------------------------------------------------------------------------------- 1 | { 2 | "preset": "google", 3 | "validateIndentation": 2, 4 | "excludeFiles": ["node_modules/**", "dist/**", "scripts/bundle.js"], 5 | "esnext": true, 6 | "disallowParenthesesAroundArrowParam": true, 7 | "requireArrowFunctions": true 8 | } 9 | -------------------------------------------------------------------------------- /.jshintignore: -------------------------------------------------------------------------------- 1 | scripts/bundle.js 2 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "browser": true, 4 | "esnext": true, 5 | "bitwise": true, 6 | "curly": true, 7 | "eqeqeq": true, 8 | "funcscope": true, 9 | "immed": true, 10 | "indent": 2, 11 | "newcap": true, 12 | "noarg": true, 13 | "quotmark": "single", 14 | "undef": true, 15 | "unused": "vars", 16 | "strict": true, 17 | "globals": { 18 | "gapi": true, 19 | "Polymer": true, 20 | "GMail": true, 21 | "GPlus": true 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2014 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | 15 | Author: Eric Bidelman (@ebidel) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## PolyMail 2 | 3 | PolyMail is an offline, mobile-first, web version of the [new Gmail native app UI](http://gmailblog.blogspot.com/2014/11/a-more-modern-gmail-app-for-android.html). It's built using [Polymer 1.0](https://www.polymer-project.org/1.0/) and [Service Worker](http://www.html5rocks.com/en/tutorials/service-worker/introduction/) and...is a WIP. 4 | 5 | Demo: [https://poly-mail.appspot.com/](https://poly-mail.appspot.com/)   (mock data: [https://poly-mail.appspot.com/?debug](https://poly-mail.appspot.com/?debug)) 6 | 7 | ![PolyMail](https://raw.githubusercontent.com/ebidel/polymer-gmail/master/images/screenshot.jpg) 8 | 9 | **Note**: the app is *read only* despite what the permissions popup says. Also, most of the buttons don't do anything. There's a lot of missing functionality. 10 | 11 | #### Performance 12 | 13 | *TLDR: paint is ~393ms and the app loads ~1s on Chrome desktop. Motorola G - Chrome - 3G Fast connection first paint is 1.66s* The full performance improvements over the Polymer 0.5 version are documented [here](https://github.com/ebidel/polymer-gmail/issues/6#issuecomment-123875813). 14 | 15 | [Full results](https://github.com/ebidel/polymer-gmail/issues/6#issuecomment-123875813) 16 | 17 | === 18 | 19 | ### Setup 20 | 21 | In your local checkout, install the deps and Polymer elements 22 | 23 | npm install 24 | 25 | This will also run `bower install` for you. 26 | 27 | ### Development & Building 28 | 29 | ##### Compile the ES6 30 | 31 | While ES6 Classes run natively in Chrome, FF Nightly, Safari 9, and Edge, some of JS 32 | in PolyMail still requires compilation using Babel. In particular, `scripts/googleapis.js` uses ES6 `=>` functions and modules (`import` statement) in addition to classes. 33 | 34 | Compile the JS/CSS: 35 | 36 | gulp 37 | 38 | This produces a single built and concatenated `dist/scripts/bundle.js` and compiles the rest of the app into `dist/`. You're ready to run the app! 39 | 40 | ### Run the app 41 | 42 | Use any webserver you'd like. I use [npm serve](https://www.npmjs.com/package/serve): 43 | 44 | serve -p 8080 45 | 46 | **You must run from /dist** 47 | 48 | `serve -p 8080` serves the root folder, but the app runs the production version from `dist/`. 49 | So be sure to first run `gulp`, then hit [http://localhost:8080/dist/](http://localhost:8080/dist/). 50 | 51 | ##### Watching files 52 | 53 | For easier development, there's a task for rebuilding the [vulcanized](https://github.com/polymer/vulcanize) elements.html bundle and compiling the ES6 as you make changes: 54 | 55 | gulp watch 56 | 57 | ### Using test data 58 | 59 | Hitting [http://localhost:8080?debug](http://localhost:8080/dist/?debug) will bypass Google Sign-in and use mock data for threads. Under this 60 | testing mode, you will no see custom labels in the left nav or user profile images show up on threads. 61 | 62 | ### Deploying 63 | 64 | npm run deploy 65 | 66 | ### Future improvements 67 | 68 | - Push notifications 69 | - Reading emails in a thread 70 | - Creating emails 71 | - Clicking Label actually does filtering 72 | - Pagination (currently only the first few emails are visible) 73 | - a11y (keyboard access, tab support) 74 | - i18n 75 | - [x] http2 push 76 | - [x] Service Worker offline support & caching 77 | - [x] Caching API requests 78 | - [x] Auto-refresh inbox 79 | - [x] Use Google Sign-in 2.0 80 | - [x] Use GMail API push notifications ([docs](https://developers.google.com/gmail/api/guides/push)) 81 | - [x] Use GMail API history feature ([docs](https://developers.google.com/gmail/api/v1/reference/users/history/list)) 82 | - [x] Searching emails. Full gmail search (e.g. `to:me from:someone@gmail.com` is supported`). 83 | -------------------------------------------------------------------------------- /app.yaml: -------------------------------------------------------------------------------- 1 | runtime: python27 2 | api_version: 1 3 | threadsafe: yes 4 | 5 | handlers: 6 | 7 | - url: /bower_components 8 | static_dir: bower_components 9 | secure: always 10 | 11 | - url: /data 12 | static_dir: data 13 | secure: always 14 | 15 | - url: /images 16 | static_dir: images 17 | secure: always 18 | 19 | - url: /(.*).(html|js|json|css) 20 | static_files: \1.\2 21 | upload: (.*)\.(html|js|json|css) 22 | secure: always 23 | 24 | - url: /$ 25 | static_files: index.html 26 | upload: index\.html 27 | http_headers: 28 | Link: '; rel=preload; as=script, ; rel=preload; as=script, ; rel=preload; as=style' 29 | # Access-Control-Allow-Origin: "*" 30 | secure: always 31 | 32 | skip_files: 33 | - ^(.*/)?app\.yaml 34 | - ^(.*/)?app\.yml 35 | - ^(.*/)?index\.yaml 36 | - ^(.*/)?index\.yml 37 | - ^(.*/)?bower\.json 38 | - ^(.*/)?#.*# 39 | - ^(.*/)?.*~ 40 | - ^(.*/)?.*\.py[co] 41 | - ^(.*/)?.*/RCS/.* 42 | - ^(.*/)?\..* 43 | - ^(.*/)?.*\.bak$ 44 | - ^(.*/)?node_modules/.* 45 | - ^(.*/).md|markdown 46 | - ^(.*/)LICENSE 47 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "PolymerMail", 3 | "version": "0.1.16", 4 | "authors": [ 5 | "Eric Bidelman " 6 | ], 7 | "description": "Polymer version of Gmail app", 8 | "main": "index.html", 9 | "keywords": [ 10 | "webcomponents", 11 | "web components", 12 | "polymer", 13 | "webview" 14 | ], 15 | "license": "http://polymer.github.io/LICENSE.txt", 16 | "ignore": [ 17 | "**/.*", 18 | "node_modules", 19 | "bower_components", 20 | "test", 21 | "tests" 22 | ], 23 | "dependencies": { 24 | "iron-elements": "PolymerElements/iron-elements#^1.0.0", 25 | "paper-elements": "PolymerElements/paper-elements#^1.0.0", 26 | "google-signin": "GoogleWebComponents/google-signin#^1.0.1", 27 | "platinum-sw": "PolymerElements/platinum-sw#^1.0.1" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Copyright 2015 Google Inc. All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | deployVersion=$1 18 | 19 | if [ -z "$deployVersion" ] 20 | then 21 | echo "Deploy version not specified." 22 | exit 0 23 | fi 24 | 25 | # Build it. 26 | echo "Building: $deployVersion" 27 | gulp release --env prod 28 | #cp app.yaml dist/app.yaml 29 | 30 | echo "Deploying: $deployVersion" 31 | gcloud preview app deploy dist/app.yaml --project poly-mail \ 32 | --version $deployVersion 33 | 34 | # Tag a release. 35 | #git tag -a $deployVersion -m 'Release $deployVersion' 36 | #git push origin --tags 37 | -------------------------------------------------------------------------------- /elements/appstyles_sd.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 88 | -------------------------------------------------------------------------------- /elements/elements.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /elements/mail-thread.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 18 | 19 | 161 | 206 | 456 | 457 | -------------------------------------------------------------------------------- /elements/material-search.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 60 | 79 | 162 | 163 | -------------------------------------------------------------------------------- /elements/profile-img.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 46 | 53 | 54 | 55 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /elements/swipeable-item.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 38 | 39 | 66 | 67 | 70 | 71 | 275 | 276 | -------------------------------------------------------------------------------- /gulpfile.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 | 17 | 'use strict'; 18 | 19 | var gulp = require('gulp'); 20 | var $ = require('gulp-load-plugins')(); 21 | var fs = require('fs'); 22 | var del = require('del'); 23 | var glob = require('glob'); 24 | 25 | var browserify = require('browserify'); 26 | var source = require('vinyl-source-stream'); 27 | var babelify = require('babelify'); 28 | 29 | var runSequence = require('run-sequence'); 30 | var path = require('path'); 31 | 32 | var isProd = false; 33 | 34 | const AUTOPREFIXER_BROWSERS = ['last 2 versions', 'ios 8', 'Safari 8']; 35 | 36 | function getVersion() { 37 | return JSON.parse(fs.readFileSync('./package.json', 'utf8')).version; 38 | } 39 | 40 | function minifyHtml() { 41 | return $.minifyHtml({quotes: true, empty: true, spare: true}); 42 | } 43 | 44 | function uglifyJS() { 45 | return $.uglify({preserveComments: 'some'}); 46 | } 47 | 48 | /** Clean */ 49 | gulp.task('clean', function(done) { 50 | return del(['dist', 'scripts/bundle.js']); 51 | }); 52 | 53 | /** Styles */ 54 | gulp.task('styles', function() { 55 | // return gulp.src('./styles/*.scss') 56 | // .pipe($.sass()) 57 | // // .pipe($.autoprefixer([ 58 | // // 'ie >= 10', 59 | // // 'ie_mob >= 10', 60 | // // 'ff >= 33', 61 | // // 'chrome >= 38', 62 | // // 'safari >= 7', 63 | // // 'opera >= 26', 64 | // // 'ios >= 7' 65 | // // ])) 66 | // .pipe($.minifyCss()) 67 | // .pipe($.license('Apache', { 68 | // organization: 'Google Inc. All rights reserved.' 69 | // })) 70 | // .pipe(gulp.dest('./dist/styles')); 71 | return gulp.src('./styles/*.css') 72 | .pipe($.autoprefixer(AUTOPREFIXER_BROWSERS)) 73 | .pipe($.minifyCss()) 74 | .pipe($.license('Apache', { 75 | organization: 'Google Inc. All rights reserved.' 76 | })) 77 | .pipe(gulp.dest('./dist/styles')); 78 | }); 79 | 80 | /** Scripts */ 81 | gulp.task('js', ['jshint', 'jscs']); 82 | 83 | // Lint JavaScript 84 | gulp.task('jshint', function() { 85 | return gulp.src(['./scripts/**/*.js']) 86 | .pipe($.jshint('.jshintrc')) 87 | .pipe($.jshint.reporter('jshint-stylish')) 88 | }); 89 | 90 | // Check JS style 91 | gulp.task('jscs', function() { 92 | return gulp.src(['./scripts/**/*.js']) 93 | .pipe($.jscs()); 94 | }); 95 | 96 | function buildBundle(file) { 97 | return browserify({ 98 | entries: [file], 99 | debug: isProd 100 | }) 101 | .transform(babelify, {presets: ['es2015']}) // es6 -> e5 102 | .bundle(); 103 | } 104 | 105 | gulp.task('jsbundle', function() { 106 | console.log('==Building JS bundle=='); 107 | 108 | //var dest = isProd ? 'dist' : ''; 109 | var dest = 'dist'; 110 | 111 | return buildBundle('./scripts/app.js') 112 | .pipe(source('bundle.js')) 113 | .pipe($.streamify(uglifyJS())) 114 | .pipe($.license('Apache', { 115 | organization: 'Google Inc. All rights reserved.' 116 | })) 117 | .pipe(gulp.dest('./' + dest + '/scripts')) 118 | }); 119 | 120 | /** Root */ 121 | gulp.task('root', function() { 122 | gulp.src([ 123 | './*.*', 124 | '!{package,bower}.json', 125 | '!gulpfile.js', 126 | '!deploy.sh', 127 | '!*.md' 128 | ]) 129 | .pipe($.replace(/@VERSION@/g, getVersion())) 130 | .pipe(gulp.dest('./dist/')); 131 | 132 | gulp.src(['./data/*.json']).pipe(gulp.dest('./dist/data')); 133 | 134 | return gulp.src('./favicon.ico') 135 | .pipe(gulp.dest('./dist/')); 136 | }); 137 | 138 | gulp.task('copy_bower_components', function() { 139 | gulp.src([ 140 | 'bower_components/webcomponentsjs/webcomponents-lite.min.js', 141 | 'bower_components/platinum-sw/*.js' 142 | ], {base: './'}) 143 | .pipe(gulp.dest('./dist')); 144 | 145 | // Service worker elements want files in a specific location. 146 | gulp.src(['bower_components/sw-toolbox/*.js']) 147 | .pipe(gulp.dest('./dist/sw-toolbox')); 148 | gulp.src(['bower_components/platinum-sw/bootstrap/*.js']) 149 | .pipe(gulp.dest('./dist/elements/bootstrap')); 150 | }); 151 | 152 | /** HTML */ 153 | // gulp.task('html', function() { 154 | // return gulp.src('./**/*.html') 155 | // .pipe($.replace(/@VERSION@/g, version)) 156 | // .pipe(gulp.dest('./dist/')); 157 | // }); 158 | 159 | /** Images */ 160 | gulp.task('images', function() { 161 | return gulp.src([ 162 | './images/**/*.svg', 163 | './images/**/*.png', 164 | './images/**/*.jpg', 165 | '!./images/screenshot.jpg' 166 | ]) 167 | .pipe(gulp.dest('./dist/images')); 168 | }); 169 | 170 | 171 | // Generate a list of files to precached when serving from 'dist'. 172 | // The list will be consumed by the element. 173 | gulp.task('precache', function(callback) { 174 | var dir = 'dist'; 175 | 176 | glob('{elements,scripts,styles}/**/*.*', {cwd: dir}, function(error, files) { 177 | if (error) { 178 | callback(error); 179 | } else { 180 | files.push('index.html', './', 'bower_components/webcomponentsjs/webcomponents-lite.min.js'); 181 | var filePath = path.join(dir, 'precache.json'); 182 | fs.writeFile(filePath, JSON.stringify(files), callback); 183 | } 184 | }); 185 | }); 186 | 187 | 188 | /** Vulcanize */ 189 | gulp.task('vulcanize', function() { 190 | console.log('==Vulcanizing HTML Imports=='); 191 | 192 | return gulp.src('./elements/elements.html') 193 | .pipe($.vulcanize({ 194 | inlineScripts: true, 195 | inlineCss: true, 196 | stripComments: true, 197 | //excludes: [path.resolve('./dist/third_party/polymer.html')] 198 | //stripExcludes: false, 199 | })) 200 | .pipe($.crisper()) // Separate JS into its own file for CSP compliance and reduce html parser load. 201 | .pipe($.if('*.html', minifyHtml())) // Minify html output 202 | .pipe($.if('*.js', uglifyJS())) // Minify js output 203 | .pipe(gulp.dest('./dist/elements')) 204 | }); 205 | 206 | /** Watches */ 207 | gulp.task('watch', function() { 208 | gulp.watch('./styles/**/*.scss', ['styles']); 209 | gulp.watch('./*.html', ['root']); 210 | // gulp.watch('./sw-import.js', ['serviceworker']); 211 | gulp.watch('./elements/**/*.html', ['vulcanize']); 212 | gulp.watch('./images/**/*.*', ['images']); 213 | gulp.watch('./scripts/**/*.js', ['jsbundle']); 214 | }); 215 | 216 | /** Main tasks */ 217 | 218 | var allTasks = ['root', 'styles', 'jsbundle', 'images'];//, 'serviceworker']; 219 | 220 | gulp.task('bump', function() { 221 | return gulp.src([ 222 | './{package,bower}.json' 223 | ]) 224 | .pipe($.bump({type: 'patch'})) 225 | .pipe(gulp.dest('./')); 226 | }); 227 | 228 | gulp.task('default', function() { 229 | isProd = true; 230 | return runSequence('clean', 'js', allTasks, 'vulcanize', 'precache', 231 | 'copy_bower_components'); 232 | }) 233 | 234 | gulp.task('dev', function() { 235 | return runSequence('clean', allTasks, 'watch'); 236 | }); 237 | 238 | gulp.task('release', ['bump'], function() { 239 | return runSequence('default'); 240 | }); 241 | -------------------------------------------------------------------------------- /images/beach.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ebidel/polymer-gmail/e6f038166db9dc3026074904846568ecf485be9b/images/beach.jpg -------------------------------------------------------------------------------- /images/gmail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ebidel/polymer-gmail/e6f038166db9dc3026074904846568ecf485be9b/images/gmail.png -------------------------------------------------------------------------------- /images/mountains.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ebidel/polymer-gmail/e6f038166db9dc3026074904846568ecf485be9b/images/mountains.jpg -------------------------------------------------------------------------------- /images/screenshot.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ebidel/polymer-gmail/e6f038166db9dc3026074904846568ecf485be9b/images/screenshot.jpg -------------------------------------------------------------------------------- /images/user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ebidel/polymer-gmail/e6f038166db9dc3026074904846568ecf485be9b/images/user.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Polymer Mail 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 29 |
30 |
31 |
32 |
33 |
34 |
35 |

Inbox

36 |
37 |
38 |
39 | 40 | 194 | 195 | 196 | 197 | 199 | 205 | 207 | 208 | 209 | 210 | 211 | 219 | 220 | 221 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Polymer Mail", 3 | "short_name": "PolyMail", 4 | "start_url": "./?utm_source=web_app_manifest", 5 | "display": "standalone", 6 | "orientation": "portrait", 7 | "icons": [{ 8 | "src": "images/gmail.png", 9 | "sizes": "192x192", 10 | "type": "image/png" 11 | }], 12 | "theme_color": "#e51c23", 13 | "background_color": "#e51c23" 14 | } 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "polymail", 3 | "version": "0.1.16", 4 | "private": true, 5 | "engines": { 6 | "node": ">=0.10.0" 7 | }, 8 | "scripts": { 9 | "postinstall": "bower install", 10 | "deploy": "./deploy.sh" 11 | }, 12 | "devDependencies": { 13 | "babel-preset-es2015": "^6.1.18", 14 | "babelify": "^7.2.0", 15 | "browserify": "^12.0.1", 16 | "del": "^2.1.0", 17 | "glob": "^6.0.1", 18 | "gulp": "^3.9.0", 19 | "gulp-autoprefixer": "^3.1.0", 20 | "gulp-bump": "^1.0.0", 21 | "gulp-crisper": "^1.0.0", 22 | "gulp-if": "^2.0.0", 23 | "gulp-jscs": "^3.0.2", 24 | "gulp-jshint": "^2.0.0", 25 | "gulp-license": "^1.0.0", 26 | "gulp-load-plugins": "^1.1.0", 27 | "gulp-minify-css": "^1.1.0", 28 | "gulp-minify-html": "^1.0.4", 29 | "gulp-rename": "^1.2.2", 30 | "gulp-replace": "^0.5.4", 31 | "gulp-sass": "^2.1.0", 32 | "gulp-streamify": "^1.0.2", 33 | "gulp-uglify": "^1.5.1", 34 | "gulp-util": "^3.0.7", 35 | "gulp-vulcanize": "^6.1.0", 36 | "gulp-watch": "^4.3.5", 37 | "jshint-stylish": "^2.1.0", 38 | "lodash.assign": "^3.1.0", 39 | "run-sequence": "^1.1.5", 40 | "vinyl-buffer": "^1.0.0", 41 | "vinyl-source-stream": "^1.1.0" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /scripts/app.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 | 17 | import {GMail as Gmail, GPlus as Gplus} from './googleapis'; 18 | 19 | (() => { 20 | 21 | 'use strict'; 22 | 23 | var DEBUG = location.search.indexOf('debug') !== -1; 24 | var REFRESH_INTERVAL = 60000; // every 60 sec. 25 | var inboxRefreshId; 26 | var pendingArchivedThreads = []; 27 | 28 | var GMail = new Gmail(); 29 | var GPlus = new Gplus(); 30 | 31 | var template = document.querySelector('#t'); 32 | template.DEBUG = DEBUG; 33 | template.isAuthenticated = true; // Presume user is logged in when app loads (better UX). 34 | template.threads = []; 35 | template.selectedThreads = []; 36 | template.headerTitle = 'Inbox'; 37 | template.user = {}; 38 | template.MAX_REFRESH_Y = 150; 39 | template.syncing = false; // True, if the mail is syncing. 40 | template.refreshStarted = false; // True if the pull to refresh has been enabled. 41 | template._scrollArchiveSetup = false; // True if the user has attempted to archive a thread. 42 | 43 | // TODO: save past searches for offline. 44 | template.previousSearches = [ 45 | 'is: chat', 46 | 'to: me', 47 | 'airline tickets' 48 | ]; 49 | 50 | // Conditionally load webcomponents polyfill (if needed). 51 | var webComponentsSupported = ( 52 | 'registerElement' in document && 53 | 'import' in document.createElement('link') && 54 | 'content' in document.createElement('template')); 55 | 56 | if (!webComponentsSupported) { 57 | var script = document.createElement('script'); 58 | script.async = true; 59 | script.src = '/bower_components/webcomponentsjs/webcomponents-lite.min.js'; 60 | script.onload = finishLazyLoadingImports; 61 | document.head.appendChild(script); 62 | } else { 63 | finishLazyLoadingImports(); 64 | } 65 | 66 | function finishLazyLoadingImports() { 67 | // Use native Shadow DOM if it's available in the browser. 68 | window.Polymer = window.Polymer || {dom: 'shadow'}; 69 | 70 | var onImportLoaded = function() { 71 | var loadContainer = document.getElementById('splash'); 72 | loadContainer.addEventListener('transitionend', e => { 73 | loadContainer.parentNode.removeChild(loadContainer); // IE 10 doesn't support el.remove() 74 | }); 75 | 76 | if (DEBUG) { 77 | loadTestData(); 78 | } 79 | 80 | document.body.classList.remove('loading'); 81 | }; 82 | 83 | // crbug.com/504944 - readyState never goes to complete until Chrome 46. 84 | // crbug.com/505279 - Resource Timing API is not available until Chrome 46. 85 | var link = document.querySelector('#bundle'); 86 | if (link.import && link.import.readyState === 'complete') { 87 | onImportLoaded(); 88 | } else { 89 | link.addEventListener('load', onImportLoaded); 90 | } 91 | } 92 | 93 | /** 94 | * Loads sample data. 95 | */ 96 | function loadTestData() { 97 | var ajax = document.createElement('iron-ajax'); 98 | ajax.auto = true; 99 | ajax.url = '/data/users.json'; 100 | ajax.addEventListener('response', e => { 101 | template.users = e.detail.response; 102 | }); 103 | 104 | var ajax2 = document.createElement('iron-ajax'); 105 | ajax2.auto = true; 106 | ajax2.url = '/data/threads.json'; 107 | ajax2.addEventListener('response', e => { 108 | template.threads = e.detail.response; 109 | }); 110 | } 111 | 112 | /** 113 | * Utility function to listen to an event on a node once. 114 | * 115 | * @param {Node} node The animated node 116 | * @param {String} event Name of an event 117 | * @param {Function} fn Event handler 118 | * @param {Array} args Additional arguments to pass to `fn` 119 | */ 120 | function listenOnce(node, event, fn, args) { 121 | // jshint validthis:true 122 | var self = this; 123 | var listener = function() { 124 | fn.apply(self, args); 125 | node.removeEventListener(event, listener, false); 126 | }; 127 | node.addEventListener(event, listener, false); 128 | } 129 | 130 | /** 131 | * Error callback handler for GMail API calls. 132 | */ 133 | function GMailErrorCallback(e) { 134 | if (e.status === 401) { 135 | template.isAuthenticated = false; 136 | } else { 137 | console.error(e); 138 | } 139 | } 140 | 141 | template._computeShowNoResults = function(threads, syncing) { 142 | return !syncing && threads && !threads.length; 143 | }; 144 | 145 | template._computeHideLogin = function(isAuthenticated) { 146 | return isAuthenticated || DEBUG; 147 | }; 148 | 149 | // template._computeThreadTabIndex = function(archived) { 150 | // return archived ? -1 : 0; 151 | // }; 152 | 153 | template._computeMainHeaderClass = function(narrow, numSelectedThreads) { 154 | return (narrow ? 'core-narrow' : 'tall') + ' ' + 155 | (numSelectedThreads ? 'selected-threads' : ''); 156 | }; 157 | 158 | template._computeHeaderTitle = function(numSelectedThreads) { 159 | return numSelectedThreads ? numSelectedThreads : 'Inbox'; 160 | }; 161 | 162 | // TODO: iron-selector bug where subscribers are not notified of changes 163 | // after the first selection. For now, use events instead to update title. 164 | // See github.com/PolymerElements/iron-selector/issues/33 165 | template._onThreadSelectChange = function(e) { 166 | this.headerTitle = this._computeHeaderTitle(this.selectedThreads.length); 167 | this.headerClass = this._computeMainHeaderClass( 168 | this.narrow, this.selectedThreads.length); 169 | }; 170 | 171 | template._onThreadTap = function(e) { 172 | e.stopPropagation(); 173 | var idx = this.$.threadlist.items.indexOf(e.detail.thread); 174 | this.$.threadlist.select(idx); 175 | }; 176 | 177 | template._onArchivedToastOpenClose = function() { 178 | if (this.$.arhivedtoast.visible) { 179 | this.$.fab.classList.add('moveup'); 180 | // jshint boss:true 181 | // for (var i = 0, threadEl; threadEl = pendingArchivedThreads[i]; ++i) { 182 | // threadEl.undo = false; // hide in-place UNDO UI. 183 | // } 184 | } else { 185 | // When the archived message toast closes, the user can no longer undo. 186 | // Remove the threads. 187 | // jshint boss:true 188 | for (var i = 0, threadEl; threadEl = pendingArchivedThreads[i]; ++i) { 189 | this.removeThread(threadEl); 190 | } 191 | 192 | pendingArchivedThreads = []; // clear previous selections. 193 | this.$.fab.classList.remove('moveup'); 194 | } 195 | }; 196 | 197 | template._onSearch = function(e) { 198 | this.toggleSearch(); 199 | this.showLoadingSpinner(); 200 | this.refreshInbox(e.detail.value); 201 | this.unshift('previousSearches', e.detail.value); 202 | }; 203 | 204 | template.toggleSearch = function() { 205 | this.$.search.toggle(); 206 | }; 207 | 208 | template.toggleToast = function(optMesssage) { 209 | var toast = optMesssage ? this.$.toast : this.$.arhivedtoast; 210 | if (optMesssage) { 211 | toast.text = optMesssage; 212 | } 213 | toast.toggle(); 214 | this._onArchivedToastOpenClose(); // Move FAB at same time as 215 | }; 216 | 217 | template.undoAll = function(e, detail, sender) { 218 | e.stopPropagation(); 219 | 220 | // jshint boss:true 221 | for (var i = 0, threadEl; threadEl = pendingArchivedThreads[i]; ++i) { 222 | threadEl.archived = false; 223 | threadEl.removed = false; 224 | } 225 | 226 | pendingArchivedThreads = []; 227 | 228 | this.toggleToast(); 229 | }; 230 | 231 | template.refreshLabels = function() { 232 | return GMail.fetchLabels().then(labels => { 233 | template.labels = labels.labels; 234 | template.labelMap = labels.labelMap; 235 | }, GMailErrorCallback); 236 | }; 237 | 238 | template.refreshInbox = function(optQuery) { 239 | clearInterval(inboxRefreshId); 240 | 241 | var query = optQuery || 'in:inbox'; 242 | 243 | return GMail.fetchMail(query).then(threads => { 244 | template.hideLoadingSpinner(); 245 | template.threads = threads; 246 | 247 | // TODO: use gmail's push api: http://googleappsdeveloper.blogspot.com/2015/05/gmail-api-push-notifications-dont-call.html 248 | // Setup auto-fresh if we're querying the inbox. 249 | if (!optQuery) { 250 | inboxRefreshId = setInterval( 251 | template.refreshInbox.bind(template), REFRESH_INTERVAL); 252 | } 253 | }, GMailErrorCallback); 254 | }; 255 | 256 | template.onRefreshInboxButton = function(e) { 257 | this.showLoadingSpinner(); 258 | this.refreshInbox(); 259 | }; 260 | 261 | template.newMail = function(e) { 262 | console.warn('Not implemented: Create new mail'); 263 | }; 264 | 265 | template.menuSelect = function(e) { 266 | this.$.drawerPanel.togglePanel(); 267 | }; 268 | 269 | template.deselectAll = function(e) { 270 | this.$.threadlist.selectedValues = []; 271 | }; 272 | 273 | // Archives currently selected messages. 274 | template.archiveAll = function(e) { 275 | e.stopPropagation(); 276 | 277 | this.inboxToastMessage = this.selectedThreads.length + ' archived'; 278 | 279 | var selectedItems = this.$.threadlist.selectedItems.slice(0); 280 | // jshint boss:true 281 | for (var i = 0, threadEl; threadEl = selectedItems[i]; ++i) { 282 | threadEl.archived = true; 283 | pendingArchivedThreads.push(threadEl); 284 | } 285 | 286 | this.async(() => { 287 | this.toggleToast(); 288 | }, 1000); // delay showing the toast. 289 | }; 290 | 291 | template.onThreadArchive = function(e) { 292 | // Ignore thread unarchive. 293 | if (!e.detail.showUndo) { 294 | return; 295 | } 296 | 297 | if (!this._scrollArchiveSetup) { 298 | // When user scrolls page, remove visibly archived threads. 299 | listenOnce(this.$.scrollheader, 'content-scroll', e => { 300 | 301 | var archivedThreads = this.$.threadlist.items.filter(el => el.archived); 302 | 303 | console.log(archivedThreads.length); 304 | 305 | var shrinkThreads = function() { 306 | return new Promise(function(resolve, reject) { 307 | // jshint boss:true 308 | for (var i = 0, threadEl; threadEl = archivedThreads[i]; ++i) { 309 | threadEl.classList.add('shrink'); 310 | threadEl.undo = false; // hide in-place UNDO UI. 311 | } 312 | template.async(() => { 313 | resolve(); 314 | }, 300); // Wait for shrink animations to finish. 315 | }); 316 | }; 317 | 318 | // TODO: this changes the indices of the array and removeThread expects 319 | // threadEl.dataset.threadIndex ordering. 320 | // It leaves some threads in an archived state. 321 | shrinkThreads().then(() => { 322 | archivedThreads.map(template.removeThread, template); 323 | }); 324 | 325 | this._scrollArchiveSetup = false; 326 | }); 327 | } 328 | 329 | this._scrollArchiveSetup = true; 330 | }; 331 | 332 | template.removeThread = function(threadEl) { 333 | threadEl.removed = true; 334 | this.splice('threads', parseInt(threadEl.dataset.threadIndex), 1); 335 | }; 336 | 337 | template.showLoadingSpinner = function() { 338 | this.syncing = true; // Visually indicate loading. 339 | 340 | // Wait for dom-if to stamp. 341 | this.async(() => { 342 | var el = document.querySelector('#refresh-spinner-container'); 343 | el.classList.remove('shrink'); 344 | }, 50); 345 | }; 346 | 347 | template.hideLoadingSpinner = function() { 348 | var el = document.querySelector('#refresh-spinner-container'); 349 | if (el) { 350 | el.classList.add('shrink'); 351 | this.async(() => { 352 | this.syncing = false; 353 | }, 300); // wait for shrink animation to finish. 354 | } 355 | }; 356 | 357 | template.onSigninSuccess = function(e) { 358 | this.isAuthenticated = true; 359 | 360 | // Cached data? We're already using it. Bomb out before making unnecessary requests. 361 | if (DEBUG || !e.target.signedIn || !navigator.onLine) { 362 | return; 363 | } 364 | 365 | // Show visual loading indicator on first load. 366 | if (!this.threads || !this.threads.length) { 367 | this.showLoadingSpinner(); 368 | } 369 | 370 | var currentUser = gapi.auth2.getAuthInstance().currentUser.get(); 371 | var profile = currentUser.getBasicProfile(); 372 | var coverImage = this.user && this.user.cover ? this.user.cover : null; 373 | 374 | template.user = { 375 | id: profile.getId(), 376 | name: profile.getName(), 377 | profile: profile.getImageUrl(), 378 | email: profile.getEmail(), 379 | cover: coverImage 380 | }; 381 | 382 | // Note: these GMail API calls are wrapped by a promise that loads the 383 | // client library, once. No need to init gapi.client. 384 | this.refreshLabels(); 385 | this.refreshInbox(); 386 | 387 | GPlus.fetchFriendProfilePics().then(users => { 388 | // Add signed in user to list or profile pics. 389 | users[template.user.name] = template.user.profile; 390 | template.users = users; 391 | }); 392 | GPlus.fetchUsersCoverImage().then(coverImg => { 393 | template.set('user.cover', coverImg); 394 | }); 395 | }; 396 | 397 | template._onCachedThreadsEmpty = function(e) { 398 | this.isAuthenticated = false; 399 | }; 400 | 401 | template.signIn = function(e) { 402 | document.querySelector('google-signin').signIn(); 403 | }; 404 | 405 | template.signOut = function(e) { 406 | document.querySelector('google-signin').signOut(); 407 | localStorage.clear(); 408 | }; 409 | 410 | template.refreshApp = function() { 411 | location.reload(); 412 | }; 413 | 414 | template.headerClass = template._computeMainHeaderClass(template.narrow, 0); 415 | 416 | template.addEventListener('dom-change', e => { 417 | // Force binding updated when narrow has been calculated via binding. 418 | template.headerClass = template._computeMainHeaderClass( 419 | template.narrow, template.selectedThreads.length); 420 | 421 | var headerEl = document.querySelector('#mainheader'); 422 | var title = document.querySelector('.title'); 423 | 424 | template.$.drawerPanel.addEventListener('paper-header-transform', e => { 425 | 426 | if (!headerEl.classList.contains('tall')) { 427 | return; 428 | } 429 | 430 | var d = e.detail; 431 | 432 | // If at the top, allow swiping and pull down refresh. When scrolled, set 433 | // pan-y so track events don't fire in the y direction. 434 | //template.touchAction = d.y == 0 ? 'none' : 'pan-y'; 435 | 436 | // d.y: the amount that the header moves up 437 | // d.height: the height of the header when it is at its full size 438 | // d.condensedHeight: the height of the header when it is condensed 439 | //scale header's title 440 | var m = d.height - d.condensedHeight; 441 | var scale = Math.max(0.5, (m - d.y) / (m / 0.25) + 0.5); 442 | // var scale = Math.max(0.5, (m - d.y) / (m / 0.4) + 0.5); 443 | Polymer.Base.transform('scale(' + scale + ') translateZ(0)', title); 444 | 445 | // Adjust header's color 446 | //document.querySelector('#mainheader').style.color = (d.y >= d.height - d.condensedHeight) ? '#fff' : ''; 447 | }); 448 | }); 449 | 450 | var sw = document.querySelector('platinum-sw-register'); 451 | sw.addEventListener('service-worker-installed', e => { 452 | var toast = document.querySelector('#swtoast'); 453 | toast.show(); 454 | }); 455 | 456 | sw.addEventListener('service-worker-updated', e => { 457 | var toast = document.querySelector('#swtoast'); 458 | toast.text = 'A new version is available. Tap to refresh'; 459 | toast.show(); 460 | }); 461 | 462 | window.addEventListener('offline', () => { 463 | template.toggleToast('Connection is flaky. Content may be stale.'); 464 | }); 465 | 466 | // Log first paint. 467 | if (window.chrome.loadTimes) { 468 | var getFP = function() { 469 | let load = window.chrome.loadTimes(); 470 | let fp = (load.firstPaintTime - load.startLoadTime) * 1000; 471 | return Math.round(fp); 472 | }; 473 | window.onload = (e) => { 474 | let render = () => { 475 | let fp = getFP(); 476 | console.info(`First paint: ${fp} ms`); 477 | }; 478 | setTimeout(render, 100); // Wait a tick so we're guaranteed a fp time. 479 | }; 480 | } 481 | 482 | // // Prevent context menu. 483 | // window.oncontextmenu = function() { 484 | // return false; 485 | // }; 486 | 487 | })(); 488 | -------------------------------------------------------------------------------- /scripts/googleapis.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 | 17 | 'use strict'; 18 | 19 | class GoogleClientAPI { 20 | 21 | constructor(apiName, version) { 22 | this._loadedPromise = null; 23 | this.apiName = apiName; 24 | this.version = version; 25 | } 26 | 27 | get loaded() { 28 | return !!(window.gapi && gapi.client && this.api); 29 | } 30 | 31 | get api() { 32 | return !!window.gapi && gapi.client ? gapi.client[this.apiName] : null; 33 | } 34 | 35 | // Loads the google client API and returns a promise. Ensures the library is 36 | // loaded from network only once. 37 | init() { 38 | // Return the API if it's already loaded. 39 | if (this.loaded) { 40 | return Promise.resolve(this.api); 41 | } 42 | 43 | // Ensure we only load the client lib once. Subscribers will race for it. 44 | if (!this._loadedPromise) { 45 | this._loadedPromise = new Promise((resolve, reject) => { 46 | gapi.load('client', () => { 47 | gapi.client.load(this.apiName, this.version).then(() => { 48 | resolve(this.api); 49 | }); 50 | }); 51 | }); 52 | } 53 | 54 | return this._loadedPromise; 55 | } 56 | } 57 | 58 | export class GMail extends GoogleClientAPI { 59 | 60 | constructor() { 61 | super('gmail', 'v1'); 62 | this._FROM_HEADER_REGEX = new RegExp(/"?(.*?)"?\s?<(.*)>/); 63 | } 64 | 65 | static get Labels() { 66 | return { 67 | Colors: ['pink', 'orange', 'green', 'yellow', 'teal', 'purple'], 68 | UNREAD: 'UNREAD', 69 | STARRED: 'STARRED' 70 | }; 71 | } 72 | 73 | static getValueForHeaderField(headers, field) { 74 | // jshint boss:true 75 | for (let i = 0, header; header = headers[i]; ++i) { 76 | if (header.name === field || header.name === field.toLowerCase()) { 77 | return header.value; 78 | } 79 | } 80 | return null; 81 | } 82 | 83 | // Returns true if date1 is the same day as date2. 84 | static isToday(date1, date2) { 85 | return date1.getDate() === date2.getDate() && 86 | date1.getMonth() === date2.getMonth() && 87 | date1.getFullYear() === date2.getFullYear(); 88 | } 89 | 90 | fixUpMessages(resp) { 91 | let messages = resp.result.messages; 92 | 93 | // jshint boss:true 94 | for (let j = 0, m; m = messages[j]; ++j) { 95 | let headers = m.payload.headers; 96 | 97 | let date = new Date(GMail.getValueForHeaderField(headers, 'Date')); 98 | 99 | let isToday = GMail.isToday(new Date(), date); 100 | if (isToday) { 101 | // Example: Thu Sep 25 2014 14:43:18 GMT-0700 (PDT) -> 14:43:18. 102 | m.date = date.toLocaleTimeString().replace( 103 | /(\d{1,2}:\d{1,2}):\d{1,2}\s(AM|PM)/, '$1 $2'); 104 | } else { 105 | // Example: Thu Sep 25 2014 14:43:18 GMT-0700 (PDT) -> Sept 25. 106 | m.date = date.toDateString().split(' ').slice(1, 3).join(' '); 107 | } 108 | 109 | m.to = GMail.getValueForHeaderField(headers, 'To'); 110 | m.subject = GMail.getValueForHeaderField(headers, 'Subject'); 111 | 112 | let fromHeaders = GMail.getValueForHeaderField(headers, 'From'); 113 | 114 | // Use Reply-To Header if From header wasn't found. 115 | if (!fromHeaders) { 116 | fromHeaders = GMail.getValueForHeaderField(headers, 'Reply-To'); 117 | } 118 | 119 | let fromHeaderMatches = fromHeaders.match(this._FROM_HEADER_REGEX); 120 | 121 | m.from = {}; 122 | 123 | // Use name if one was found. Otherwise, use email address. 124 | if (fromHeaderMatches) { 125 | // If no a name, use email address for displayName. 126 | m.from.name = fromHeaderMatches[1].length ? fromHeaderMatches[1] : 127 | fromHeaderMatches[2]; 128 | m.from.email = fromHeaderMatches[2]; 129 | } else { 130 | m.from.name = fromHeaders.split('@')[0]; 131 | m.from.email = fromHeaders; 132 | } 133 | m.from.name = m.from.name.split('@')[0]; // Ensure email is split. 134 | 135 | m.unread = (m.labelIds ? 136 | m.labelIds.indexOf(GMail.Labels.UNREAD) !== -1 : false); 137 | m.starred = (m.labelIds ? 138 | m.labelIds.indexOf(GMail.Labels.STARRED) !== -1 : false); 139 | } 140 | 141 | return messages; 142 | } 143 | 144 | fetchLabels() { 145 | return this.init().then(api => { 146 | let fetchLabels = api.users.labels.list({userId: 'me'}); 147 | return fetchLabels.then(resp => { 148 | let labels = resp.result.labels.filter((label, i) => { 149 | // Add color to label. 150 | label.color = GMail.Labels.Colors[i % GMail.Labels.Colors.length]; 151 | return label.type !== 'system'; // Don't include system labels. 152 | }); 153 | 154 | let labelMap = labels.reduce((o, v, i) => { 155 | o[v.id] = v; 156 | return o; 157 | }, {}); 158 | 159 | return {labels: labels, labelMap: labelMap}; 160 | }); 161 | }); 162 | } 163 | 164 | fetchMail(q) { 165 | return this.init().then(api => { 166 | // Fetch only the emails in the user's inbox. 167 | let fetchThreads = api.users.threads.list({userId: 'me', q: q}); 168 | return fetchThreads.then(resp => { 169 | 170 | let batch = gapi.client.newBatch(); 171 | let threads = resp.result.threads; 172 | 173 | if (!threads) { 174 | return []; 175 | } 176 | 177 | // Setup a batch operation to fetch all messages for each thread. 178 | // jshint boss:true 179 | for (let i = 0, thread; thread = threads[i]; ++i) { 180 | let req = api.users.threads.get({userId: 'me', 'id': thread.id}); 181 | batch.add(req, {id: thread.id}); // Give each request a unique id for lookup later. 182 | } 183 | 184 | // Like Promise.all, but resp is an object instead of promise results. 185 | return batch.then(resp => { 186 | // jshint boss:true 187 | for (let i = 0, thread; thread = threads[i]; ++i) { 188 | thread.messages = this.fixUpMessages( 189 | resp.result[thread.id]).reverse(); 190 | //thread.archived = false; // initialize archived. 191 | } 192 | return threads; 193 | }); 194 | 195 | }); 196 | }); 197 | } 198 | } 199 | 200 | export class GPlus extends GoogleClientAPI { 201 | 202 | constructor() { 203 | super('plus', 'v1'); 204 | } 205 | 206 | get COVER_IMAGE_SIZE() { return 315; } 207 | get PROFILE_IMAGE_SIZE() { return 75; } 208 | 209 | _getAllUserProfileImages(users, nextPageToken, callback) { 210 | this.api.people.list({ 211 | userId: 'me', collection: 'visible', pageToken: nextPageToken 212 | }).then(resp => { 213 | 214 | // Map name to profile image. 215 | users = resp.result.items.reduce((o, v, i) => { 216 | o[v.displayName] = v.image.url; 217 | return o; 218 | }, users); 219 | 220 | if (resp.result.nextPageToken) { 221 | this._getAllUserProfileImages( 222 | users, resp.result.nextPageToken, callback); 223 | } else { 224 | callback(users); 225 | } 226 | 227 | }); 228 | } 229 | 230 | fetchFriendProfilePics() { 231 | let users = {}; 232 | return this.init().then(plus => { 233 | return new Promise((resolve, reject) => { 234 | this._getAllUserProfileImages(users, null, resolve); 235 | }); 236 | }); 237 | } 238 | 239 | fetchUsersCoverImage() { 240 | return this.init().then(api => { 241 | // Get user's profile pic, cover image, email, and name. 242 | return api.people.get({userId: 'me'}).then(resp => { 243 | // let img = resp.result.image && resp.result.image.url.replace(/(.+)\?sz=\d\d/, "$1?sz=" + this.PROFILE_IMAGE_SIZE); 244 | if (!resp.result.cover) { 245 | return null; 246 | } 247 | return resp.result.cover.coverPhoto.url.replace( 248 | /\/s\d{3}-/, '/s' + this.COVER_IMAGE_SIZE + '-'); 249 | }); 250 | }); 251 | } 252 | } 253 | -------------------------------------------------------------------------------- /scripts/polymer_metrics.js: -------------------------------------------------------------------------------- 1 | //jscs:disable requireArrowFunctions 2 | 3 | 'use strict'; 4 | 5 | function PolymerMetrics(optTemplate) { 6 | var t = optTemplate || document; 7 | 8 | t.addEventListener('dom-change', function(e) { 9 | window.performance.mark('mark_dom_change_fired'); 10 | }); 11 | 12 | document.addEventListener('WebComponentsReady', function(e) { 13 | window.performance.mark('mark_WebComponentsReady_fired'); 14 | }); 15 | } 16 | 17 | PolymerMetrics.prototype.getFirstPaintTime = function() { 18 | var firstPaint = 0; 19 | var firstPaintTime; 20 | 21 | if (window.chrome && window.chrome.loadTimes) { 22 | // Convert to ms 23 | var loadTimes = window.chrome.loadTimes(); 24 | firstPaint = loadTimes.firstPaintTime * 1000; 25 | firstPaintTime = firstPaint - (loadTimes.startLoadTime * 1000); 26 | } else if (typeof window.performance.timing.msFirstPaint === 'number') { // IE 27 | firstPaint = window.performance.timing.msFirstPaint; 28 | firstPaintTime = firstPaint - window.performance.timing.navigationStart; 29 | } 30 | 31 | return firstPaintTime; 32 | }; 33 | 34 | PolymerMetrics.prototype.printPageMetrics = function() { 35 | // First paint. 36 | // var times = chrome.loadTimes(); 37 | // firstPaintMetric = (times.firstPaintTime || performance.msFirstPaint) - times.startLoadTime; 38 | // firstPaint = Math.min(firstPaintRaf, firstPaintMetric); 39 | // console.info('First paint @', firstPaint); 40 | 41 | var p = window.performance; 42 | 43 | p.measure('DOMContentLoaded', 'navigationStart', 'domContentLoadedEventEnd'); 44 | p.measure('load', 'navigationStart', 'loadEventStart'); 45 | p.measure('dom-change', 'navigationStart', 'mark_dom_change_fired'); 46 | p.measure('WebComponentsReady', 'navigationStart', 47 | 'mark_WebComponentsReady_fired'); 48 | 49 | // console.log(performance.timing.domComplete - performance.timing.navigationStart) 50 | // console.debug('DOMContentLoaded', '@', p.timing.domComplete - p.timing.navigationStart, 'ms');//, item.duration, 'ms'); 51 | 52 | console.info('POLYMER METRICS'); 53 | 54 | var items = p.getEntriesByType('measure').sort(function(a, b) { 55 | return a.duration - b.duration; 56 | }); 57 | 58 | // jshint boss:true 59 | for (var i = 0, item; item = items[i]; ++i) { 60 | console.debug(item.name, '@', item.duration, 'ms'); 61 | } 62 | 63 | // console.info('First paint @', polyMetrics.getFirstPaintTime()); 64 | }; 65 | 66 | // if (window.PolymerMetrics) { 67 | // var polyMetrics = new PolymerMetrics(template); 68 | // window.addEventListener('load', polyMetrics.printPageMetrics); 69 | // } 70 | 71 | // Main page: 72 | // 73 | // var firstPaintRaf; 74 | // requestAnimationFrame(function() { 75 | // firstPaintRaf = performance.now(); 76 | // }); 77 | 78 | // if (window.PolymerMetrics) { 79 | // var polyMetrics = new PolymerMetrics(template); 80 | // window.addEventListener('load', polyMetrics.printPageMetrics); 81 | // } 82 | -------------------------------------------------------------------------------- /styles/app.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | 5 | body { 6 | font-family: "Roboto", sans-serif; 7 | -webkit-tap-highlight-color: transparent; 8 | tap-highlight-color: transparent; 9 | background-color: #fafafa; 10 | color: #444; 11 | font-weight: 400; 12 | -webkit-font-smoothing: antialiased; 13 | -moz-osx-font-smoothing: grayscale; 14 | margin: 0; 15 | } 16 | 17 | #loginscreen { 18 | z-index: 1; 19 | background-color: rgba(255,255,255,0.5); 20 | } 21 | 22 | #drawerPanel:not([narrow]) paper-scroll-header-panel .scroll-content { 23 | padding: 30px 0; 24 | background-color: #fafafa; 25 | } 26 | 27 | #drawerPanel[narrow][selected="drawer"] .fade-on-drawer-open { 28 | opacity: 0; 29 | } 30 | 31 | .fade-on-drawer-open { 32 | transition: opacity 300ms ease-in-out; 33 | } 34 | 35 | #drawerPanel paper-header-panel { 36 | /*background: #fafafa;*/ 37 | box-shadow: 1px 0 1px rgba(0, 0, 0, 0.1); 38 | color: #757575; 39 | } 40 | 41 | #drawerPanel paper-header-panel .separator { 42 | padding: 16px; 43 | font-size: 14px; 44 | font-weight: 500; 45 | color: #aaa; 46 | border-top: 1px solid #e0e0e0; 47 | } 48 | 49 | #drawerPanel paper-header-panel paper-menu { 50 | margin: 16px 0 0 0; 51 | } 52 | 53 | #drawerPanel paper-scroll-header-panel::shadow #headerBg { 54 | background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAUAAAADoCAYAAACeqD2dAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyhpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMDE0IDc5LjE1Njc5NywgMjAxNC8wOC8yMC0wOTo1MzowMiAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTQgKE1hY2ludG9zaCkiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6MTE4NTYwN0QxNDI0MTFFNUIwNjFGMkREOEVEQ0Y1MzYiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6MTE4NTYwN0UxNDI0MTFFNUIwNjFGMkREOEVEQ0Y1MzYiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDoxMTg1NjA3QjE0MjQxMUU1QjA2MUYyREQ4RURDRjUzNiIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDoxMTg1NjA3QzE0MjQxMUU1QjA2MUYyREQ4RURDRjUzNiIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PvzZgKQAAD8aSURBVHja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) no-repeat 115% 56px; 55 | background-size: 300px; 56 | background-color: #e51c23; 57 | } 58 | 59 | #drawerPanel paper-scroll-header-panel::shadow #condensedHeaderBg { 60 | background-color: #e51c23; 61 | opacity: 1 !important; /* Prevents the background color from dissolving. */ 62 | } 63 | 64 | paper-icon-item iron-icon { 65 | color: #757575; 66 | } 67 | 68 | paper-icon-item.iron-selected { 69 | background: #eee; 70 | } 71 | 72 | paper-icon-item.iron-selected iron-icon { 73 | color: #212121; 74 | } 75 | 76 | #navheader { 77 | font-size: inherit; 78 | color: white; 79 | flex-shrink: 0; 80 | } 81 | 82 | #navheader iron-image { 83 | background-blend-mode: multiply; 84 | background-repeat: no-repeat; 85 | background-color: #999; 86 | background-size: cover; 87 | } 88 | 89 | #navheader img.profile { 90 | border-radius: 50%; 91 | width: 64px; 92 | height: 64px; 93 | margin-bottom: 20px; 94 | } 95 | 96 | #coverimage { 97 | height: 192px; 98 | top: 0; 99 | } 100 | 101 | #navheader .bottom :last-child { 102 | font-size: small; 103 | opacity: 0.8; 104 | } 105 | 106 | #mainheader { 107 | color: #fff; 108 | background-color: transparent; 109 | box-shadow: 0 1px 5px rgba(0,0,0,0.3); 110 | } 111 | 112 | #mainheader .title { 113 | font-size: 20px; 114 | transform-origin: 0px 50%; 115 | margin-left: 70px; 116 | isolation: isolate; 117 | } 118 | 119 | #mainheader.tall .title { 120 | font-size: 40px; 121 | line-height: 40px; 122 | } 123 | 124 | #mainheader.selected-threads { 125 | background-color: #757575; 126 | } 127 | 128 | #mainheader .selected-item { 129 | visibility: hidden; 130 | will-change: transform; 131 | transform: rotateX(90deg); 132 | transition: transform 300ms ease-in-out; 133 | width: 0; 134 | padding: 0; 135 | margin: 0; 136 | } 137 | 138 | #mainheader paper-icon-button[icon="arrow-back"] { 139 | transform: translateX(-42px) rotateX(0); 140 | } 141 | 142 | #mainheader.selected-threads paper-icon-button[icon="arrow-back"] { 143 | transform: translateX(0) rotateX(0); 144 | } 145 | 146 | #mainheader.selected-threads .selected-item { 147 | visibility: visible; 148 | transform: rotateX(0); 149 | width: auto; 150 | margin: 0 8px; 151 | padding: 8px; 152 | } 153 | 154 | #mainheader.selected-threads paper-icon-button:not(.selected-item) { 155 | display: none; 156 | } 157 | 158 | #mainheader paper-dropdown { 159 | color: initial; 160 | } 161 | 162 | #mainheader.selected-threads paper-menu-button { 163 | padding: 0 !important; 164 | } 165 | 166 | paper-scroll-header-panel .scroll-content { 167 | height: 100%; 168 | } 169 | 170 | #threadlist { 171 | display: block; 172 | } 173 | 174 | #fab { 175 | position: fixed; 176 | bottom: 16px; 177 | right: 16px; 178 | color: white; 179 | will-change: transform; 180 | transition: transform 0.2s ease-in-out; 181 | } 182 | 183 | #fab.moveup { 184 | transform: translate3d(0, -48px, 0); 185 | } 186 | 187 | #refresh { 188 | z-index: 1; 189 | pointer-events: none; 190 | } 191 | 192 | #refresh-spinner-container { 193 | background: #fff; 194 | border-radius: 50%; 195 | padding: 10px; 196 | box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14), 197 | 0 1px 5px 0 rgba(0, 0, 0, 0.12), 198 | 0 3px 1px -2px rgba(0, 0, 0, 0.2); 199 | transition: transform 300ms cubic-bezier(0,0,0.2,1); 200 | will-change: transform; 201 | } 202 | 203 | #refresh-spinner-container.shrink { 204 | transform: scale(0); 205 | } 206 | 207 | #splash { 208 | display: flex; 209 | position: absolute; 210 | top: 0; 211 | left: 0; 212 | right: 0; 213 | bottom: 0; 214 | transition: opacity 400ms cubic-bezier(0,0,0.2,1); 215 | opacity: 0; 216 | will-change: opacity; 217 | z-index: 1; 218 | } 219 | 220 | #splash::before { 221 | position: absolute; 222 | content: ''; 223 | top: 0; 224 | left: 0; 225 | right: 0; 226 | bottom: 0; 227 | z-index: 2; 228 | background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAUAAAADoCAYAAACeqD2dAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyhpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMDE0IDc5LjE1Njc5NywgMjAxNC8wOC8yMC0wOTo1MzowMiAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTQgKE1hY2ludG9zaCkiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6MTE4NTYwN0QxNDI0MTFFNUIwNjFGMkREOEVEQ0Y1MzYiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6MTE4NTYwN0UxNDI0MTFFNUIwNjFGMkREOEVEQ0Y1MzYiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDoxMTg1NjA3QjE0MjQxMUU1QjA2MUYyREQ4RURDRjUzNiIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDoxMTg1NjA3QzE0MjQxMUU1QjA2MUYyREQ4RURDRjUzNiIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PvzZgKQAAD8aSURBVHja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) no-repeat 50% 50%; 229 | background-size: 150px; 230 | background-color: #E53935; 231 | } 232 | 233 | #swtoast { 234 | z-index: 3; /* above loading screen */ 235 | } 236 | 237 | body.loading #splash { 238 | opacity: 1; 239 | } 240 | 241 | .appshell_drawer { 242 | width: 256px; 243 | display: flex; 244 | flex-direction: column; 245 | z-index: 1; 246 | } 247 | 248 | .appshell_drawer_top { 249 | background-color: #212121; 250 | height: 192px; 251 | } 252 | 253 | .appshell_drawer_bottom { 254 | box-shadow: 1px 0 1px rgba(0,0,0,.1); 255 | flex: 1; 256 | } 257 | 258 | .appshell_main { 259 | display: flex; 260 | flex-direction: column; 261 | flex: 1; 262 | } 263 | 264 | .appshell_header { 265 | background: #e53935; 266 | height: 192px; 267 | position: relative; 268 | box-shadow: 0 1px 5px rgba(0,0,0,.3); 269 | color: #fff; 270 | } 271 | 272 | .appshell_header h1 { 273 | font-weight: 400; 274 | font-size: 40px; 275 | line-height: 40px; 276 | margin: 0 0 0 calc(70px + 16px); 277 | position: absolute; 278 | bottom: 12px; 279 | } 280 | 281 | .appshell_content { 282 | flex: 1; 283 | background-color: #fafafa; 284 | } 285 | 286 | .version { 287 | font-size: 10px; 288 | opacity: 0.3; 289 | position: fixed; 290 | bottom: 16px; 291 | left: 16px; 292 | } 293 | 294 | @media (min-width: 640px) { 295 | #splash { 296 | background: #fff; 297 | } 298 | #splash::before { 299 | display: none; 300 | } 301 | } 302 | -------------------------------------------------------------------------------- /sw-import.js: -------------------------------------------------------------------------------- 1 | importScripts('bower_components/platinum-sw/service-worker.js'); --------------------------------------------------------------------------------