New offline plugin, auto triage

This commit is contained in:
Nicholas Zuber 2020-01-26 15:23:58 -05:00
parent 60b4348166
commit 71dd090ca3
9 changed files with 187 additions and 103 deletions

View File

@ -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.

163
package-lock.json generated
View File

@ -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",

View File

@ -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",

View File

@ -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',
};

View File

@ -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(<App />, 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();

View File

@ -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';

View File

@ -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 => {

View File

@ -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);

View File

@ -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);