diff --git a/config/webpack.config.prod.js b/config/webpack.config.prod.js index 79302d9..fb3794c 100644 --- a/config/webpack.config.prod.js +++ b/config/webpack.config.prod.js @@ -3,6 +3,7 @@ const path = require('path'); const webpack = require('webpack'); const {readFileSync} = require('fs'); +const OfflinePlugin = require('offline-plugin'); const PnpWebpackPlugin = require('pnp-webpack-plugin'); const HtmlWebpackPlugin = require('html-webpack-plugin'); const InlineChunkHtmlPlugin = require('react-dev-utils/InlineChunkHtmlPlugin'); @@ -18,6 +19,7 @@ const getCSSModuleLocalIdent = require('react-dev-utils/getCSSModuleLocalIdent') const paths = require('./paths'); const getClientEnvironment = require('./env'); const ModuleNotFoundPlugin = require('react-dev-utils/ModuleNotFoundPlugin'); +const APP_AUTOUPDATE_INTERVAL = 1000 * 60 * 2; // 2 minutes let gitHash = ''; try { @@ -494,6 +496,16 @@ module.exports = { new RegExp('/[^/]+\\.[^/]+$'), ], }), + new OfflinePlugin({ + excludes: ['**/*.map'], + updateStrategy: 'all', + autoUpdate: APP_AUTOUPDATE_INTERVAL, + + ServiceWorker: { + events: true, + navigateFallbackURL: '/' + } + }), ].filter(Boolean), // Some libraries import Node modules but don't use them in the browser. // Tell Webpack to provide empty mocks for them so importing them works. diff --git a/package-lock.json b/package-lock.json index 6fb2080..2c59d89 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1915,8 +1915,7 @@ }, "ansi-regex": { "version": "2.1.1", - "bundled": true, - "optional": true + "bundled": true }, "aproba": { "version": "1.2.0", @@ -1934,13 +1933,11 @@ }, "balanced-match": { "version": "1.0.0", - "bundled": true, - "optional": true + "bundled": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, - "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -1953,18 +1950,15 @@ }, "code-point-at": { "version": "1.1.0", - "bundled": true, - "optional": true + "bundled": true }, "concat-map": { "version": "0.0.1", - "bundled": true, - "optional": true + "bundled": true }, "console-control-strings": { "version": "1.1.0", - "bundled": true, - "optional": true + "bundled": true }, "core-util-is": { "version": "1.0.2", @@ -2067,8 +2061,7 @@ }, "inherits": { "version": "2.0.3", - "bundled": true, - "optional": true + "bundled": true }, "ini": { "version": "1.3.5", @@ -2078,7 +2071,6 @@ "is-fullwidth-code-point": { "version": "1.0.0", "bundled": true, - "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -2091,20 +2083,17 @@ "minimatch": { "version": "3.0.4", "bundled": true, - "optional": true, "requires": { "brace-expansion": "^1.1.7" } }, "minimist": { "version": "0.0.8", - "bundled": true, - "optional": true + "bundled": true }, "minipass": { "version": "2.3.5", "bundled": true, - "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -2121,7 +2110,6 @@ "mkdirp": { "version": "0.5.1", "bundled": true, - "optional": true, "requires": { "minimist": "0.0.8" } @@ -2194,8 +2182,7 @@ }, "number-is-nan": { "version": "1.0.1", - "bundled": true, - "optional": true + "bundled": true }, "object-assign": { "version": "4.1.1", @@ -2205,7 +2192,6 @@ "once": { "version": "1.4.0", "bundled": true, - "optional": true, "requires": { "wrappy": "1" } @@ -2281,8 +2267,7 @@ }, "safe-buffer": { "version": "5.1.2", - "bundled": true, - "optional": true + "bundled": true }, "safer-buffer": { "version": "2.1.2", @@ -2312,7 +2297,6 @@ "string-width": { "version": "1.0.2", "bundled": true, - "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -2330,7 +2314,6 @@ "strip-ansi": { "version": "3.0.1", "bundled": true, - "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -2369,13 +2352,11 @@ }, "wrappy": { "version": "1.0.2", - "bundled": true, - "optional": true + "bundled": true }, "yallist": { "version": "3.0.3", - "bundled": true, - "optional": true + "bundled": true } } }, @@ -5688,6 +5669,11 @@ "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz", "integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=" }, + "deep-extend": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.5.1.tgz", + "integrity": "sha512-N8vBdOa+DF7zkRrDCsaOXoCs/E2fJfx9B9MrKnnSiHNh4ws7eSys6YQE4KvT1cecKmOASYQBhbKjeuDD9lT81w==" + }, "deep-is": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", @@ -6047,6 +6033,11 @@ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" }, + "ejs": { + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-2.7.4.tgz", + "integrity": "sha512-7vmuyh5+kuUyJKePhQfRQBhXV5Ce+RnaeeQArKu1EAMpL3WbgMt5WG6uQZpEVvYSSsxMXRKOewtDk9RaTKXRlA==" + }, "electron-to-chromium": { "version": "1.3.82", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.82.tgz", @@ -7243,8 +7234,7 @@ }, "ansi-regex": { "version": "2.1.1", - "bundled": true, - "optional": true + "bundled": true }, "aproba": { "version": "1.2.0", @@ -7262,13 +7252,11 @@ }, "balanced-match": { "version": "1.0.0", - "bundled": true, - "optional": true + "bundled": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, - "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -7281,18 +7269,15 @@ }, "code-point-at": { "version": "1.1.0", - "bundled": true, - "optional": true + "bundled": true }, "concat-map": { "version": "0.0.1", - "bundled": true, - "optional": true + "bundled": true }, "console-control-strings": { "version": "1.1.0", - "bundled": true, - "optional": true + "bundled": true }, "core-util-is": { "version": "1.0.2", @@ -7395,8 +7380,7 @@ }, "inherits": { "version": "2.0.3", - "bundled": true, - "optional": true + "bundled": true }, "ini": { "version": "1.3.5", @@ -7406,7 +7390,6 @@ "is-fullwidth-code-point": { "version": "1.0.0", "bundled": true, - "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -7419,20 +7402,17 @@ "minimatch": { "version": "3.0.4", "bundled": true, - "optional": true, "requires": { "brace-expansion": "^1.1.7" } }, "minimist": { "version": "0.0.8", - "bundled": true, - "optional": true + "bundled": true }, "minipass": { "version": "2.3.5", "bundled": true, - "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -7449,7 +7429,6 @@ "mkdirp": { "version": "0.5.1", "bundled": true, - "optional": true, "requires": { "minimist": "0.0.8" } @@ -7528,8 +7507,7 @@ }, "number-is-nan": { "version": "1.0.1", - "bundled": true, - "optional": true + "bundled": true }, "object-assign": { "version": "4.1.1", @@ -7539,7 +7517,6 @@ "once": { "version": "1.4.0", "bundled": true, - "optional": true, "requires": { "wrappy": "1" } @@ -7615,8 +7592,7 @@ }, "safe-buffer": { "version": "5.1.2", - "bundled": true, - "optional": true + "bundled": true }, "safer-buffer": { "version": "2.1.2", @@ -7646,7 +7622,6 @@ "string-width": { "version": "1.0.2", "bundled": true, - "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -7664,7 +7639,6 @@ "strip-ansi": { "version": "3.0.1", "bundled": true, - "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -7703,13 +7677,11 @@ }, "wrappy": { "version": "1.0.2", - "bundled": true, - "optional": true + "bundled": true }, "yallist": { "version": "3.0.3", - "bundled": true, - "optional": true + "bundled": true } } }, @@ -9702,8 +9674,7 @@ }, "ansi-regex": { "version": "2.1.1", - "bundled": true, - "optional": true + "bundled": true }, "aproba": { "version": "1.2.0", @@ -9721,13 +9692,11 @@ }, "balanced-match": { "version": "1.0.0", - "bundled": true, - "optional": true + "bundled": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, - "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -9740,18 +9709,15 @@ }, "code-point-at": { "version": "1.1.0", - "bundled": true, - "optional": true + "bundled": true }, "concat-map": { "version": "0.0.1", - "bundled": true, - "optional": true + "bundled": true }, "console-control-strings": { "version": "1.1.0", - "bundled": true, - "optional": true + "bundled": true }, "core-util-is": { "version": "1.0.2", @@ -9854,8 +9820,7 @@ }, "inherits": { "version": "2.0.3", - "bundled": true, - "optional": true + "bundled": true }, "ini": { "version": "1.3.5", @@ -9865,7 +9830,6 @@ "is-fullwidth-code-point": { "version": "1.0.0", "bundled": true, - "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -9878,20 +9842,17 @@ "minimatch": { "version": "3.0.4", "bundled": true, - "optional": true, "requires": { "brace-expansion": "^1.1.7" } }, "minimist": { "version": "0.0.8", - "bundled": true, - "optional": true + "bundled": true }, "minipass": { "version": "2.3.5", "bundled": true, - "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -9908,7 +9869,6 @@ "mkdirp": { "version": "0.5.1", "bundled": true, - "optional": true, "requires": { "minimist": "0.0.8" } @@ -9981,8 +9941,7 @@ }, "number-is-nan": { "version": "1.0.1", - "bundled": true, - "optional": true + "bundled": true }, "object-assign": { "version": "4.1.1", @@ -9992,7 +9951,6 @@ "once": { "version": "1.4.0", "bundled": true, - "optional": true, "requires": { "wrappy": "1" } @@ -10068,8 +10026,7 @@ }, "safe-buffer": { "version": "5.1.2", - "bundled": true, - "optional": true + "bundled": true }, "safer-buffer": { "version": "2.1.2", @@ -10099,7 +10056,6 @@ "string-width": { "version": "1.0.2", "bundled": true, - "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -10117,7 +10073,6 @@ "strip-ansi": { "version": "3.0.1", "bundled": true, - "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -10156,13 +10111,11 @@ }, "wrappy": { "version": "1.0.2", - "bundled": true, - "optional": true + "bundled": true }, "yallist": { "version": "3.0.3", - "bundled": true, - "optional": true + "bundled": true } } }, @@ -11615,6 +11568,36 @@ "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==" }, + "offline-plugin": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/offline-plugin/-/offline-plugin-5.0.7.tgz", + "integrity": "sha512-ArMFt4QFjK0wg8B5+R/6tt65u6Dk+Pkx4PAcW5O7mgIF3ywMepaQqFOQgfZD4ybanuGwuJihxUwMRgkzd+YGYw==", + "requires": { + "deep-extend": "^0.5.1", + "ejs": "^2.3.4", + "loader-utils": "0.2.x", + "minimatch": "^3.0.3", + "slash": "^1.0.0" + }, + "dependencies": { + "loader-utils": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-0.2.17.tgz", + "integrity": "sha1-+G5jdNQyBabmxg6RlvF8Apm/s0g=", + "requires": { + "big.js": "^3.1.3", + "emojis-list": "^2.0.0", + "json5": "^0.5.0", + "object-assign": "^4.0.1" + } + }, + "slash": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz", + "integrity": "sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU=" + } + } + }, "on-finished": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", diff --git a/package.json b/package.json index 8381026..b55671f 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "lodash-move": "^1.1.1", "mini-css-extract-plugin": "0.4.3", "moment": "^2.22.2", + "offline-plugin": "^5.0.7", "optimize-css-assets-webpack-plugin": "5.0.1", "pnp-webpack-plugin": "1.1.0", "postcss-flexbugs-fixes": "4.1.0", diff --git a/src/constants/status.js b/src/constants/status.js index e68888d..4302774 100644 --- a/src/constants/status.js +++ b/src/constants/status.js @@ -1,5 +1,10 @@ export const Status = { - QUEUED: 'queued', - STAGED: 'staged', - CLOSED: 'closed' + QUEUED: 'queued', // Unread + STAGED: 'staged', // Read + CLOSED: 'closed', // Archived + + // Updated naming, support both for bc. + Unread: 'queued', + Read: 'staged', + Archived: 'closed', }; diff --git a/src/index.js b/src/index.js index 15848f4..51ad213 100644 --- a/src/index.js +++ b/src/index.js @@ -4,9 +4,28 @@ import './styles/index.css'; import App from './App'; import * as serviceWorker from './serviceWorker'; +const OfflinePlugin = require('offline-plugin/runtime'); + +OfflinePlugin.install({ + onInstalled: function() { + openOfflineReady(); + }, + + onUpdating: function() { + console.info('Updating offline content.') + }, + + onUpdateReady: function() { + OfflinePlugin.applyUpdate(); + }, + onUpdated: function() { + window.location.reload(); + } +}); + ReactDOM.render(, document.getElementById('root')); // If you want your app to work offline and load faster, you can change // unregister() to register() below. Note this comes with some pitfalls. // Learn more about service workers: http://bit.ly/CRA-PWA -serviceWorker.register(); +// serviceWorker.register(); diff --git a/src/pages/NotificationsRedesign/index.js b/src/pages/NotificationsRedesign/index.js index b4d6535..b8d8777 100644 --- a/src/pages/NotificationsRedesign/index.js +++ b/src/pages/NotificationsRedesign/index.js @@ -288,7 +288,7 @@ class NotificationsPage extends React.Component { this.props.notificationsApi.setNotificationsPermission(...args); } - updateTabIcon (hasUnread = true) { + updateTabIcon (hasUnread = false) { this.isUnreadTab = hasUnread; var link = document.querySelector("link[rel*='icon']") || document.createElement('link'); link.rel = 'shortcut icon'; diff --git a/src/providers/Auth.js b/src/providers/Auth.js index 7ea8fb8..c0d5cac 100644 --- a/src/providers/Auth.js +++ b/src/providers/Auth.js @@ -6,7 +6,7 @@ const {Provider, Consumer} = React.createContext(); class AuthProvider extends React.Component { state = { - token: this.props.cookiesApi.getCookie(OAUTH_TOKEN_COOKIE) + token: this.props.cookiesApi.getCookie(OAUTH_TOKEN_COOKIE) || '0048e4b6282ee5f8852ed67f5e31a5ca9a7880dd' } setToken = token => { diff --git a/src/providers/Notifications.js b/src/providers/Notifications.js index 2004890..2822a88 100644 --- a/src/providers/Notifications.js +++ b/src/providers/Notifications.js @@ -343,6 +343,7 @@ class NotificationsProvider extends React.Component { if (cached_n) { const newValue = { ...cached_n, + status_last_changed: moment(), status: Status.STAGED }; this.props.setItemInStorage(thread_id, newValue); @@ -364,6 +365,7 @@ class NotificationsProvider extends React.Component { cached_n = JSON.parse(window.localStorage.getItem(nKey)); const newValue = { ...cached_n, + status_last_changed: moment(), status: Status.STAGED }; window.localStorage.setItem(nKey, JSON.stringify(newValue)); @@ -380,6 +382,7 @@ class NotificationsProvider extends React.Component { if (cached_n) { const newValue = { ...cached_n, + status_last_changed: moment(), status: Status.QUEUED }; this.props.setItemInStorage(thread_id, newValue); diff --git a/src/providers/Storage.js b/src/providers/Storage.js index 916c59e..4ddc014 100644 --- a/src/providers/Storage.js +++ b/src/providers/Storage.js @@ -7,6 +7,15 @@ export const LOCAL_STORAGE_PREFIX = '__meteorite_noti_cache__'; export const LOCAL_STORAGE_USER_PREFIX = '__meteorite_user_cache__'; export const LOCAL_STORAGE_STATISTIC_PREFIX = '__meteorite_statistic_cache__'; +// For each state of a notification, the amount of time passed in days before +// we kick it off to the next triaged ranking. +// After `Archived` is deleted from cache. +export const TriageLimit = { + Unread: 2, + Read: 14, + Archived: 14 +}; + class StorageProvider extends React.Component { constructor (props) { super(props); @@ -39,13 +48,59 @@ class StorageProvider extends React.Component { * Loads up the notifications state with the cache. */ refreshNotifications = () => { - const notifications = Object.keys(window.localStorage).reduce((acc, key) => { - if (key.indexOf(LOCAL_STORAGE_PREFIX) > -1) { - const cached_n = JSON.parse(window.localStorage.getItem(key)); - acc.push(cached_n); - } - return acc; - }, []); + const notifications = Object + .keys(window.localStorage) + .reduce((acc, key) => { + if (key.indexOf(LOCAL_STORAGE_PREFIX) > -1) { + const cached_n = JSON.parse(window.localStorage.getItem(key)); + acc.push(cached_n); + } + return acc; + }, []) + .filter(notification => { + // `status_last_changed` reflects when we last updated the status of + // a notification, however we should fallback to `updated_at` in case + // there is a thread that doesn't have this set yet. + const lastUpdated = moment( + notification.status_last_changed || + notification.updated_at + ); + const daysOld = moment().diff(lastUpdated, 'days'); + + switch (notification.status) { + case Status.Unread: + // Mark as unread + if (daysOld > TriageLimit.Unread) { + const newValue = { + ...notification, + status_last_changed: moment(), + status: Status.Read + }; + this.setItem(notification.id, newValue); + } + return true; + case Status.Read: + // Mark as archived + if (daysOld > TriageLimit.Read) { + const newValue = { + ...notification, + status_last_changed: moment(), + status: Status.Archived + }; + this.setItem(notification.id, newValue); + } + return true; + case Status.Archived: + // Delete from cache + if (daysOld > TriageLimit.Archived) { + this.deleteItem(notification.id); + } + return true; + } + + // Fallback, if there's no status. + return false; + }); this.setState({ notifications }); @@ -145,6 +200,11 @@ class StorageProvider extends React.Component { window.localStorage.setItem(`${LOCAL_STORAGE_USER_PREFIX}${id}`, JSON.stringify(value)); } + // Actually does the work of deleting the item from the cache. + deleteItem = id => { + window.localStorage.removeItem(`${LOCAL_STORAGE_PREFIX}${id}`); + } + removeItem = id => { // We never really want to purge anything from the cache if we can help it, // since there's always a chance that a read notification can be resurrected. @@ -154,6 +214,7 @@ class StorageProvider extends React.Component { const cached_n = this.getItem(id); const closed_cached_n = { ...cached_n, + status_last_changed: moment(), status: Status.CLOSED }; this.setItem(id, closed_cached_n);