From 65f084193ebd725d50ba6b61ef8fa5ba5668d4bd Mon Sep 17 00:00:00 2001 From: Michael Mayer Date: Sat, 16 Nov 2019 16:06:34 +0100 Subject: [PATCH] Add event hub & websocket for push notifications Signed-off-by: Michael Mayer --- frontend/package-lock.json | 124 ++++++++----------- frontend/package.json | 1 + frontend/src/app.js | 2 + frontend/src/common/alert.js | 10 +- frontend/src/common/api.js | 5 +- frontend/src/common/websocket.js | 22 ++++ frontend/src/component/p-alert.vue | 18 ++- frontend/src/component/p-photo-clipboard.vue | 6 +- frontend/src/pages/library/import.vue | 42 +++++-- frontend/src/pages/library/index.vue | 42 +++++-- frontend/src/pages/library/upload.vue | 11 +- go.mod | 10 +- go.sum | 20 ++- internal/api/import.go | 11 +- internal/api/index.go | 11 +- internal/api/websocket.go | 79 ++++++++++++ internal/event/hub.go | 61 +++++++++ internal/event/hub_test.go | 31 +++++ internal/photoprism/importer.go | 7 +- internal/photoprism/indexer.go | 12 ++ internal/server/routes.go | 8 ++ 21 files changed, 407 insertions(+), 126 deletions(-) create mode 100644 frontend/src/common/websocket.js create mode 100644 internal/api/websocket.go create mode 100644 internal/event/hub.go create mode 100644 internal/event/hub_test.go diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 49523b8f8..e86522a5b 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -4404,39 +4404,6 @@ } } }, - "engine.io-client": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-3.2.1.tgz", - "integrity": "sha512-y5AbkytWeM4jQr7m/koQLc5AxpRKC1hEVUb/s1FUAWEJq5AzJJ4NLvzuKPuxtDi5Mq755WuDvZ6Iv2rXj4PTzw==", - "requires": { - "component-emitter": "1.2.1", - "component-inherit": "0.0.3", - "debug": "~3.1.0", - "engine.io-parser": "~2.1.1", - "has-cors": "1.1.0", - "indexof": "0.0.1", - "parseqs": "0.0.5", - "parseuri": "0.0.5", - "ws": "~3.3.1", - "xmlhttprequest-ssl": "~1.5.4", - "yeast": "0.1.2" - }, - "dependencies": { - "component-emitter": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", - "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=" - }, - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "requires": { - "ms": "2.0.0" - } - } - } - }, "engine.io-parser": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-2.1.3.tgz", @@ -6171,9 +6138,9 @@ } }, "handlebars": { - "version": "4.5.1", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.5.1.tgz", - "integrity": "sha512-C29UoFzHe9yM61lOsIlCE5/mQVGrnIOrOq7maQl76L7tYPCgC1og0Ajt6uWnX4ZTxBPnjw+CUvawphwCfJgUnA==", + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.5.2.tgz", + "integrity": "sha512-29Zxv/cynYB7mkT1rVWQnV7mGX6v7H/miQ6dbEpYTKq5eJBN7PsRB+ViYJlcT6JINTSu4dVB9kOqEun78h6Exg==", "requires": { "neo-async": "^2.6.0", "optimist": "^0.6.1", @@ -10713,42 +10680,6 @@ "socket.io-client": "2.1.1", "socket.io-parser": "~3.2.0" }, - "dependencies": { - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "requires": { - "ms": "2.0.0" - } - } - } - }, - "socket.io-adapter": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-1.1.1.tgz", - "integrity": "sha1-KoBeihTWNyEk3ZFZrUUC+MsH8Gs=" - }, - "socket.io-client": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-2.1.1.tgz", - "integrity": "sha512-jxnFyhAuFxYfjqIgduQlhzqTcOEQSn+OHKVfAxWaNWa7ecP7xSNk2Dx/3UEsDcY7NcFafxvNvKPmmO7HTwTxGQ==", - "requires": { - "backo2": "1.0.2", - "base64-arraybuffer": "0.1.5", - "component-bind": "1.0.0", - "component-emitter": "1.2.1", - "debug": "~3.1.0", - "engine.io-client": "~3.2.0", - "has-binary2": "~1.0.2", - "has-cors": "1.1.0", - "indexof": "0.0.1", - "object-component": "0.0.3", - "parseqs": "0.0.5", - "parseuri": "0.0.5", - "socket.io-parser": "~3.2.0", - "to-array": "0.1.4" - }, "dependencies": { "component-emitter": { "version": "1.2.1", @@ -10762,9 +10693,53 @@ "requires": { "ms": "2.0.0" } + }, + "engine.io-client": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-3.2.1.tgz", + "integrity": "sha512-y5AbkytWeM4jQr7m/koQLc5AxpRKC1hEVUb/s1FUAWEJq5AzJJ4NLvzuKPuxtDi5Mq755WuDvZ6Iv2rXj4PTzw==", + "requires": { + "component-emitter": "1.2.1", + "component-inherit": "0.0.3", + "debug": "~3.1.0", + "engine.io-parser": "~2.1.1", + "has-cors": "1.1.0", + "indexof": "0.0.1", + "parseqs": "0.0.5", + "parseuri": "0.0.5", + "ws": "~3.3.1", + "xmlhttprequest-ssl": "~1.5.4", + "yeast": "0.1.2" + } + }, + "socket.io-client": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-2.1.1.tgz", + "integrity": "sha512-jxnFyhAuFxYfjqIgduQlhzqTcOEQSn+OHKVfAxWaNWa7ecP7xSNk2Dx/3UEsDcY7NcFafxvNvKPmmO7HTwTxGQ==", + "requires": { + "backo2": "1.0.2", + "base64-arraybuffer": "0.1.5", + "component-bind": "1.0.0", + "component-emitter": "1.2.1", + "debug": "~3.1.0", + "engine.io-client": "~3.2.0", + "has-binary2": "~1.0.2", + "has-cors": "1.1.0", + "indexof": "0.0.1", + "object-component": "0.0.3", + "parseqs": "0.0.5", + "parseuri": "0.0.5", + "socket.io-parser": "~3.2.0", + "to-array": "0.1.4" + } } } }, + "socket.io-adapter": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-1.1.1.tgz", + "integrity": "sha1-KoBeihTWNyEk3ZFZrUUC+MsH8Gs=" + }, "socket.io-parser": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.2.0.tgz", @@ -10795,6 +10770,11 @@ } } }, + "sockette": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/sockette/-/sockette-2.0.6.tgz", + "integrity": "sha512-W6iG8RGV6Zife3Cj+FhuyHV447E6fqFM2hKmnaQrTvg3OydINV3Msj3WPFbX76blUlUxvQSMMMdrJxce8NqI5Q==" + }, "sort-keys": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz", diff --git a/frontend/package.json b/frontend/package.json index 321d5bcef..876456a13 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -86,6 +86,7 @@ "resolve-url-loader": "^3.1.1", "sass-loader": "^7.3.1", "sinon": "^7.5.0", + "sockette": "^2.0.6", "style-loader": "^0.23.1", "sugarss": "^2.0.0", "svg-url-loader": "^2.3.3", diff --git a/frontend/src/app.js b/frontend/src/app.js index 785e83a7a..27fbc3209 100644 --- a/frontend/src/app.js +++ b/frontend/src/app.js @@ -4,6 +4,7 @@ import Router from "vue-router"; import PhotoPrism from "photoprism.vue"; import Routes from "routes"; import Api from "common/api"; +import Socket from "common/websocket"; import Config from "common/config"; import Clipboard from "common/clipboard"; import Components from "component/components"; @@ -33,6 +34,7 @@ Vue.prototype.$alert = Alert; Vue.prototype.$viewer = viewer; Vue.prototype.$session = Session; Vue.prototype.$api = Api; +Vue.prototype.$socket = Socket; Vue.prototype.$config = config; Vue.prototype.$clipboard = clipboard; diff --git a/frontend/src/common/alert.js b/frontend/src/common/alert.js index 8e120f3f3..b84a8475c 100644 --- a/frontend/src/common/alert.js +++ b/frontend/src/common/alert.js @@ -2,17 +2,17 @@ import Event from "pubsub-js"; const Alert = { info: function (message) { - Event.publish("alert.info", message); + Event.publish("alert.info", {msg: message}); }, warning: function (message) { - Event.publish("alert.warning", message); + Event.publish("alert.warning", {msg: message}); }, error: function (message) { - Event.publish("alert.error", message); + Event.publish("alert.error", {msg: message}); }, success: function (message) { - Event.publish("alert.success", message); + Event.publish("alert.success", {msg: message}); }, }; -export default Alert; \ No newline at end of file +export default Alert; diff --git a/frontend/src/common/api.js b/frontend/src/common/api.js index 5b07fafc6..09e29c057 100644 --- a/frontend/src/common/api.js +++ b/frontend/src/common/api.js @@ -1,6 +1,7 @@ +import "@babel/polyfill/noConflict"; import axios from "axios"; import Event from "pubsub-js"; -import "@babel/polyfill/noConflict"; +import Alert from "common/alert"; const Api = axios.create({ baseURL: "/api/v1", @@ -36,7 +37,7 @@ Api.interceptors.response.use(function (response) { } Event.publish("ajax.end"); - Event.publish("alert.error", errorMessage); + Alert.error(errorMessage); return Promise.reject(error); }); diff --git a/frontend/src/common/websocket.js b/frontend/src/common/websocket.js new file mode 100644 index 000000000..a51f7f8a7 --- /dev/null +++ b/frontend/src/common/websocket.js @@ -0,0 +1,22 @@ +import Sockette from "sockette"; +import Event from "pubsub-js"; + +const host = window.location.host; +const Socket = new Sockette("ws://" + host + "/api/v1/ws", { + timeout: 5e3, + onopen: e => { + console.log('Connected!', e); + Socket.send("hello world"); + }, + onmessage: e => { + const m = JSON.parse(e.data); + console.log('Received:', m); + Event.publish(m.event, m.data); + }, + onreconnect: e => console.log('Reconnecting...', e), + onmaximum: e => console.log('Stop Attempting!', e), + onclose: e => console.log('Closed!', e), + onerror: e => console.log('Error:', e) +}); + +export default Socket; diff --git a/frontend/src/component/p-alert.vue b/frontend/src/component/p-alert.vue index 0cc90a7db..a76af8955 100644 --- a/frontend/src/component/p-alert.vue +++ b/frontend/src/component/p-alert.vue @@ -43,24 +43,30 @@ Event.unsubscribe(this.subscriptionId); }, methods: { - handleAlertEvent: function (ev, message) { + handleAlertEvent: function (ev, data) { const type = ev.split('.')[1]; + // get message from data object + let m = data.msg; + + // first letter uppercase + m = m.replace(/^./, m[0].toUpperCase()); + switch (type) { case 'warning': - this.addWarningMessage(message); + this.addWarningMessage(m); break; case 'error': - this.addErrorMessage(message); + this.addErrorMessage(m); break; case 'success': - this.addSuccessMessage(message); + this.addSuccessMessage(m); break; case 'info': - this.addInfoMessage(message); + this.addInfoMessage(m); break; default: - alert(message); + alert(m); } }, diff --git a/frontend/src/component/p-photo-clipboard.vue b/frontend/src/component/p-photo-clipboard.vue index 53d1c824f..4c12245ac 100644 --- a/frontend/src/component/p-photo-clipboard.vue +++ b/frontend/src/component/p-photo-clipboard.vue @@ -131,7 +131,7 @@ Api.post("batch/photos/private", {"ids": this.selection}).then(function () { Event.publish("ajax.end"); - Event.publish("alert.success", "Toggled private flag"); + this.$alert.success("Toggled private flag"); ctx.clearClipboard(); ctx.refresh(); }).catch(() => { @@ -145,7 +145,7 @@ Api.post("batch/photos/story", {"ids": this.selection}).then(function () { Event.publish("ajax.end"); - Event.publish("alert.success", "Toggled story flag"); + this.$alert.success("Toggled story flag"); ctx.clearClipboard(); ctx.refresh(); }).catch(() => { @@ -161,7 +161,7 @@ Api.post("batch/photos/delete", {"ids": this.selection}).then(function () { Event.publish("ajax.end"); - Event.publish("alert.success", "Photos deleted"); + this.$alert.success("Photos deleted"); ctx.clearClipboard(); ctx.refresh(); }).catch(() => { diff --git a/frontend/src/pages/library/import.vue b/frontend/src/pages/library/import.vue index 062fff960..04d296341 100644 --- a/frontend/src/pages/library/import.vue +++ b/frontend/src/pages/library/import.vue @@ -3,7 +3,8 @@

- Importing files from directory... + Indexed {{ fileName}}... + Importing files from directory... Done. Press button to import photos from directory...

@@ -26,7 +27,6 @@ diff --git a/frontend/src/pages/library/index.vue b/frontend/src/pages/library/index.vue index 71129e79c..171706e94 100644 --- a/frontend/src/pages/library/index.vue +++ b/frontend/src/pages/library/index.vue @@ -3,7 +3,8 @@

- Re-indexing existing files and photos... + Indexed {{ fileName}}... + Re-indexing existing files and photos... Done. Press button to re-index existing files and photos...

@@ -26,7 +27,6 @@ diff --git a/frontend/src/pages/library/upload.vue b/frontend/src/pages/library/upload.vue index 3ad3e05c5..fed9a90f9 100644 --- a/frontend/src/pages/library/upload.vue +++ b/frontend/src/pages/library/upload.vue @@ -29,7 +29,6 @@