diff --git a/packages/ui-default/build/config/webpack.js b/packages/ui-default/build/config/webpack.js index 6bd105436..d9668ddc0 100644 --- a/packages/ui-default/build/config/webpack.js +++ b/packages/ui-default/build/config/webpack.js @@ -206,7 +206,6 @@ export default function (env = {}) { { from: root('static') }, { from: root(`${dirname(require.resolve('vditor/package.json'))}/dist`), to: 'vditor/dist' }, { from: `${dirname(require.resolve('monaco-themes/package.json'))}/themes`, to: 'monaco/themes/' }, - { from: root('.build/sharedworker.js'), to: 'sharedworker.js' }, ], }), new webpack.DefinePlugin({ diff --git a/packages/ui-default/build/main.ts b/packages/ui-default/build/main.ts index e97125b83..dec6bab96 100644 --- a/packages/ui-default/build/main.ts +++ b/packages/ui-default/build/main.ts @@ -3,7 +3,6 @@ import cac from 'cac'; import fs from 'fs-extra'; import webpack from 'webpack'; -import esbuild from 'esbuild'; import WebpackDevServer from 'webpack-dev-server'; import gulp from 'gulp'; import log from 'fancy-log'; @@ -18,15 +17,6 @@ const argv = cac().parse(); async function runWebpack({ watch, production, measure, dev, }) { - const result = esbuild.build({ - bundle: true, - format: 'iife', - entryPoints: [root('sharedworker.ts')], - outfile: root('.build/sharedworker.js'), - watch: dev || watch, - minify: production, - }); - if (!dev && !watch) await result; const compiler = webpack(webpackConfig({ watch, production, measure })); if (dev) { const server = new WebpackDevServer(compiler, { @@ -36,7 +26,7 @@ async function runWebpack({ stats: 'none', index: root('public'), proxy: { - context: (path) => !path.includes('sharedworker.js'), + context: (path) => !path.includes('sockjs-node'), target: 'http://localhost:2333', ws: true, }, diff --git a/packages/ui-default/components/message/index.page.js b/packages/ui-default/components/message/index.page.js deleted file mode 100644 index a396f23a5..000000000 --- a/packages/ui-default/components/message/index.page.js +++ /dev/null @@ -1,67 +0,0 @@ -import { AutoloadPage } from 'vj/misc/Page'; -import { InfoDialog } from 'vj/components/dialog'; -import VjNotification from 'vj/components/notification/index'; -import { FLAG_ALERT } from 'vj/constant/message'; -import i18n from 'vj/utils/i18n'; -import tpl from 'vj/utils/tpl'; - -const messagePage = new AutoloadPage('messagePage', (pagename) => { - if (pagename === 'home_messages' || !UserContext._id) return; - - if (UserContext.unreadMsg) { - new VjNotification({ - type: 'info', - message: i18n(`You have ${UserContext.unreadMsg > 1 ? 'new messages' : 'a new message'}.`), - duration: 5000, - action: () => window.open('/home/messages', '_blank'), - }).show(); - } - - const onmessage = (message) => { - const msg = JSON.parse(message); - console.log('Received message', msg); - if (msg.mdoc.flag & FLAG_ALERT) { - // Is alert - return new InfoDialog({ - cancelByClickingBack: false, - $body: tpl` -
- ${{ templateRaw: true, html: i18n('Message from {0}:', `
${msg.mdoc.from}
`) }} -

${i18n(msg.mdoc.content)}

-
`, - }).open(); - } - if (msg.udoc._id === 1 && msg.mdoc.flag & 4) { - // System notification - return new VjNotification({ - message: i18n('You received a system message, click here to view.'), - duration: 15000, - action: () => window.open(`/home/messages?uid=${msg.udoc._id}`, '_blank'), - }).show(); - } - // Is message - return new VjNotification({ - title: msg.udoc.uname, - avatar: msg.udoc.avatarUrl, - message: msg.mdoc.content, - duration: 15000, - action: () => window.open(`/home/messages?uid=${msg.udoc._id}`, '_blank'), - }).show(); - }; - - const path = new URL('/home/messages-conn', window.location.href).toString(); - const worker = new SharedWorker('/sharedworker.js', { name: 'Hydro Messages Shared Worker' }); - worker.port.start(); - worker.port.postMessage({ type: 'init', path, cookie: document.cookie }); - worker.port.onmessage = (e) => { - if (e.data.type !== 'message') return; - if (document.hidden) { - worker.port.postMessage({ type: 'fail' }); - } else { - worker.port.postMessage({ type: 'success' }); - onmessage(e.data.payload); - } - }; -}); - -export default messagePage; diff --git a/packages/ui-default/components/message/index.page.ts b/packages/ui-default/components/message/index.page.ts new file mode 100644 index 000000000..088646162 --- /dev/null +++ b/packages/ui-default/components/message/index.page.ts @@ -0,0 +1,129 @@ +import { AutoloadPage } from 'vj/misc/Page'; +import { InfoDialog } from 'vj/components/dialog'; +import VjNotification from 'vj/components/notification/index'; +import ReconnectingWebsocket from 'reconnecting-websocket'; +import { FLAG_ALERT } from 'vj/constant/message'; +import i18n from 'vj/utils/i18n'; +import tpl from 'vj/utils/tpl'; +import { nanoid } from 'nanoid'; +import sleep from 'vj/utils/delay'; + +function asMaster() { + console.log('init as master'); + const channel = new BroadcastChannel('hydro-messages'); + const interval = setInterval(() => channel.postMessage({ type: 'master' }), 5000); + const url = new URL('/home/messages-conn', window.location.href.replace('http', 'ws')); + url.searchParams.append('sid', document.cookie); + const sock = new ReconnectingWebsocket(url.toString()); + function destory() { + sock.close(); + channel.close(); + clearInterval(interval); + } + const pending = {}; + sock.onopen = () => console.log('Connected'); + sock.onerror = console.error; + sock.onclose = (...args) => console.log('Closed', ...args); + sock.onmessage = async (message) => { + if (process.env.NODE_ENV !== 'production') console.log('onmessage: ', message); + const payload = JSON.parse(message.data); + if (['PermissionError', 'PrivilegeError'].includes(payload.error)) { + destory(); + return; + } + const id = nanoid(); + channel.postMessage({ type: 'message', id, payload }); + const success = await new Promise((resolve) => { + pending[id] = resolve; + setTimeout(() => { + delete pending[id]; + resolve(false); + }, 1000); + }); + if (!success && window.Notification?.permission === 'granted') { + // eslint-disable-next-line no-new + new window.Notification( + payload.udoc.uname || 'Hydro Notification', + { + icon: payload.udoc.avatarUrl || '/android-chrome-192x192.png', + body: payload.mdoc.content, + }, + ); + } + }; + channel.onmessage = (msg) => { + if (msg.data.type === 'message-push') pending[msg.data.id]?.(true); + }; +} + +const onmessage = (msg) => { + console.log('Received message', msg); + if (msg.mdoc.flag & FLAG_ALERT) { + // Is alert + return new InfoDialog({ + cancelByClickingBack: false, + $body: tpl` +
+ ${{ templateRaw: true, html: i18n('Message from {0}:', `
${msg.mdoc.from}
`) }} +

${i18n(msg.mdoc.content)}

+
`, + }).open(); + } + // Is message + return new VjNotification({ + ...(msg.udoc._id === 1 && msg.mdoc.flag & 4) + ? { message: i18n('You received a system message, click here to view.') } + : { + title: msg.udoc.uname, + avatar: msg.udoc.avatarUrl, + message: msg.mdoc.content, + }, + duration: 15000, + action: () => window.open(`/home/messages?uid=${msg.udoc._id}`, '_blank'), + }).show(); +}; + +async function asSlave() { + console.log('message load as slave'); + const channel = new BroadcastChannel('hydro-messages'); + const selfId = nanoid(); + setInterval(() => channel.postMessage({ type: 'node', id: selfId }), 5000); + let alive = false; + let nodes = new Set([selfId]); + channel.onmessage = (msg) => { + if (msg.data.target && msg.data.target !== selfId) return; + if (msg.data.type === 'master') alive = true; + if (msg.data.type === 'node') nodes.add(msg.data.id); + if (msg.data.type === 'message') { + if (!document.hidden) { + channel.postMessage({ type: 'message-push', id: msg.data.id }); + onmessage(msg.data.payload); + } + } + }; + while (true) { // eslint-disable-line no-constant-condition + alive = false; + nodes = new Set([selfId]); + await sleep(7000); + if (!alive && Array.from(nodes).sort()[0] === selfId) asMaster(); + } +} + +const messagePage = new AutoloadPage('messagePage', (pagename) => { + if (pagename === 'home_messages' || !UserContext._id) return; + if (UserContext.unreadMsg) { + new VjNotification({ + type: 'info', + message: i18n(`You have ${UserContext.unreadMsg > 1 ? 'new messages' : 'a new message'}.`), + duration: 5000, + action: () => window.open('/home/messages', '_blank'), + }).show(); + } + if (!window.BroadcastChannel) { + console.error('BoardcastChannel not supported'); + return; + } + asSlave(); +}); + +export default messagePage; diff --git a/packages/ui-default/components/socket/index.js b/packages/ui-default/components/socket/index.js index 0435d062b..52208e113 100644 --- a/packages/ui-default/components/socket/index.js +++ b/packages/ui-default/components/socket/index.js @@ -19,11 +19,8 @@ export default class Sock { this.sock.onmessage = (message) => { if (process.env.NODE_ENV !== 'production') console.log('Sock.onmessage: ', message); const msg = JSON.parse(message.data); - if (msg.event === 'auth') { - if (msg.error === 'PermissionError' || msg.error === 'PrivilegeError') { - this.close(); - } - } else this.onmessage?.(message); + if (msg.error === 'PermissionError' || msg.error === 'PrivilegeError') this.close(); + else this.onmessage?.(message); }; } diff --git a/packages/ui-default/package.json b/packages/ui-default/package.json index 70504d0e6..a879f8f0a 100644 --- a/packages/ui-default/package.json +++ b/packages/ui-default/package.json @@ -1,6 +1,6 @@ { "name": "@hydrooj/ui-default", - "version": "4.35.0", + "version": "4.35.1", "author": "undefined ", "license": "AGPL-3.0", "main": "hydro.js", diff --git a/packages/ui-default/sharedworker.ts b/packages/ui-default/sharedworker.ts deleted file mode 100644 index 798ed7045..000000000 --- a/packages/ui-default/sharedworker.ts +++ /dev/null @@ -1,54 +0,0 @@ -import ReconnectingWebsocket from 'reconnecting-websocket'; - -console.log('SharedWorker init'); -const ports: MessagePort[] = []; -let conn: ReconnectingWebsocket; - -function initSharedConn(path: string, sid: string) { - if (conn) return; - console.log('Init connection for', path); - const url = new URL(path); - url.protocol = url.protocol.replace('http', 'ws'); - url.searchParams.append('sid', sid); - const sock = new ReconnectingWebsocket(url.toString()); - conn = sock; - sock.onopen = () => console.log('Connected'); - sock.onerror = console.error; - sock.onclose = (...args) => console.log('Closed', ...args); - sock.onmessage = async (message) => { - if (process.env.NODE_ENV !== 'production') console.log('SharedWorker.port.onmessage: ', message); - const payload = JSON.parse(message.data); - if (payload.event === 'auth') { - if (['PermissionError', 'PrivilegeError'].includes(payload.error)) { - conn?.close(); - conn = null; - } - return; - } - for (const port of ports) { - port.postMessage({ type: 'message', payload: message.data }); - const success = await new Promise((resolve) => { - const handle = (msg) => { - if (['success', 'fail'].includes(msg.data.type)) { - port.removeEventListener('message', handle); - resolve(msg.data.type === 'success'); - } - }; - port.addEventListener('message', handle); - }); - if (success) return; - } - console.log('Failed to push notification'); - // TODO try system notification - }; -} - -// @ts-ignore -onconnect = function (e) { // eslint-disable-line no-undef - const port: MessagePort = e.ports[0]; - port.addEventListener('message', (msg) => { - initSharedConn(msg.data.path, msg.data.cookie); - }); - port.start(); - ports.push(port); -};