diff --git a/art/themes/luciano_blocktronics/theme.hjson b/art/themes/luciano_blocktronics/theme.hjson index aa720934..f359539f 100644 --- a/art/themes/luciano_blocktronics/theme.hjson +++ b/art/themes/luciano_blocktronics/theme.hjson @@ -353,6 +353,21 @@ } } + nodeMessage: { + 0: { + mci: { + SM1: { + width: 22 + itemFormat: "|00|07{text} |08(|07{userName}|08)" + focusItemFormat: "|00|15{text} |07(|15{userName}|07)" + } + ET2: { + width: 70 + } + } + } + } + messageAreaViewPost: { 0: { diff --git a/core/ansi_term.js b/core/ansi_term.js index 44c82464..f00fd011 100644 --- a/core/ansi_term.js +++ b/core/ansi_term.js @@ -56,7 +56,7 @@ exports.getSyncTERMFontFromAlias = getSyncTERMFontFromAlias; exports.setSyncTermFontWithAlias = setSyncTermFontWithAlias; exports.setCursorStyle = setCursorStyle; exports.setEmulatedBaudRate = setEmulatedBaudRate; -exports.vtxHyperlink = vtxHyperlink; +exports.vtxHyperlink = vtxHyperlink; // // See also diff --git a/core/client.js b/core/client.js index 1caaa85d..6f4aec79 100644 --- a/core/client.js +++ b/core/client.js @@ -32,13 +32,14 @@ ----/snip/---------------------- */ // ENiGMA½ -const term = require('./client_term.js'); -const ansi = require('./ansi_term.js'); -const User = require('./user.js'); -const Config = require('./config.js').get; -const MenuStack = require('./menu_stack.js'); -const ACS = require('./acs.js'); -const Events = require('./events.js'); +const term = require('./client_term.js'); +const ansi = require('./ansi_term.js'); +const User = require('./user.js'); +const Config = require('./config.js').get; +const MenuStack = require('./menu_stack.js'); +const ACS = require('./acs.js'); +const Events = require('./events.js'); +const UserInterruptQueue = require('./user_interrupt_queue.js'); // deps const stream = require('stream'); @@ -84,6 +85,7 @@ function Client(/*input, output*/) { this.menuStack = new MenuStack(this); this.acs = new ACS(this); this.mciCache = {}; + this.interruptQueue = new UserInterruptQueue(this); this.clearMciCache = function() { this.mciCache = {}; diff --git a/core/client_connections.js b/core/client_connections.js index 93bb9465..f0bca4d7 100644 --- a/core/client_connections.js +++ b/core/client_connections.js @@ -15,11 +15,16 @@ exports.getActiveNodeList = getActiveNodeList; exports.addNewClient = addNewClient; exports.removeClient = removeClient; exports.getConnectionByUserId = getConnectionByUserId; +exports.getConnectionByNodeId = getConnectionByNodeId; const clientConnections = []; -exports.clientConnections = clientConnections; +exports.clientConnections = clientConnections; -function getActiveConnections() { return clientConnections; } +function getActiveConnections(authUsersOnly = false) { + return clientConnections.filter(conn => { + return ((authUsersOnly && conn.user.isAuthenticated()) || !authUsersOnly); + }); +} function getActiveNodeList(authUsersOnly) { @@ -29,11 +34,7 @@ function getActiveNodeList(authUsersOnly) { const now = moment(); - const activeConnections = getActiveConnections().filter(ac => { - return ((authUsersOnly && ac.user.isAuthenticated()) || !authUsersOnly); - }); - - return _.map(activeConnections, ac => { + return _.map(getActiveConnections(authUsersOnly), ac => { const entry = { node : ac.node, authenticated : ac.user.isAuthenticated(), @@ -118,3 +119,7 @@ function removeClient(client) { function getConnectionByUserId(userId) { return getActiveConnections().find( ac => userId === ac.user.userId ); } + +function getConnectionByNodeId(nodeId) { + return getActiveConnections().find( ac => nodeId == ac.node ); +} diff --git a/core/menu_module.js b/core/menu_module.js index ee1502cb..ce8eb556 100644 --- a/core/menu_module.js +++ b/core/menu_module.js @@ -25,15 +25,13 @@ exports.MenuModule = class MenuModule extends PluginModule { this.menuName = options.menuName; this.menuConfig = options.menuConfig; this.client = options.client; - //this.menuConfig.options = options.menuConfig.options || {}; this.menuMethods = {}; // methods called from @method's this.menuConfig.config = this.menuConfig.config || {}; - this.cls = _.get(this.menuConfig.config, 'cls', Config().menus.cls); - - //this.cls = _.isBoolean(this.menuConfig.options.cls) ? this.menuConfig.options.cls : Config().menus.cls; - this.viewControllers = {}; + + // *initial* interruptable state for this menu + this.disableInterruption(); } enter() { @@ -44,6 +42,14 @@ exports.MenuModule = class MenuModule extends PluginModule { this.detachViewControllers(); } + toggleInterruptionAndDisplayQueued(cb) { + this.enableInterruption(); + this.displayQueuedInterruptions( () => { + this.disableInterruption(); + return cb(null); + }); + } + initSequence() { const self = this; const mciData = {}; @@ -51,8 +57,11 @@ exports.MenuModule = class MenuModule extends PluginModule { async.series( [ + function beforeArtInterrupt(callback) { + return self.toggleInterruptionAndDisplayQueued(callback); + }, function beforeDisplayArt(callback) { - self.beforeArt(callback); + return self.beforeArt(callback); }, function displayMenuArt(callback) { if(!_.isString(self.menuConfig.art)) { @@ -160,6 +169,48 @@ exports.MenuModule = class MenuModule extends PluginModule { // nothing in base } + neverInterruptable() { + return this.menuConfig.config.interruptable === 'never'; + } + + enableInterruption() { + if(!this.neverInterruptable()) { + this.interruptable = true; + } + } + + disableInterruption() { + if(!this.neverInterruptable()) { + this.interruptable = false; + } + } + + displayQueuedInterruptions(cb) { + if(true !== this.interruptable) { + return cb(null); + } + + async.whilst( + () => this.client.interruptQueue.hasItems(), + next => { + this.client.interruptQueue.display( (err, interruptItem) => { + if(err) { + return next(err); + } + + if(interruptItem.pause) { + return this.pausePrompt(next); + } + + return next(null); + }); + }, + err => { + return cb(err); + } + ) + } + getSaveState() { // nothing in base } @@ -178,11 +229,15 @@ exports.MenuModule = class MenuModule extends PluginModule { return this.prevMenu(cb); // no next, go to prev } - return this.client.menuStack.next(cb); + this.displayQueuedInterruptions( () => { + return this.client.menuStack.next(cb); + }); } prevMenu(cb) { - return this.client.menuStack.prev(cb); + this.displayQueuedInterruptions( () => { + return this.client.menuStack.prev(cb); + }); } gotoMenu(name, options, cb) { @@ -234,13 +289,13 @@ exports.MenuModule = class MenuModule extends PluginModule { } autoNextMenu(cb) { - const self = this; - - function gotoNextMenu() { - if(self.haveNext()) { - return menuUtil.handleNext(self.client, self.menuConfig.next, {}, cb); + const gotoNextMenu = () => { + if(this.haveNext()) { + this.displayQueuedInterruptions( () => { + return menuUtil.handleNext(this.client, this.menuConfig.next, {}, cb); + }); } else { - return self.prevMenu(cb); + return this.prevMenu(cb); } } diff --git a/core/node_msg.js b/core/node_msg.js new file mode 100644 index 00000000..2c6f23ed --- /dev/null +++ b/core/node_msg.js @@ -0,0 +1,126 @@ +/* jslint node: true */ +'use strict'; + +// ENiGMA½ +const { MenuModule } = require('./menu_module.js'); +const { Errors } = require('./enig_error.js'); +const { + getActiveNodeList, + getConnectionByNodeId, +} = require('./client_connections.js'); +const UserInterruptQueue = require('./user_interrupt_queue.js'); + +// deps +const series = require('async/series'); +const _ = require('lodash'); + +exports.moduleInfo = { + name : 'Node Message', + desc : 'Multi-node messaging', + author : 'NuSkooler', +}; + +const FormIds = { + sendMessage : 0, +}; + +const MciViewIds = { + sendMessage : { + nodeSelect : 1, + message : 2, + preview : 3, + + customRangeStart : 10, + } +} + +exports.getModule = class NodeMessageModule extends MenuModule { + constructor(options) { + super(options); + this.config = Object.assign({}, _.get(options, 'menuConfig.config'), { extraArgs : options.extraArgs }); + + this.menuMethods = { + sendMessage : (formData, extraArgs, cb) => { + const nodeId = formData.value.node; + const message = formData.value.message; + + const interruptItem = { + contents : message, + } + + if(0 === nodeId) { + // ALL nodes + UserInterruptQueue.queueGlobalOtherActive(interruptItem, this.client); + } else { + UserInterruptQueue.queueGlobal(interruptItem, [ getConnectionByNodeId(nodeId) ]); + } + + return this.prevMenu(cb); + }, + } + } + + mciReady(mciData, cb) { + super.mciReady(mciData, err => { + if(err) { + return cb(err); + } + + series( + [ + (next) => { + return this.prepViewController('sendMessage', FormIds.sendMessage, mciData.menu, next); + }, + (next) => { + const nodeSelectView = this.viewControllers.sendMessage.getView(MciViewIds.sendMessage.nodeSelect); + if(!nodeSelectView) { + return next(Errors.MissingMci(`Missing node selection MCI ${MciViewIds.sendMessage.nodeSelect}`)); + } + + this.prepareNodeList(); + + nodeSelectView.on('index update', idx => { + this.nodeListSelectionIndexUpdate(idx); + }); + + nodeSelectView.setItems(this.nodeList); + nodeSelectView.redraw(); + this.nodeListSelectionIndexUpdate(0); + return next(null); + } + ], + err => { + return cb(err); + } + ); + }); + } + + prepareNodeList() { + // standard node list with {text} field added for compliance + this.nodeList = [{ + text : '-ALL-', + // dummy fields: + node : 0, + authenticated : false, + userId : 0, + action : 'N/A', + userName : 'Everyone', + realName : 'All Users', + location : 'N/A', + affils : 'N/A', + timeOn : 'N/A', + }].concat(getActiveNodeList(true) + .map(node => Object.assign(node, { text : node.node.toString() } )) + ).filter(node => node.node !== this.client.node); // remove our client's node + this.nodeList.sort( (a, b) => a.node - b.node ); // sort by node + } + + nodeListSelectionIndexUpdate(idx) { + const node = this.nodeList[idx]; + if(!node) { + return; + } + this.updateCustomViewTextsWithFilter('sendMessage', MciViewIds.sendMessage.customRangeStart, node); + } +} \ No newline at end of file diff --git a/core/user_interrupt_queue.js b/core/user_interrupt_queue.js new file mode 100644 index 00000000..a29fd4df --- /dev/null +++ b/core/user_interrupt_queue.js @@ -0,0 +1,58 @@ +/* jslint node: true */ +'use strict'; + +// ENiGMA½ +const Art = require('./art.js'); +const { + getActiveConnections +} = require('./client_connections.js'); +const ANSI = require('./ansi_term.js'); + +// deps +const _ = require('lodash'); + +module.exports = class UserInterruptQueue +{ + constructor(client) { + this.client = client; + this.queue = []; + } + + static queueGlobal(interruptItem, connections) { + connections.forEach(conn => { + conn.interruptQueue.queueItem(interruptItem); + }); + } + + // common shortcut: queue global, all active clients minus |client| + static queueGlobalOtherActive(interruptItem, client) { + const otherConnections = getActiveConnections(true).filter(ac => ac.node !== client.node); + return UserInterruptQueue.queueGlobal(interruptItem, otherConnections ); + } + + queueItem(interruptItem) { + interruptItem.pause = _.get(interruptItem, 'pause', true); + this.queue.push(interruptItem); + } + + hasItems() { + return this.queue.length > 0; + } + + display(cb) { + const interruptItem = this.queue.pop(); + if(!interruptItem) { + return cb(null); + } + + if(interruptItem.cls) { + this.client.term.rawWrite(ANSI.clearScreen()); + } else { + this.client.term.rawWrite('\r\n\r\n'); + } + + Art.display(this.client, interruptItem.contents, err => { + return cb(err, interruptItem); + }); + } +}; \ No newline at end of file