diff --git a/packages/ui-default/build/config/webpack.ts b/packages/ui-default/build/config/webpack.ts
index 6edf19584..5078069da 100644
--- a/packages/ui-default/build/config/webpack.ts
+++ b/packages/ui-default/build/config/webpack.ts
@@ -146,10 +146,21 @@ export default function (env: { production?: boolean, measure?: boolean } = {})
},
{
test: /\.[mc]?[jt]sx?$/,
- exclude: /@types\//,
+ exclude: [/@types\//, /components\/message\//],
type: 'javascript/auto',
use: [esbuildLoader()],
},
+ {
+ test: /\.[mc]?[jt]sx?$/,
+ include: /components\/message\//,
+ type: 'javascript/auto',
+ use: [{
+ loader: 'ts-loader',
+ options: {
+ transpileOnly: true,
+ },
+ }],
+ },
{
test: /\.styl$/,
use: [extractCssLoader(), cssLoader(), postcssLoader(), stylusLoader()],
diff --git a/packages/ui-default/components/message/index.page.ts b/packages/ui-default/components/message/index.page.ts
index 146f65aaa..c18e57d0f 100644
--- a/packages/ui-default/components/message/index.page.ts
+++ b/packages/ui-default/components/message/index.page.ts
@@ -9,9 +9,10 @@ import tpl from 'vj/utils/tpl';
const onmessage = (msg) => {
console.log('Received message', msg);
+ if (document.hidden) return false;
if (msg.mdoc.flag & FLAG_ALERT) {
// Is alert
- return new InfoDialog({
+ new InfoDialog({
cancelByClickingBack: false,
$body: tpl`
@@ -19,9 +20,10 @@ const onmessage = (msg) => {
${i18n(msg.mdoc.content)}
`,
}).open();
+ return true;
}
// Is message
- return new VjNotification({
+ new VjNotification({
...(msg.udoc._id === 1 && msg.mdoc.flag & 4)
? { message: i18n('You received a system message, click here to view.') }
: {
@@ -32,6 +34,26 @@ const onmessage = (msg) => {
duration: 15000,
action: () => window.open(`/home/messages?uid=${msg.udoc._id}`, '_blank'),
}).show();
+ return true;
+};
+
+const url = new URL('/home/messages-conn', window.location.href);
+// TODO handle a better way for cookie
+url.searchParams.append('sid', document.cookie);
+const endpoint = url.toString().replace('http', 'ws');
+
+const initWorkerMode = () => {
+ console.log('Messages: using SharedWorker');
+ const worker = new SharedWorker(new URL('./worker', import.meta.url), { name: 'Hydro Messages Worker' });
+ worker.port.start();
+ worker.port.postMessage({ type: 'conn', path: endpoint, cookie: document.cookie });
+ worker.port.onmessage = async (message) => {
+ if (process.env.NODE_ENV !== 'production') console.log('onmessage: ', message);
+ const { payload, type } = message.data;
+ if (type === 'message') {
+ if (onmessage(payload)) worker.port.postMessage({ type: 'ack', id: payload.mdoc._id });
+ }
+ };
};
const messagePage = new AutoloadPage('messagePage', (pagename) => {
@@ -44,11 +66,16 @@ const messagePage = new AutoloadPage('messagePage', (pagename) => {
action: () => window.open('/home/messages', '_blank'),
}).show();
}
+ if (window.SharedWorker) {
+ initWorkerMode();
+ return;
+ }
if (!window.BroadcastChannel) {
console.error('BoardcastChannel not supported');
return;
}
+ console.log('Messages: using BroadcastChannel');
let isMaster = false;
const selfId = nanoid();
const channel = new BroadcastChannel('hydro-messages');
@@ -70,45 +97,19 @@ const messagePage = new AutoloadPage('messagePage', (pagename) => {
isMaster = true;
localStorage.setItem('page.master', selfId);
const masterChannel = new BroadcastChannel('hydro-messages');
- const url = new URL('/home/messages-conn', window.location.href.replace('http', 'ws'));
- // TODO handle a better way for cookie
- url.searchParams.append('sid', document.cookie);
const sock = new ReconnectingWebsocket(url.toString());
- 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);
- const id = nanoid();
- masterChannel.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') {
- const notification = new window.Notification(
- payload.udoc.uname || 'Hydro Notification',
- {
- icon: payload.udoc.avatarUrl || '/android-chrome-192x192.png',
- body: payload.mdoc.content,
- },
- );
- notification.onclick = () => window.open('/home/messages');
- }
- };
- masterChannel.onmessage = (msg) => {
- if (msg.data.type === 'message-push') pending[msg.data.id]?.(true);
+ masterChannel.postMessage({ type: 'message', payload });
};
}
channel.onmessage = (msg) => {
if (msg.data.type === 'message' && !document.hidden) {
- channel.postMessage({ type: 'message-push', id: msg.data.id });
onmessage(msg.data.payload);
}
if (msg.data.type === 'master') {
diff --git a/packages/ui-default/components/message/worker.ts b/packages/ui-default/components/message/worker.ts
new file mode 100644
index 000000000..38cae6cc4
--- /dev/null
+++ b/packages/ui-default/components/message/worker.ts
@@ -0,0 +1,80 @@
+/* eslint-disable no-restricted-globals */
+///
+import ReconnectingWebsocket from 'reconnecting-websocket';
+
+console.log('SharedWorker init');
+
+let conn: ReconnectingWebsocket;
+let lcookie: string;
+const ports: MessagePort[] = [];
+interface RequestInitSharedConnPayload {
+ type: 'conn';
+ cookie: string;
+ path: string;
+}
+interface RequestAckPayload {
+ type: 'ack';
+ id: string;
+}
+type RequestPayload = RequestInitSharedConnPayload | RequestAckPayload;
+const ack = {};
+
+function broadcastMsg(message: any) {
+ for (const p of ports) p.postMessage(message);
+}
+function initConn(path: string, port: MessagePort, cookie: any) {
+ if (cookie !== lcookie) conn?.close();
+ else if (conn && conn.readyState === conn.OPEN) return;
+ lcookie = cookie;
+ console.log('Init connection for', path);
+ conn = new ReconnectingWebsocket(path);
+ ports.push(port);
+ conn.onopen = () => conn.send(cookie);
+ conn.onerror = () => broadcastMsg({ type: 'error' });
+ conn.onclose = (ev) => broadcastMsg({ type: 'close', error: ev.reason });
+ conn.onmessage = (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)) {
+ broadcastMsg({ type: 'close', error: payload.error });
+ conn.close();
+ } else {
+ console.log('Connected to', path);
+ broadcastMsg({ type: 'open' });
+ }
+ } else {
+ broadcastMsg({ type: 'message', payload });
+ let acked = false;
+ ack[payload.mdoc.id] = () => { acked = true; };
+ setTimeout(() => {
+ delete ack[payload.mdoc.id];
+ if (acked) return;
+ if (Notification?.permission !== 'granted') {
+ console.log('Notification permission denied');
+ return;
+ }
+ const notification = new Notification(
+ payload.udoc.uname || 'Hydro Notification',
+ {
+ icon: payload.udoc.avatarUrl || '/android-chrome-192x192.png',
+ body: payload.mdoc.content,
+ },
+ );
+ notification.onclick = () => window.open('/home/messages');
+ }, 5000);
+ }
+ };
+}
+
+// eslint-disable-next-line no-undef
+onconnect = function (e) {
+ const port = e.ports[0];
+
+ port.addEventListener('message', (msg: { data: RequestPayload }) => {
+ if (msg.data.type === 'conn') initConn(msg.data.path, port, msg.data.cookie);
+ if (msg.data.type === 'ack') ack[msg.data.id]();
+ });
+
+ port.start();
+};
diff --git a/packages/ui-default/package.json b/packages/ui-default/package.json
index c1d715c05..c5a04f4e4 100644
--- a/packages/ui-default/package.json
+++ b/packages/ui-default/package.json
@@ -1,6 +1,6 @@
{
"name": "@hydrooj/ui-default",
- "version": "4.39.19",
+ "version": "4.39.20",
"author": "undefined ",
"license": "AGPL-3.0",
"main": "hydro.js",
@@ -31,6 +31,7 @@
"@types/qrcode": "^1.5.0",
"@types/react-dom": "^18.0.6",
"@types/redux-logger": "^3.0.9",
+ "@types/sharedworker": "^0.0.80",
"@vscode/codicons": "^0.0.32",
"autoprefixer": "^10.4.8",
"browser-update": "^3.3.40",
@@ -104,6 +105,7 @@
"through2": "^4.0.2",
"timeago-react": "^3.0.5",
"timeago.js": "^4.0.2",
+ "ts-loader": "^9.3.1",
"vditor": "^3.8.17",
"vinyl-buffer": "^1.0.1",
"web-streams-polyfill": "^3.2.1",