diff --git a/.gitignore b/.gitignore index 397f0537..00aca6da 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ dist .nyc_output .tox .pytest_cache +*.iml android/app/src/main/assets ios/send-ios/assets/ios.js ios/send-ios/assets/vendor.js diff --git a/android/android.iml b/android/android.iml deleted file mode 100644 index eff6737c..00000000 --- a/android/android.iml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/android/android.js b/android/android.js index bdefe305..83e21c9c 100644 --- a/android/android.js +++ b/android/android.js @@ -24,7 +24,7 @@ import html from 'choo/html'; import Raven from 'raven-js'; import assets from '../common/assets'; -import header from '../app/ui/header'; +import Header from '../app/ui/header'; import locale from '../common/locales'; import storage from '../app/storage'; import controller from '../app/controller'; @@ -59,7 +59,7 @@ function body(main) { > - ${header(state, emit)} ${main(state, emit)} + ${state.cache(Header, 'header').render()} ${main(state, emit)} `; diff --git a/android/app/app.iml b/android/app/app.iml deleted file mode 100644 index 72864524..00000000 --- a/android/app/app.iml +++ /dev/null @@ -1,196 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/android/app/build.gradle b/android/app/build.gradle index 4f597e68..4b15da97 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -35,8 +35,7 @@ dependencies { } task generateAndLinkBundle(type: Exec, description: 'Generate the android.js bundle and link it into the assets directory') { - commandLine 'node' - args '../generateAndLinkBundle.js' + commandLine './buildAssets.sh' } tasks.withType(JavaCompile) { diff --git a/android/app/buildAssets.sh b/android/app/buildAssets.sh new file mode 100755 index 00000000..07cef6e8 --- /dev/null +++ b/android/app/buildAssets.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +npm run build +rm -rf src/main/assets +mkdir -p src/main/assets +cp -R ../../dist/* src/main/assets +sed -i '' 's/url(/url(\/android_asset/g' src/main/assets/app.*.css \ No newline at end of file diff --git a/android/generateAndLinkBundle.js b/android/generateAndLinkBundle.js deleted file mode 100644 index c646a18b..00000000 --- a/android/generateAndLinkBundle.js +++ /dev/null @@ -1,10 +0,0 @@ -const child_process = require('child_process'); -const path = require('path'); - -child_process.execSync('npm run build'); -child_process.execSync( - `cp -R ${path.resolve(__dirname, '../dist/*')} ${path.resolve( - __dirname, - 'app/src/main/assets' - )}` -); diff --git a/android/pages/home.js b/android/pages/home.js index 7a81203d..71db6190 100644 --- a/android/pages/home.js +++ b/android/pages/home.js @@ -18,6 +18,7 @@ module.exports = function(state, emit) { } const archives = state.storage.files + .filter(archive => !archive.expired) .map(archive => archiveTile(state, emit, archive)) .reverse(); diff --git a/app/main.js b/app/main.js index 37dbd6f5..29966925 100644 --- a/app/main.js +++ b/app/main.js @@ -1,5 +1,7 @@ import 'fast-text-encoding'; // MS Edge support import 'fluent-intl-polyfill'; +import choo from 'choo'; +import nanotiming from 'nanotiming'; import routes from './routes'; import capabilities from './capabilities'; import locale from '../common/locales'; @@ -14,7 +16,10 @@ import './main.css'; import User from './user'; (async function start() { - const app = routes(); + const app = routes(choo()); + if (process.env.NODE_ENV === 'production') { + nanotiming.disabled = true; + } if (navigator.doNotTrack !== '1' && window.RAVEN_CONFIG) { Raven.config(window.SENTRY_ID, window.RAVEN_CONFIG).install(); } diff --git a/app/routes.js b/app/routes.js index cb616c42..6c64cae8 100644 --- a/app/routes.js +++ b/app/routes.js @@ -1,37 +1,8 @@ const choo = require('choo'); -const html = require('choo/html'); -const nanotiming = require('nanotiming'); const download = require('./ui/download'); -const footer = require('./ui/footer'); -const fxPromo = require('./ui/fxPromo'); -const header = require('./ui/header'); +const body = require('./ui/body'); -nanotiming.disabled = true; - -function banner(state, emit) { - if (state.promo && !state.route.startsWith('/unsupported/')) { - return fxPromo(state, emit); - } -} - -function body(main) { - return function(state, emit) { - const b = html` - ${banner(state, emit)} - ${header(state, emit)} - ${main(state, emit)} - ${footer(state)} - `; - if (state.layout) { - // server side only - return state.layout(state, b); - } - return b; - }; -} - -module.exports = function() { - const app = choo(); +module.exports = function(app = choo()) { app.route('/', body(require('./ui/home'))); app.route('/download/:id', body(download)); app.route('/download/:id/:key', body(download)); diff --git a/app/serviceWorker.js b/app/serviceWorker.js index e110487d..e325e785 100644 --- a/app/serviceWorker.js +++ b/app/serviceWorker.js @@ -1,3 +1,5 @@ +import assets from '../common/assets'; +import { version } from '../package.json'; import Keychain from './keychain'; import { downloadStream } from './api'; import { transformStream } from './streams'; @@ -8,11 +10,11 @@ let noSave = false; const map = new Map(); self.addEventListener('install', event => { - self.skipWaiting(); + event.waitUntil(precache()); }); self.addEventListener('activate', event => { - self.clients.claim(); + event.waitUntil(self.clients.claim()); }); async function decryptStream(id) { @@ -77,11 +79,32 @@ async function decryptStream(id) { } } +async function precache() { + const oldCaches = await caches.keys(); + for (const c of oldCaches) { + if (c !== version) { + await caches.delete(c); + } + } + const cache = await caches.open(version); + const images = assets.match(/.*\.(png|svg|jpg)$/); + await cache.addAll(images); + return self.skipWaiting(); +} + +async function cachedOrFetch(req) { + const cache = await caches.open(version); + const cached = await cache.match(req); + return cached || fetch(req); +} + self.onfetch = event => { const req = event.request; const match = /\/api\/download\/([A-Fa-f0-9]{4,})/.exec(req.url); if (match) { event.respondWith(decryptStream(match[1])); + } else { + event.respondWith(cachedOrFetch(req)); } }; diff --git a/app/ui/account.js b/app/ui/account.js index 3e25a0dc..395cb53b 100644 --- a/app/ui/account.js +++ b/app/ui/account.js @@ -1,58 +1,103 @@ const html = require('choo/html'); +const Component = require('choo/component'); -module.exports = function(state, emit) { - if (!state.capabilities.account) { - return null; +class Account extends Component { + constructor(name, state, emit) { + super(name); + this.state = state; + this.emit = emit; + this.enabled = state.capabilities.account; + this.local = state.components[name] = {}; + this.setState(); } - const user = state.user; - if (!user.loggedIn) { - return html``; - } - return html`
- - -
`; - function avatarClick(event) { + avatarClick(event) { event.preventDefault(); const menu = document.getElementById('accountMenu'); menu.classList.toggle('invisible'); menu.focus(); } - function hideMenu(event) { + hideMenu(event) { event.stopPropagation(); const menu = document.getElementById('accountMenu'); menu.classList.add('invisible'); } - function login(event) { + login(event) { event.preventDefault(); - emit('login'); + this.emit('login'); } - function logout(event) { + logout(event) { event.preventDefault(); - emit('logout'); + this.emit('logout'); } -}; + + changed() { + return this.local.loggedIn !== this.state.user.loggedIn; + } + + setState() { + const changed = this.changed(); + if (changed) { + this.local.loggedIn = this.state.user.loggedIn; + } + return changed; + } + + update() { + return this.setState(); + } + + createElement() { + if (!this.enabled) { + return html` +
+ `; + } + const user = this.state.user; + const translate = this.state.translate; + if (!this.local.loggedIn) { + return html` +
+ +
+ `; + } + return html` +
+ + +
+ `; + } +} + +module.exports = Account; diff --git a/app/ui/body.js b/app/ui/body.js new file mode 100644 index 00000000..c208acf9 --- /dev/null +++ b/app/ui/body.js @@ -0,0 +1,28 @@ +const html = require('choo/html'); +const Promo = require('./promo'); +const Header = require('./header'); +const Footer = require('./footer'); + +function banner(state) { + if (state.promo && !state.route.startsWith('/unsupported/')) { + return state.cache(Promo, 'promo').render(); + } +} + +module.exports = function body(main) { + return function(state, emit) { + const b = html` + + ${banner(state, emit)} ${state.cache(Header, 'header').render()} + ${main(state, emit)} ${state.cache(Footer, 'footer').render()} + + `; + if (state.layout) { + // server side only + return state.layout(state, b); + } + return b; + }; +}; diff --git a/app/ui/footer.js b/app/ui/footer.js index 913b88bc..f5933eb1 100644 --- a/app/ui/footer.js +++ b/app/ui/footer.js @@ -1,52 +1,74 @@ const html = require('choo/html'); +const Component = require('choo/component'); const version = require('../../package.json').version; const { browserName } = require('../utils'); -module.exports = function(state) { - const browser = browserName(); - const feedbackUrl = `https://qsurvey.mozilla.com/s3/txp-firefox-send?ver=${version}&browser=${browser}`; - const footer = html``; - // HACK - // We only want to render this once because we - // toggle the targets of the links with utils/openLinksInNewTab - footer.isSameNode = function(target) { - return target && target.nodeName && target.nodeName === 'FOOTER'; - }; - return footer; -}; +class Footer extends Component { + constructor(name, state) { + super(name); + this.state = state; + } + + update() { + return false; + } + + createElement() { + const translate = this.state.translate; + const browser = browserName(); + const feedbackUrl = `https://qsurvey.mozilla.com/s3/txp-firefox-send?ver=${version}&browser=${browser}`; + return html` + + `; + } +} + +module.exports = Footer; diff --git a/app/ui/fxPromo.js b/app/ui/fxPromo.js deleted file mode 100644 index b791f291..00000000 --- a/app/ui/fxPromo.js +++ /dev/null @@ -1,19 +0,0 @@ -const html = require('choo/html'); -const assets = require('../../common/assets'); - -module.exports = function() { - return html` -
-
- Firefox - Send is brought to you by the all-new Firefox. - Download Firefox now ≫ - -
-
`; -}; diff --git a/app/ui/header.js b/app/ui/header.js index 9deac68f..768c5d51 100644 --- a/app/ui/header.js +++ b/app/ui/header.js @@ -1,25 +1,34 @@ const html = require('choo/html'); -const account = require('./account'); +const Component = require('choo/component'); +const Account = require('./account'); -module.exports = function(state, emit) { - const header = html` -
- - ${account(state, emit)} - -
`; - // HACK - // We only want to render this once because we - // toggle the targets of the links with utils/openLinksInNewTab - // header.isSameNode = function(target) { - // return target && target.nodeName && target.nodeName === 'HEADER'; - // }; - return header; -}; +class Header extends Component { + constructor(name, state, emit) { + super(name); + this.state = state; + this.emit = emit; + this.account = state.cache(Account, 'account'); + } + + update() { + this.account.render(); + return false; + } + + createElement() { + return html` +
+ + ${this.account.render()} +
+ `; + } +} + +module.exports = Header; diff --git a/app/ui/home.js b/app/ui/home.js index 813c3286..8e031f09 100644 --- a/app/ui/home.js +++ b/app/ui/home.js @@ -5,9 +5,9 @@ const modal = require('./modal'); const intro = require('./intro'); module.exports = function(state, emit) { - const archives = state.storage.files.map(archive => - archiveTile(state, emit, archive) - ); + const archives = state.storage.files + .filter(archive => !archive.expired) + .map(archive => archiveTile(state, emit, archive)); let left = ''; if (state.uploading) { left = archiveTile.uploading(state, emit); @@ -23,11 +23,12 @@ module.exports = function(state, emit) { : list(archives, 'list-reset h-full overflow-y-scroll', 'mb-3'); return html` -
- ${state.modal && modal(state, emit)} -
-
${left}
-
${right}
-
-
`; +
+ ${state.modal && modal(state, emit)} +
+
${left}
+
${right}
+
+
+ `; }; diff --git a/app/ui/promo.js b/app/ui/promo.js new file mode 100644 index 00000000..de9d5fc6 --- /dev/null +++ b/app/ui/promo.js @@ -0,0 +1,39 @@ +const html = require('choo/html'); +const Component = require('choo/component'); +const assets = require('../../common/assets'); + +class Promo extends Component { + constructor(name) { + super(name); + } + + update() { + return false; + } + + createElement() { + return html` +
+
+ Firefox + Send is brought to you by the all-new Firefox. + Download Firefox now ≫ + +
+
+ `; + } +} + +module.exports = Promo; diff --git a/app/utils.js b/app/utils.js index 761477ca..174cd238 100644 --- a/app/utils.js +++ b/app/utils.js @@ -184,8 +184,17 @@ async function streamToArrayBuffer(stream, size) { } function list(items, ulStyle = '', liStyle = '') { - const lis = items.map(i => html`
  • ${i}
  • `); - return html``; + const lis = items.map( + i => + html` +
  • ${i}
  • + ` + ); + return html` + + `; } function secondsToL10nId(seconds) { @@ -199,6 +208,9 @@ function secondsToL10nId(seconds) { } function timeLeft(milliseconds) { + if (milliseconds < 1) { + return { id: 'linkExpiredAlt' }; + } const minutes = Math.floor(milliseconds / 1000 / 60); const hours = Math.floor(minutes / 60); const days = Math.floor(hours / 24); diff --git a/webpack.config.js b/webpack.config.js index 7dd10331..a0414283 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -34,7 +34,56 @@ const serviceWorker = { path: path.resolve(__dirname, 'dist'), publicPath: '/' }, - devtool: 'source-map' + devtool: 'source-map', + module: { + rules: [ + { + include: [require.resolve('./assets/cryptofill')], + use: [ + { + loader: 'file-loader', + options: { + name: '[name].[hash:8].[ext]' + } + } + ] + }, + { + test: /\.(png|jpg)$/, + loader: 'file-loader', + options: { + name: '[name].[hash:8].[ext]' + } + }, + { + test: /\.svg$/, + use: [ + { + loader: 'file-loader', + options: { + name: '[name].[hash:8].[ext]' + } + }, + { + loader: 'svgo-loader', + options: { + plugins: [ + { removeViewBox: false }, // true causes stretched images + { convertStyleToAttrs: true }, // for CSP, no unsafe-eval + { removeTitle: true } // for smallness + ] + } + } + ] + }, + { + // loads all assets from assets/ for use by common/assets.js + test: require.resolve('./build/generate_asset_map.js'), + use: ['babel-loader', 'val-loader'] + } + ] + }, + plugins: [new webpack.IgnorePlugin(/\.\.\/dist/)] }; const web = { @@ -185,6 +234,7 @@ const web = { from: '*.*' } ]), + new webpack.EnvironmentPlugin(['NODE_ENV']), new webpack.IgnorePlugin(/\.\.\/dist/), // used in common/*.js new webpack.IgnorePlugin(/require-from-string/), // used in common/locales.js new webpack.HashedModuleIdsPlugin(),