From bbf4adb0751abc401e97c7769362482cec38ffa5 Mon Sep 17 00:00:00 2001 From: SamTv12345 Date: Wed, 17 Jul 2024 10:47:59 +0200 Subject: [PATCH] Added live reloading. --- src/ep.json | 3 +- src/node/handler/PadMessageHandler.ts | 3 +- src/node/hooks/express/specialpages.ts | 160 ++++++++++++++++--------- src/static/js/pad.js | 11 ++ src/static/js/timeslider.js | 8 ++ src/static/js/vendors/html10n.ts | 50 ++++---- 6 files changed, 144 insertions(+), 91 deletions(-) diff --git a/src/ep.json b/src/ep.json index a6d65a08f..c9b26c175 100644 --- a/src/ep.json +++ b/src/ep.json @@ -42,7 +42,8 @@ "name": "specialpages", "hooks": { "expressCreateServer": "ep_etherpad-lite/node/hooks/express/specialpages", - "expressPreSession": "ep_etherpad-lite/node/hooks/express/specialpages" + "expressPreSession": "ep_etherpad-lite/node/hooks/express/specialpages", + "socketio": "ep_etherpad-lite/node/hooks/express/specialpages" } }, { diff --git a/src/node/handler/PadMessageHandler.ts b/src/node/handler/PadMessageHandler.ts index 2ed40391c..390949607 100644 --- a/src/node/handler/PadMessageHandler.ts +++ b/src/node/handler/PadMessageHandler.ts @@ -996,7 +996,8 @@ const handleClientReady = async (socket:any, message: typeof ChatMessage) => { percentageToScrollWhenUserPressesArrowUp: settings.scrollWhenFocusLineIsOutOfViewport.percentageToScrollWhenUserPressesArrowUp, }, - initialChangesets: [], // FIXME: REMOVE THIS SHIT + initialChangesets: [], // FIXME: REMOVE THIS SHIT, + mode: process.env.NODE_ENV }; // Add a username to the clientVars if one avaiable diff --git a/src/node/hooks/express/specialpages.ts b/src/node/hooks/express/specialpages.ts index 6e2b03df9..2c1286f40 100644 --- a/src/node/hooks/express/specialpages.ts +++ b/src/node/hooks/express/specialpages.ts @@ -12,6 +12,13 @@ const webaccess = require('./webaccess'); const plugins = require('../../../static/js/pluginfw/plugin_defs'); import {build, buildSync} from 'esbuild' +let ioI: { sockets: { sockets: any[]; }; } | null = null + +exports.socketio = (hookName: string, {io}: any) => { + ioI = io +} + + exports.expressPreSession = async (hookName:string, {app}:any) => { // This endpoint is intended to conform to: // https://www.ietf.org/archive/id/draft-inadarei-api-health-check-06.html @@ -100,6 +107,99 @@ const convertTypescript = (content: string) => { } } +const handleLiveReload = async (args: any, padString: string, timeSliderString: string ) => { + const chokidar = await import('chokidar') + const watcher = chokidar.watch(path.join(settings.root, 'src', 'static', 'js')); + let routeHandlers: { [key: string]: Function } = {}; + + const setRouteHandler = (path: string, newHandler: Function) => { + routeHandlers[path] = newHandler; + }; + args.app.use((req: any, res: any, next: Function) => { + if (req.path.startsWith('/p/') && req.path.split('/').length == 3) { + req.params = { + pad: req.path.split('/')[2] + } + routeHandlers['/p/:pad'](req, res); + } else if (req.path.startsWith('/p/') && req.path.split('/').length == 4) { + req.params = { + pad: req.path.split('/')[2] + } + routeHandlers['/p/:pad/timeslider'](req, res); + } else if (routeHandlers[req.path]) { + routeHandlers[req.path](req, res); + } else { + next(); + } + }); + + function handleUpdate() { + convertTypescriptWatched(padString, (output, hash) => { + console.log("New pad hash is", hash) + setRouteHandler('/watch/pad', (req: any, res: any) => { + res.header('Content-Type', 'application/javascript'); + res.send(output) + }) + + setRouteHandler("/p/:pad", (req: any, res: any, next: Function) => { + // The below might break for pads being rewritten + const isReadOnly = !webaccess.userCanModify(req.params.pad, req); + + hooks.callAll('padInitToolbar', { + toolbar, + isReadOnly + }); + + // can be removed when require-kernel is dropped + res.header('Feature-Policy', 'sync-xhr \'self\''); + const content = eejs.require('ep_etherpad-lite/templates/pad.html', { + req, + toolbar, + isReadOnly, + entrypoint: '/watch/pad?hash=' + hash + }) + res.send(content); + }) + ioI!.sockets.sockets.forEach(socket => socket.emit('liveupdate')) + }) + convertTypescriptWatched(timeSliderString, (output, hash) => { + // serve timeslider.html under /p/$padname/timeslider + console.log("New timeslider hash is", hash) + + setRouteHandler('/watch/timeslider', (req: any, res: any) => { + res.header('Content-Type', 'application/javascript'); + res.send(output) + }) + + setRouteHandler("/p/:pad/timeslider", (req: any, res: any, next: Function) => { + console.log("Reloading pad") + // The below might break for pads being rewritten + const isReadOnly = !webaccess.userCanModify(req.params.pad, req); + + hooks.callAll('padInitToolbar', { + toolbar, + isReadOnly + }); + + // can be removed when require-kernel is dropped + res.header('Feature-Policy', 'sync-xhr \'self\''); + const content = eejs.require('ep_etherpad-lite/templates/pad.html', { + req, + toolbar, + isReadOnly, + entrypoint: '/watch/timeslider?hash=' + hash + }) + res.send(content); + }) + }) + } + + watcher.on('change', path => { + console.log(`File ${path} has been changed`); + handleUpdate(); + }); + handleUpdate() +} const convertTypescriptWatched = (content: string, cb: (output:string, hash: string)=>void) => { build({ @@ -224,67 +324,9 @@ exports.expressCreateServer = async (hookName: string, args: any, cb: Function) })); }); } else { - const chokidar = await import('chokidar') - const map = new Map() - const watcher = chokidar.watch(path.join(settings.root, 'src','static','js')); - watcher.on('change', path => { - console.log(`File ${path} has been changed`); - convertTypescriptWatched(padString, (output, hash)=>{ - console.log("New hash is", hash) - map.set('output', output) - map.set('fileNamePad', `padbootstrap-${hash}.js`) - }); - convertTypescriptWatched(timeSliderString, (output, hash)=>{ - // serve timeslider.html under /p/$padname/timeslider - console.log("New hash is", hash) - - args.app.get('/watch/timeslider', (req: any, res: any) => { - res.header('Content-Type', 'application/javascript'); - res.send(output) - }) - }); - }); - - args.app.get('/watch/pad', (req: any, res: any) => { - res.header('Content-Type', 'application/javascript'); - res.send(map.get('output')) - }) - // serve pad.html under /p - args.app.get('/p/:pad', (req: any, res: any, next: Function) => { - console.log("Reloading pad") - // The below might break for pads being rewritten - const isReadOnly = !webaccess.userCanModify(req.params.pad, req); - - hooks.callAll('padInitToolbar', { - toolbar, - isReadOnly - }); - - // can be removed when require-kernel is dropped - res.header('Feature-Policy', 'sync-xhr \'self\''); - const content = eejs.require('ep_etherpad-lite/templates/pad.html', { - req, - toolbar, - isReadOnly, - entrypoint: map.get('fileNamePad') - }) - res.send(content); - }); - args.app.get('/p/:pad/timeslider', (req: any, res: any, next: Function) => { - hooks.callAll('padInitToolbar', { - toolbar, - }); - let timeSliderFileName = "/watch/timeslider?hash="+map.get('fileNamePad') - - res.send(eejs.require('ep_etherpad-lite/templates/timeslider.html', { - req, - toolbar, - entrypoint: timeSliderFileName - })); - }); + await handleLiveReload(args, padString, timeSliderString) } - // The client occasionally polls this endpoint to get an updated expiration for the express_sid // cookie. This handler must be installed after the express-session middleware. args.app.put('/_extendExpressSessionLifetime', (req: any, res: any) => { diff --git a/src/static/js/pad.js b/src/static/js/pad.js index bcf5f5621..d2da1b6fb 100644 --- a/src/static/js/pad.js +++ b/src/static/js/pad.js @@ -24,6 +24,7 @@ let socket; + // These jQuery things should create local references, but for now `require()` // assigns to the global `$` and augments it with plugins. require('./vendors/jquery'); @@ -283,6 +284,7 @@ const handshake = async () => { } }); + socket.on('error', (error) => { // pad.collabClient might be null if the error occurred before the hanshake completed. if (pad.collabClient != null) { @@ -315,6 +317,15 @@ const handshake = async () => { () => $.ajax('../_extendExpressSessionLifetime', {method: 'PUT'}).catch(() => {}); setInterval(ping, window.clientVars.sessionRefreshInterval); } + if(window.clientVars.mode === "development") { + console.warn('Enabling development mode with live update') + socket.on('liveupdate', ()=>{ + + console.log('Live reload update received') + location.reload() + }) + } + } else if (obj.disconnect) { padconnectionstatus.disconnected(obj.disconnect); socket.disconnect(); diff --git a/src/static/js/timeslider.js b/src/static/js/timeslider.js index 4cc5d45a6..9a065a973 100644 --- a/src/static/js/timeslider.js +++ b/src/static/js/timeslider.js @@ -117,6 +117,14 @@ const handleClientVars = (message) => { setInterval(ping, window.clientVars.sessionRefreshInterval); } + if(window.clientVars.mode === "development") { + console.warn('Enabling development mode with live update') + socket.on('liveupdate', ()=>{ + console.log('Doing live reload') + location.reload() + }) + } + // load all script that doesn't work without the clientVars BroadcastSlider = require('./broadcast_slider') .loadBroadcastSliderJS(fireWhenAllScriptsAreLoaded); diff --git a/src/static/js/vendors/html10n.ts b/src/static/js/vendors/html10n.ts index 90fd4d1ae..6167a8541 100644 --- a/src/static/js/vendors/html10n.ts +++ b/src/static/js/vendors/html10n.ts @@ -8,15 +8,14 @@ export class Html10n { private rtl: string[] private _pluralRules?: PluralFunc public mt: MicroEvent - private loader: Loader - private translations: Map + private loader: Loader | undefined + public translations: Map private macros: Map constructor() { this.language = undefined this.rtl = ["ar","dv","fa","ha","he","ks","ku","ps","ur","yi"] this.mt = new MicroEvent() - this.loader = new Loader([]) this.translations = new Map() this.macros = new Map() @@ -469,16 +468,6 @@ export class Html10n { } getTranslatableChildren(element: HTMLElement) { - if(!document.querySelectorAll) { - if (!element) return [] - const nodes = element.getElementsByTagName('*') - , l10nElements = [] - for (let i=0, n=nodes.length; i < n; i++) { - if (nodes[i].getAttribute('data-l10n-id')) - l10nElements.push(nodes[i]); - } - return l10nElements - } return element.querySelectorAll('*[data-l10n-id]') } @@ -494,6 +483,7 @@ export class Html10n { }) this.build(langs, (er: null, translations: Map) =>{ + console.log("Translations are", translations) this.translations = translations this.translateElement(translations) this.mt.trigger('localized') @@ -535,13 +525,11 @@ export class Html10n { * @param cb Function - a callback that will be called once all langs have been loaded */ build(langs: (string|undefined)[], cb: Function) { - - const that = this; const build = new Map() - this.asyncForEach(langs, function (lang: string, _i: number, next:LoaderFunc) { + this.asyncForEach(langs, (lang: string, _i: number, next:LoaderFunc)=> { if(!lang) return next(); - that.loader.load(lang, next) + this.loader!.load(lang, next) }, () =>{ let lang; langs.reverse() @@ -549,30 +537,31 @@ export class Html10n { // loop through the priority array... for (let i=0, n=langs.length; i < n; i++) { lang = langs[i] - if(!lang) continue; - if(!(lang in that.loader.langs)) {// uh, we don't have this lang availbable.. + if(!(lang in langs)) {// uh, we don't have this lang availbable.. // then check for related langs if(~lang.indexOf('-')) lang = lang.split('-')[0]; - let l - for(l in that.loader.langs) { - if(lang != l && l.indexOf(lang) === 0) { + let l: string|undefined = '' + for(l of langs) { + if(l && lang != l && l.indexOf(lang) === 0) { lang = l break; } } + // @ts-ignore if(lang != l) continue; } // ... and apply all strings of the current lang in the list // to our build object - for (let string in that.loader.langs.get(lang)) { - build.set(string,that.loader.langs.get(lang).get(string)) + //lang = "de" + for (let string in this.loader!.langs.get(lang)) { + build.set(string,this.loader!.langs.get(lang)[string]) } // the last applied lang will be exposed as the // lang the page was translated to - that.language = lang + this.language = lang } cb(null, build) }) @@ -848,7 +837,6 @@ class Loader { } fetch(href: string, lang: string, callback: ErrorFunc) { - const that = this; if (this.cache.get(href)) { this.parse(lang, href, this.cache.get(href), callback) @@ -860,13 +848,14 @@ class Loader { if (xhr.overrideMimeType) { xhr.overrideMimeType('application/json; charset=utf-8'); } - xhr.onreadystatechange = function() { + xhr.onreadystatechange = ()=> { if (xhr.readyState == 4) { if (xhr.status == 200 || xhr.status === 0) { const data = JSON.parse(xhr.responseText); - that.cache.set(href, data) + console.log("Data is", data) + this.cache.set(href, data) // Pass on the contents for parsing - that.parse(lang, href, data, callback) + this.parse(lang, href, data, callback) } else { callback(new Error('Failed to load '+href)) } @@ -875,6 +864,7 @@ class Loader { xhr.send(null); } + parse(lang: string, href: string, data: { [key: string]: string }, callback: ErrorFunc) { @@ -957,7 +947,6 @@ class Loader { return callback(new Error(msg)); } } - } @@ -983,6 +972,7 @@ class Loader { return } + console.log("Setting lang", lang) this.langs.set(lang,data[lang]) // TODO: Also store accompanying langs callback()