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()}
+
+
+ ) + }) + } +
+
{ + e.preventDefault() + sendMessage() + }} className="send-message search-field" style={{display: 'flex', gap: '10px'}}> + { + setSticky(!sticky); + }}> + + + setMessage(v.target.value)} + style={{width: '100%', paddingRight: '55px', backgroundColor: '#e0e0e0', flexGrow: 1}}/> + sendMessage()}/> + +
+
+ ) +} 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}) {