diff --git a/admin/package.json b/admin/package.json
index 05ee422a4..29a5906c3 100644
--- a/admin/package.json
+++ b/admin/package.json
@@ -9,19 +9,12 @@
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview"
},
- "dependencies": {},
+ "dependencies": {
+ "@radix-ui/react-switch": "^1.0.3"
+ },
"devDependencies": {
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-toast": "^1.1.5",
- "i18next": "^23.11.2",
- "i18next-browser-languagedetector": "^7.2.1",
- "lucide-react": "^0.372.0",
- "react": "^18.2.0",
- "react-dom": "^18.2.0",
- "react-hook-form": "^7.51.3",
- "react-i18next": "^14.1.0",
- "react-router-dom": "^6.22.3",
- "zustand": "^4.5.2",
"@types/react": "^18.2.79",
"@types/react-dom": "^18.2.25",
"@typescript-eslint/eslint-plugin": "^7.7.0",
@@ -30,10 +23,19 @@
"eslint": "^9.0.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.5",
+ "i18next": "^23.11.2",
+ "i18next-browser-languagedetector": "^7.2.1",
+ "lucide-react": "^0.372.0",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "react-hook-form": "^7.51.3",
+ "react-i18next": "^14.1.0",
+ "react-router-dom": "^6.22.3",
"socket.io-client": "^4.7.5",
"typescript": "^5.4.5",
"vite": "^5.2.9",
"vite-plugin-static-copy": "^1.0.3",
- "vite-plugin-svgr": "^4.2.0"
+ "vite-plugin-svgr": "^4.2.0",
+ "zustand": "^4.5.2"
}
}
diff --git a/admin/src/App.tsx b/admin/src/App.tsx
index a1a1e4377..b3238ef9a 100644
--- a/admin/src/App.tsx
+++ b/admin/src/App.tsx
@@ -6,7 +6,7 @@ import {NavLink, Outlet, useNavigate} from "react-router-dom";
import {useStore} from "./store/store.ts";
import {LoadingScreen} from "./utils/LoadingScreen.tsx";
import {Trans, useTranslation} from "react-i18next";
-import {Cable, Construction, Crown, NotepadText, Wrench} from "lucide-react";
+import {Cable, Construction, Crown, NotepadText, Wrench, PhoneCall} from "lucide-react";
const WS_URL = import.meta.env.DEV? 'http://localhost:9001' : ''
export const App = ()=> {
@@ -96,8 +96,10 @@ export const App = ()=> {
diff --git a/admin/src/components/ShoutType.ts b/admin/src/components/ShoutType.ts
new file mode 100644
index 000000000..f7e8b1df3
--- /dev/null
+++ b/admin/src/components/ShoutType.ts
@@ -0,0 +1,13 @@
+export type ShoutType = {
+ type: string,
+ data:{
+ type: string,
+ payload: {
+ message: {
+ message: string,
+ sticky: boolean
+ },
+ timestamp: number
+ }
+ }
+}
diff --git a/admin/src/index.css b/admin/src/index.css
index 591f584d5..a7330448e 100644
--- a/admin/src/index.css
+++ b/admin/src/index.css
@@ -604,6 +604,25 @@ pre {
outline: none;
}
+
+.send-message {
+ position: relative;
+}
+
+.send-message input {
+ width: auto;
+}
+
+.send-message {
+}
+
+.send-message svg {
+ position: absolute;
+ right: 3px;
+ bottom: -3px;
+ left: auto !important;
+}
+
.search-field svg {
position: absolute;
left: 3px;
@@ -733,3 +752,52 @@ input, button, select, optgroup, textarea {
right: 10px;
color: #666;
}
+
+
+.SwitchRoot {
+ align-self: center;
+ width: 60px;
+ height: 30px;
+ background-color: black;
+ border-radius: 9999px;
+ position: relative;
+ box-shadow: 0 2px 10px var(--black-a7);
+ -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
+}
+.SwitchRoot:focus {
+ box-shadow: 0 0 0 2px black;
+}
+.SwitchRoot[data-state='checked'] {
+ background-color: var(--etherpad-color);
+}
+
+.SwitchThumb {
+ display: block;
+ width: 20px;
+ height: 20px;
+ background-color: white;
+ border-radius: 9999px;
+ box-shadow: 0 2px 2px var(--black-a7);
+ transition: transform 100ms;
+ transform: translateX(2px);
+ will-change: transform;
+}
+.SwitchThumb[data-state='checked'] {
+ transform: translateX(25px);
+}
+
+.Label {
+ color: white;
+ font-size: 15px;
+ line-height: 1;
+}
+
+.message {
+ position: relative;
+ padding: 10px;
+ border: 1px solid #e0e0e0;
+ margin: 10px 20px 10px 10px;
+ border-radius: 10px 0 10px 10px;
+ background-color: var(--etherpad-color);
+ color: white
+}
diff --git a/admin/src/main.tsx b/admin/src/main.tsx
index 03ec73104..5efc26de6 100644
--- a/admin/src/main.tsx
+++ b/admin/src/main.tsx
@@ -12,6 +12,7 @@ import {I18nextProvider} from "react-i18next";
import i18n from "./localization/i18n.ts";
import {PadPage} from "./pages/PadPage.tsx";
import {ToastDialog} from "./utils/Toast.tsx";
+import {ShoutPage} from "./pages/ShoutPage.tsx";
const router = createBrowserRouter(createRoutesFromElements(
<>}>
@@ -20,6 +21,7 @@ const router = createBrowserRouter(createRoutesFromElements(
}/>
}/>
}/>
+ }/>
}/>
>
diff --git a/admin/src/pages/ShoutPage.tsx b/admin/src/pages/ShoutPage.tsx
new file mode 100644
index 000000000..8348e18fa
--- /dev/null
+++ b/admin/src/pages/ShoutPage.tsx
@@ -0,0 +1,76 @@
+import {useEffect, useState} from "react";
+import {SendHorizonal} from 'lucide-react'
+import {useStore} from "../store/store.ts";
+import * as Switch from '@radix-ui/react-switch';
+import {ShoutType} from "../components/ShoutType.ts";
+
+export const ShoutPage = ()=>{
+ const [totalUsers, setTotalUsers] = useState(0);
+ const [message, setMessage] = useState("");
+ const [sticky, setSticky] = useState(false);
+ const socket = useStore(state => state.settingsSocket);
+ const [shouts, setShouts] = useState([]);
+
+ useEffect(() => {
+ fetch('/stats')
+ .then(response => response.json())
+ .then(data => setTotalUsers(data.totalUsers));
+ }, []);
+
+
+ useEffect(() => {
+ if(socket) {
+ socket.on('shout', (shout) => {
+ setShouts([...shouts, shout])
+ })
+ }
+ }, [socket, shouts])
+
+ const sendMessage = () => {
+ socket?.emit('shout', {
+ message,
+ sticky
+ });
+ setMessage('')
+ }
+
+ return (
+
+
Communication
+ {totalUsers > 0 &&
There {totalUsers>1?"are":"is"} currently {totalUsers} user{totalUsers>1?"s":""} online
}
+
+
+ {
+ shouts.map((shout) => {
+ return (
+
+
{shout.data.payload.message.message}
+
+
+
{new Date(shout.data.payload.timestamp).toLocaleTimeString()
+ + " " + new Date(shout.data.payload.timestamp).toLocaleDateString()}
+
+
+ )
+ })
+ }
+
+
+
+
+ )
+}
diff --git a/admin/vite.config.ts b/admin/vite.config.ts
index d90386ca8..23921ca85 100644
--- a/admin/vite.config.ts
+++ b/admin/vite.config.ts
@@ -28,8 +28,11 @@ export default defineConfig({
'/admin-auth/': {
target: 'http://localhost:9001',
changeOrigin: true,
- rewrite: (path) => path.replace(/^\/admin-prox/, '/admin/')
+ },
+ '/stats': {
+ target: 'http://localhost:9001',
+ changeOrigin: true,
+ }
}
- }
}
})
diff --git a/package.json b/package.json
index f414943a3..39c31ae6b 100644
--- a/package.json
+++ b/package.json
@@ -44,5 +44,6 @@
"url": "https://github.com/ether/etherpad-lite.git"
},
"version": "2.0.2",
- "license": "Apache-2.0"
+ "license": "Apache-2.0",
+ "packageManager": "pnpm@9.0.4+sha256.caa915eaae9d9aefccf50ee8aeda25a2f8684d8f9d5c6e367eaf176d97c1f89e"
}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index aecb1af67..fda48d514 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -23,6 +23,10 @@ importers:
version: link:ui
admin:
+ dependencies:
+ '@radix-ui/react-switch':
+ specifier: ^1.0.3
+ version: 1.0.3(@types/react-dom@18.2.25)(@types/react@18.2.79)(react-dom@18.2.0)(react@18.2.0)
devDependencies:
'@radix-ui/react-dialog':
specifier: ^1.0.5
@@ -683,7 +687,6 @@ packages:
engines: {node: '>=6.9.0'}
dependencies:
regenerator-runtime: 0.14.1
- dev: true
/@babel/template@7.24.0:
resolution: {integrity: sha512-Bkf2q8lMB0AFpX0NFEqSbx1OkTHf0f+0j82mkw+ZpzBnkk7e9Ql0891vlfgi+kHwOk8tQjiQHpqh4LaSa0fKEA==}
@@ -1339,7 +1342,6 @@ packages:
resolution: {integrity: sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw==}
dependencies:
'@babel/runtime': 7.24.0
- dev: true
/@radix-ui/react-collection@1.0.3(@types/react-dom@18.2.25)(@types/react@18.2.79)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-3SzW+0PW7yBBoQlT8wNcGtaxaD0XSu0uLUFgrtHY08Acx05TaHaOmVLR73c0j/cqpDy53KBMO7s0dx2wmOIDIA==}
@@ -1377,7 +1379,6 @@ packages:
'@babel/runtime': 7.24.0
'@types/react': 18.2.79
react: 18.2.0
- dev: true
/@radix-ui/react-context@1.0.1(@types/react@18.2.79)(react@18.2.0):
resolution: {integrity: sha512-ebbrdFoYTcuZ0v4wG5tedGnp9tzcV8awzsxYph7gXUyvnNLuTIcCk1q17JEbnVhXAKG9oX3KtchwiMIAYp9NLg==}
@@ -1391,7 +1392,6 @@ packages:
'@babel/runtime': 7.24.0
'@types/react': 18.2.79
react: 18.2.0
- dev: true
/@radix-ui/react-dialog@1.0.5(@types/react-dom@18.2.25)(@types/react@18.2.79)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-GjWJX/AUpB703eEBanuBnIWdIXg6NvJFCXcNlSZk4xdszCdhrJgBoUd1cGk67vFO+WdA2pfI/plOpqz/5GUP6Q==}
@@ -1566,7 +1566,6 @@ packages:
'@types/react-dom': 18.2.25
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
- dev: true
/@radix-ui/react-slot@1.0.2(@types/react@18.2.79)(react@18.2.0):
resolution: {integrity: sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==}
@@ -1581,7 +1580,33 @@ packages:
'@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.79)(react@18.2.0)
'@types/react': 18.2.79
react: 18.2.0
- dev: true
+
+ /@radix-ui/react-switch@1.0.3(@types/react-dom@18.2.25)(@types/react@18.2.79)(react-dom@18.2.0)(react@18.2.0):
+ resolution: {integrity: sha512-mxm87F88HyHztsI7N+ZUmEoARGkC22YVW5CaC+Byc+HRpuvCrOBPTAnXgf+tZ/7i0Sg/eOePGdMhUKhPaQEqow==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0
+ react-dom: ^16.8 || ^17.0 || ^18.0
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+ dependencies:
+ '@babel/runtime': 7.24.0
+ '@radix-ui/primitive': 1.0.1
+ '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.79)(react@18.2.0)
+ '@radix-ui/react-context': 1.0.1(@types/react@18.2.79)(react@18.2.0)
+ '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.25)(@types/react@18.2.79)(react-dom@18.2.0)(react@18.2.0)
+ '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.79)(react@18.2.0)
+ '@radix-ui/react-use-previous': 1.0.1(@types/react@18.2.79)(react@18.2.0)
+ '@radix-ui/react-use-size': 1.0.1(@types/react@18.2.79)(react@18.2.0)
+ '@types/react': 18.2.79
+ '@types/react-dom': 18.2.25
+ react: 18.2.0
+ react-dom: 18.2.0(react@18.2.0)
+ dev: false
/@radix-ui/react-toast@1.1.5(@types/react-dom@18.2.25)(@types/react@18.2.79)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-fRLn227WHIBRSzuRzGJ8W+5YALxofH23y0MlPLddaIpLpCDqdE0NZlS2NRQDRiptfxDeeCjgFIpexB1/zkxDlw==}
@@ -1627,7 +1652,6 @@ packages:
'@babel/runtime': 7.24.0
'@types/react': 18.2.79
react: 18.2.0
- dev: true
/@radix-ui/react-use-controllable-state@1.0.1(@types/react@18.2.79)(react@18.2.0):
resolution: {integrity: sha512-Svl5GY5FQeN758fWKrjM6Qb7asvXeiZltlT4U2gVfl8Gx5UAv2sMR0LWo8yhsIZh2oQ0eFdZ59aoOOMV7b47VA==}
@@ -1642,7 +1666,6 @@ packages:
'@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.79)(react@18.2.0)
'@types/react': 18.2.79
react: 18.2.0
- dev: true
/@radix-ui/react-use-escape-keydown@1.0.3(@types/react@18.2.79)(react@18.2.0):
resolution: {integrity: sha512-vyL82j40hcFicA+M4Ex7hVkB9vHgSse1ZWomAqV2Je3RleKGO5iM8KMOEtfoSB0PnIelMd2lATjTGMYqN5ylTg==}
@@ -1671,7 +1694,35 @@ packages:
'@babel/runtime': 7.24.0
'@types/react': 18.2.79
react: 18.2.0
- dev: true
+
+ /@radix-ui/react-use-previous@1.0.1(@types/react@18.2.79)(react@18.2.0):
+ resolution: {integrity: sha512-cV5La9DPwiQ7S0gf/0qiD6YgNqM5Fk97Kdrlc5yBcrF3jyEZQwm7vYFqMo4IfeHgJXsRaMvLABFtd0OVEmZhDw==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ dependencies:
+ '@babel/runtime': 7.24.0
+ '@types/react': 18.2.79
+ react: 18.2.0
+ dev: false
+
+ /@radix-ui/react-use-size@1.0.1(@types/react@18.2.79)(react@18.2.0):
+ resolution: {integrity: sha512-ibay+VqrgcaI6veAojjofPATwledXiSmX+C0KrBk/xgpX9rBzPV3OsfwlhQdUOFbh+LKQorLYT+xTXW9V8yd0g==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ dependencies:
+ '@babel/runtime': 7.24.0
+ '@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.2.79)(react@18.2.0)
+ '@types/react': 18.2.79
+ react: 18.2.0
+ dev: false
/@radix-ui/react-visually-hidden@1.0.3(@types/react-dom@18.2.25)(@types/react@18.2.79)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-D4w41yN5YRKtu464TLnByKzMDG/JlMPHtfZgQAu9v6mNakUqGUI9vUrfQKz8NK41VMm/xbZbh76NUTVtIYqOMA==}
@@ -2357,7 +2408,6 @@ packages:
/@types/prop-types@15.7.11:
resolution: {integrity: sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==}
- dev: true
/@types/qs@6.9.11:
resolution: {integrity: sha512-oGk0gmhnEJK4Yyk+oI7EfXsLayXatCWPHary1MtcmbAifkobT9cM9yutG/hZKIseOU0MqbIwQ/u2nn/Gb+ltuQ==}
@@ -2371,14 +2421,12 @@ packages:
resolution: {integrity: sha512-o/V48vf4MQh7juIKZU2QGDfli6p1+OOi5oXx36Hffpc9adsHeXjVp8rHuPkjd8VT8sOJ2Zp05HR7CdpGTIUFUA==}
dependencies:
'@types/react': 18.2.79
- dev: true
/@types/react@18.2.79:
resolution: {integrity: sha512-RwGAGXPl9kSXwdNTafkOEuFrTBD5SA2B3iEB96xi8+xu5ddUa/cpvyVCSNn+asgLCTHkb5ZxN8gbuibYJi4s1w==}
dependencies:
'@types/prop-types': 15.7.11
csstype: 3.1.3
- dev: true
/@types/semver@7.5.8:
resolution: {integrity: sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==}
@@ -3391,7 +3439,6 @@ packages:
/csstype@3.1.3:
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
- dev: true
/data-uri-to-buffer@6.0.2:
resolution: {integrity: sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==}
@@ -5179,7 +5226,6 @@ packages:
/js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
- dev: true
/js-yaml@4.1.0:
resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==}
@@ -5512,7 +5558,6 @@ packages:
hasBin: true
dependencies:
js-tokens: 4.0.0
- dev: true
/lower-case@2.0.2:
resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==}
@@ -6291,7 +6336,6 @@ packages:
loose-envify: 1.4.0
react: 18.2.0
scheduler: 0.23.0
- dev: true
/react-hook-form@7.51.3(react@18.2.0):
resolution: {integrity: sha512-cvJ/wbHdhYx8aviSWh28w9ImjmVsb5Y05n1+FW786vEZQJV5STNM0pW6ujS+oiBecb0ARBxJFyAnXj9+GHXACQ==}
@@ -6402,7 +6446,6 @@ packages:
engines: {node: '>=0.10.0'}
dependencies:
loose-envify: 1.4.0
- dev: true
/readdirp@3.6.0:
resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
@@ -6413,7 +6456,6 @@ packages:
/regenerator-runtime@0.14.1:
resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==}
- dev: true
/regexp.prototype.flags@1.5.2:
resolution: {integrity: sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==}
@@ -6579,7 +6621,6 @@ packages:
resolution: {integrity: sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==}
dependencies:
loose-envify: 1.4.0
- dev: true
/security@1.0.0:
resolution: {integrity: sha512-5qfoAgfRWS1sUn+fUJtdbbqM1BD/LoQGa+smPTDjf9OqHyuJqi6ewtbYL0+V1S1RaU6OCOCMWGZocIfz2YK4uw==}
diff --git a/src/ep.json b/src/ep.json
index 95cb4135e..a96890fb4 100644
--- a/src/ep.json
+++ b/src/ep.json
@@ -112,6 +112,12 @@
"hooks": {
"expressPreSession": "ep_etherpad-lite/node/hooks/express/openapi"
}
+ },
+ {
+ "name": "ep_message_all",
+ "client_hooks": {
+ "handleClientMessage_shoutMessage": "ep_etherpad-lite/static/js/messageHandler"
+ }
}
]
}
diff --git a/src/node/hooks/express/adminplugins.ts b/src/node/hooks/express/adminplugins.ts
index 502799197..5272719f5 100644
--- a/src/node/hooks/express/adminplugins.ts
+++ b/src/node/hooks/express/adminplugins.ts
@@ -87,6 +87,7 @@ exports.socketio = (hookName:string, args:ArgsExpressType, cb:Function) => {
});
});
+
socket.on('uninstall', (pluginName:string) => {
uninstall(pluginName, (err:ErrorCaused) => {
if (err) console.warn(err.stack || err.toString());
diff --git a/src/node/hooks/express/adminsettings.ts b/src/node/hooks/express/adminsettings.ts
index 03258c584..732b64f20 100644
--- a/src/node/hooks/express/adminsettings.ts
+++ b/src/node/hooks/express/adminsettings.ts
@@ -17,195 +17,217 @@ const api = require('../../db/API');
const queryPadLimit = 12;
-exports.socketio = (hookName:string, {io}:any) => {
- io.of('/settings').on('connection', (socket: any ) => {
- // @ts-ignore
- const {session: {user: {is_admin: isAdmin} = {}} = {}} = socket.conn.request;
- if (!isAdmin) return;
+exports.socketio = (hookName: string, {io}: any) => {
+ io.of('/settings').on('connection', (socket: any) => {
+ // @ts-ignore
+ const {session: {user: {is_admin: isAdmin} = {}} = {}} = socket.conn.request;
+ if (!isAdmin) return;
- socket.on('load', async (query:string):Promise => {
- let data;
- try {
- data = await fsp.readFile(settings.settingsFilename, 'utf8');
- } catch (err) {
- return console.log(err);
- }
- // if showSettingsInAdminPage is set to false, then return NOT_ALLOWED in the result
- if (settings.showSettingsInAdminPage === false) {
- socket.emit('settings', {results: 'NOT_ALLOWED'});
- } else {
- socket.emit('settings', {results: data});
- }
- });
-
- socket.on('saveSettings', async (newSettings:string) => {
- console.log('Admin request to save settings through a socket on /admin/settings');
- await fsp.writeFile(settings.settingsFilename, newSettings);
- socket.emit('saveprogress', 'saved');
- });
-
-
- socket.on('help', ()=> {
- const gitCommit = settings.getGitCommit();
- const epVersion = settings.getEpVersion();
-
- const hooks:Map> = plugins.getHooks('hooks', false);
- const clientHooks:Map> = plugins.getHooks('client_hooks', false);
-
- function mapToObject(map: Map) {
- let obj = Object.create(null);
- for (let [k,v] of map) {
- if(v instanceof Map) {
- obj[k] = mapToObject(v);
- } else {
- obj[k] = v;
- }
- }
- return obj;
- }
-
- socket.emit('reply:help', {
- gitCommit,
- epVersion,
- installedPlugins: plugins.getPlugins(),
- installedParts: plugins.getParts(),
- installedServerHooks: mapToObject(hooks),
- installedClientHooks: mapToObject(clientHooks),
- latestVersion: UpdateCheck.getLatestVersion(),
- })
- });
-
-
- socket.on('padLoad', async (query: PadSearchQuery) => {
- const {padIDs} = await padManager.listAllPads();
-
- const data:{
- total: number,
- results?: PadQueryResult[]
- } = {
- total: padIDs.length,
- };
- let result: string[] = padIDs;
- let maxResult;
-
- // Filter out matches
- if (query.pattern) {
- result = result.filter((padName: string) => padName.includes(query.pattern));
- }
-
- data.total = result.length;
-
- maxResult = result.length - 1;
- if (maxResult < 0) {
- maxResult = 0;
- }
-
- if (query.offset && query.offset < 0) {
- query.offset = 0;
- } else if (query.offset > maxResult) {
- query.offset = maxResult;
- }
-
- if (query.limit && query.limit < 0) {
- query.limit = 0;
- } else if (query.limit > queryPadLimit) {
- query.limit = queryPadLimit;
- }
-
- if (query.sortBy === 'padName') {
- result = result.sort((a,b)=>{
- if(a < b) return query.ascending ? -1 : 1;
- if(a > b) return query.ascending ? 1 : -1;
- return 0;
- }).slice(query.offset, query.offset + query.limit);
-
- data.results = await Promise.all(result.map(async (padName: string) => {
- const pad = await padManager.getPad(padName);
- const revisionNumber = pad.getHeadRevisionNumber()
- const userCount = api.padUsersCount(padName).padUsersCount;
- const lastEdited = await pad.getLastEdit();
-
- return {
- padName,
- lastEdited,
- userCount,
- revisionNumber
- }}));
- } else {
- const currentWinners: PadQueryResult[] = []
- let queryOffsetCounter = 0
- for (let res of result) {
-
- const pad = await padManager.getPad(res);
- const padType = {
- padName: res,
- lastEdited: await pad.getLastEdit(),
- userCount: api.padUsersCount(res).padUsersCount,
- revisionNumber: pad.getHeadRevisionNumber()
- };
-
- if (currentWinners.length < query.limit) {
- if(queryOffsetCounter < query.offset){
- queryOffsetCounter++
- continue
+ socket.on('load', async (query: string): Promise => {
+ let data;
+ try {
+ data = await fsp.readFile(settings.settingsFilename, 'utf8');
+ } catch (err) {
+ return console.log(err);
}
- currentWinners.push({
- padName: res,
- lastEdited: await pad.getLastEdit(),
- userCount: api.padUsersCount(res).padUsersCount,
- revisionNumber: pad.getHeadRevisionNumber()
- })
- } else {
- // Kick out worst pad and replace by current pad
- let worstPad = currentWinners.sort((a, b) => {
- if (a[query.sortBy] < b[query.sortBy]) return query.ascending ? -1 : 1;
- if (a[query.sortBy] > b[query.sortBy]) return query.ascending ? 1 : -1;
- return 0;
- })
- if(worstPad[0]&&worstPad[0][query.sortBy] < padType[query.sortBy]){
- if(queryOffsetCounter < query.offset){
- queryOffsetCounter++
- continue
- }
- currentWinners.splice(currentWinners.indexOf(worstPad[0]), 1)
- currentWinners.push({
- padName: res,
- lastEdited: await pad.getLastEdit(),
- userCount: api.padUsersCount(res).padUsersCount,
- revisionNumber: pad.getHeadRevisionNumber()
- })
+ // if showSettingsInAdminPage is set to false, then return NOT_ALLOWED in the result
+ if (settings.showSettingsInAdminPage === false) {
+ socket.emit('settings', {results: 'NOT_ALLOWED'});
+ } else {
+ socket.emit('settings', {results: data});
}
- }
+ });
+
+ socket.on('saveSettings', async (newSettings: string) => {
+ console.log('Admin request to save settings through a socket on /admin/settings');
+ await fsp.writeFile(settings.settingsFilename, newSettings);
+ socket.emit('saveprogress', 'saved');
+ });
+
+
+ type ShoutMessage = {
+ message: string,
+ sticky: boolean,
}
- data.results = currentWinners;
- }
- socket.emit('results:padLoad', data);
- })
+ socket.on('shout', (message: ShoutMessage) => {
+ const messageToSend = {
+ type: "COLLABROOM",
+ data: {
+ type: "shoutMessage",
+ payload: {
+ message: message,
+ timestamp: Date.now()
+ }
+ }
+ }
+
+ io.of('/settings').emit('shout', messageToSend);
+ io.sockets.emit('shout', messageToSend);
+ })
- socket.on('deletePad', async (padId: string) => {
- const padExists = await padManager.doesPadExists(padId);
- if (padExists) {
- const pad = await padManager.getPad(padId);
- await pad.remove();
- socket.emit('results:deletePad', padId);
- }
- })
+ socket.on('help', () => {
+ const gitCommit = settings.getGitCommit();
+ const epVersion = settings.getEpVersion();
- socket.on('restartServer', async () => {
- console.log('Admin request to restart server through a socket on /admin/settings');
- settings.reloadSettings();
- await plugins.update();
- await hooks.aCallAll('loadSettings', {settings});
- await hooks.aCallAll('restartServer');
+ const hooks: Map> = plugins.getHooks('hooks', false);
+ const clientHooks: Map> = plugins.getHooks('client_hooks', false);
+
+ function mapToObject(map: Map) {
+ let obj = Object.create(null);
+ for (let [k, v] of map) {
+ if (v instanceof Map) {
+ obj[k] = mapToObject(v);
+ } else {
+ obj[k] = v;
+ }
+ }
+ return obj;
+ }
+
+ socket.emit('reply:help', {
+ gitCommit,
+ epVersion,
+ installedPlugins: plugins.getPlugins(),
+ installedParts: plugins.getParts(),
+ installedServerHooks: mapToObject(hooks),
+ installedClientHooks: mapToObject(clientHooks),
+ latestVersion: UpdateCheck.getLatestVersion(),
+ })
+ });
+
+
+ socket.on('padLoad', async (query: PadSearchQuery) => {
+ const {padIDs} = await padManager.listAllPads();
+
+ const data: {
+ total: number,
+ results?: PadQueryResult[]
+ } = {
+ total: padIDs.length,
+ };
+ let result: string[] = padIDs;
+ let maxResult;
+
+ // Filter out matches
+ if (query.pattern) {
+ result = result.filter((padName: string) => padName.includes(query.pattern));
+ }
+
+ data.total = result.length;
+
+ maxResult = result.length - 1;
+ if (maxResult < 0) {
+ maxResult = 0;
+ }
+
+ if (query.offset && query.offset < 0) {
+ query.offset = 0;
+ } else if (query.offset > maxResult) {
+ query.offset = maxResult;
+ }
+
+ if (query.limit && query.limit < 0) {
+ query.limit = 0;
+ } else if (query.limit > queryPadLimit) {
+ query.limit = queryPadLimit;
+ }
+
+ if (query.sortBy === 'padName') {
+ result = result.sort((a, b) => {
+ if (a < b) return query.ascending ? -1 : 1;
+ if (a > b) return query.ascending ? 1 : -1;
+ return 0;
+ }).slice(query.offset, query.offset + query.limit);
+
+ data.results = await Promise.all(result.map(async (padName: string) => {
+ const pad = await padManager.getPad(padName);
+ const revisionNumber = pad.getHeadRevisionNumber()
+ const userCount = api.padUsersCount(padName).padUsersCount;
+ const lastEdited = await pad.getLastEdit();
+
+ return {
+ padName,
+ lastEdited,
+ userCount,
+ revisionNumber
+ }
+ }));
+ } else {
+ const currentWinners: PadQueryResult[] = []
+ let queryOffsetCounter = 0
+ for (let res of result) {
+
+ const pad = await padManager.getPad(res);
+ const padType = {
+ padName: res,
+ lastEdited: await pad.getLastEdit(),
+ userCount: api.padUsersCount(res).padUsersCount,
+ revisionNumber: pad.getHeadRevisionNumber()
+ };
+
+ if (currentWinners.length < query.limit) {
+ if (queryOffsetCounter < query.offset) {
+ queryOffsetCounter++
+ continue
+ }
+ currentWinners.push({
+ padName: res,
+ lastEdited: await pad.getLastEdit(),
+ userCount: api.padUsersCount(res).padUsersCount,
+ revisionNumber: pad.getHeadRevisionNumber()
+ })
+ } else {
+ // Kick out worst pad and replace by current pad
+ let worstPad = currentWinners.sort((a, b) => {
+ if (a[query.sortBy] < b[query.sortBy]) return query.ascending ? -1 : 1;
+ if (a[query.sortBy] > b[query.sortBy]) return query.ascending ? 1 : -1;
+ return 0;
+ })
+ if (worstPad[0] && worstPad[0][query.sortBy] < padType[query.sortBy]) {
+ if (queryOffsetCounter < query.offset) {
+ queryOffsetCounter++
+ continue
+ }
+ currentWinners.splice(currentWinners.indexOf(worstPad[0]), 1)
+ currentWinners.push({
+ padName: res,
+ lastEdited: await pad.getLastEdit(),
+ userCount: api.padUsersCount(res).padUsersCount,
+ revisionNumber: pad.getHeadRevisionNumber()
+ })
+ }
+ }
+ }
+ data.results = currentWinners;
+ }
+
+ socket.emit('results:padLoad', data);
+ })
+
+
+ socket.on('deletePad', async (padId: string) => {
+ const padExists = await padManager.doesPadExists(padId);
+ if (padExists) {
+ const pad = await padManager.getPad(padId);
+ await pad.remove();
+ socket.emit('results:deletePad', padId);
+ }
+ })
+
+ socket.on('restartServer', async () => {
+ console.log('Admin request to restart server through a socket on /admin/settings');
+ settings.reloadSettings();
+ await plugins.update();
+ await hooks.aCallAll('loadSettings', {settings});
+ await hooks.aCallAll('restartServer');
+ });
});
- });
};
-
-const searchPad = async (query:PadSearchQuery) => {
+const searchPad = async (query: PadSearchQuery) => {
}
diff --git a/src/static/js/pad.js b/src/static/js/pad.js
index 1278f4852..eb5b72cbc 100644
--- a/src/static/js/pad.js
+++ b/src/static/js/pad.js
@@ -250,11 +250,27 @@ const handshake = async () => {
socket.on('disconnect', (reason) => {
// The socket.io client will automatically try to reconnect for all reasons other than "io
// server disconnect".
+ console.log(`Socket disconnected: ${reason}`)
if (reason !== 'io server disconnect') return;
socketReconnecting();
socket.connect();
});
+
+ socket.on('shout', (obj) => {
+ if(obj.type === "COLLABROOM") {
+ let date = new Date(obj.data.payload.timestamp);
+ $.gritter.add({
+ // (string | mandatory) the heading of the notification
+ title: 'Admin message',
+ // (string | mandatory) the text inside the notification
+ text: '[' + date.toLocaleTimeString() + ']: ' + obj.data.payload.message.message,
+ // (bool | optional) if you want it to fade out on its own or just sit there
+ sticky: obj.data.payload.message.sticky
+ });
+ }
+ })
+
socket.on('reconnecting', socketReconnecting);
socket.on('reconnect_failed', (error) => {
diff --git a/src/tests/frontend-new/admin-spec/admintroubleshooting.spec.ts b/src/tests/frontend-new/admin-spec/admintroubleshooting.spec.ts
index 9dc7c7a20..9155e9cbd 100644
--- a/src/tests/frontend-new/admin-spec/admintroubleshooting.spec.ts
+++ b/src/tests/frontend-new/admin-spec/admintroubleshooting.spec.ts
@@ -10,7 +10,7 @@ test('Shows troubleshooting page manager', async ({page}) => {
await page.goto('http://localhost:9001/admin/help')
await page.waitForSelector('.menu')
const menu = page.locator('.menu');
- await expect(menu.locator('li')).toHaveCount(4);
+ await expect(menu.locator('li')).toHaveCount(5);
})
test('Shows a version number', async function ({page}) {