Merge pull request #210 from NuSkooler/user-interruptions
User interruptions
This commit is contained in:
commit
154beb54ec
Binary file not shown.
|
@ -363,6 +363,27 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
nodeMessage: {
|
||||||
|
config: {
|
||||||
|
messageFormat: "|00|08 :: |03message from |11{fromUserName} |08/ |03node |11{fromNodeId}|08 @ |11{timestamp} |08::\r\n|07 {message}"
|
||||||
|
}
|
||||||
|
0: {
|
||||||
|
mci: {
|
||||||
|
SM1: {
|
||||||
|
width: 25
|
||||||
|
itemFormat: "|00|03node |07{text} |08(|07{userName}|08)"
|
||||||
|
focusItemFormat: "|00|11node |15{text} |07(|15{userName}|07)"
|
||||||
|
}
|
||||||
|
ET2: {
|
||||||
|
width: 65
|
||||||
|
}
|
||||||
|
TL3: {
|
||||||
|
width: 65
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
messageAreaViewPost: {
|
messageAreaViewPost: {
|
||||||
0: {
|
0: {
|
||||||
|
|
|
@ -56,7 +56,7 @@ exports.getSyncTERMFontFromAlias = getSyncTERMFontFromAlias;
|
||||||
exports.setSyncTermFontWithAlias = setSyncTermFontWithAlias;
|
exports.setSyncTermFontWithAlias = setSyncTermFontWithAlias;
|
||||||
exports.setCursorStyle = setCursorStyle;
|
exports.setCursorStyle = setCursorStyle;
|
||||||
exports.setEmulatedBaudRate = setEmulatedBaudRate;
|
exports.setEmulatedBaudRate = setEmulatedBaudRate;
|
||||||
exports.vtxHyperlink = vtxHyperlink;
|
exports.vtxHyperlink = vtxHyperlink;
|
||||||
|
|
||||||
//
|
//
|
||||||
// See also
|
// See also
|
||||||
|
|
|
@ -32,13 +32,14 @@
|
||||||
----/snip/----------------------
|
----/snip/----------------------
|
||||||
*/
|
*/
|
||||||
// ENiGMA½
|
// ENiGMA½
|
||||||
const term = require('./client_term.js');
|
const term = require('./client_term.js');
|
||||||
const ansi = require('./ansi_term.js');
|
const ansi = require('./ansi_term.js');
|
||||||
const User = require('./user.js');
|
const User = require('./user.js');
|
||||||
const Config = require('./config.js').get;
|
const Config = require('./config.js').get;
|
||||||
const MenuStack = require('./menu_stack.js');
|
const MenuStack = require('./menu_stack.js');
|
||||||
const ACS = require('./acs.js');
|
const ACS = require('./acs.js');
|
||||||
const Events = require('./events.js');
|
const Events = require('./events.js');
|
||||||
|
const UserInterruptQueue = require('./user_interrupt_queue.js');
|
||||||
|
|
||||||
// deps
|
// deps
|
||||||
const stream = require('stream');
|
const stream = require('stream');
|
||||||
|
@ -84,6 +85,7 @@ function Client(/*input, output*/) {
|
||||||
this.menuStack = new MenuStack(this);
|
this.menuStack = new MenuStack(this);
|
||||||
this.acs = new ACS(this);
|
this.acs = new ACS(this);
|
||||||
this.mciCache = {};
|
this.mciCache = {};
|
||||||
|
this.interruptQueue = new UserInterruptQueue(this);
|
||||||
|
|
||||||
this.clearMciCache = function() {
|
this.clearMciCache = function() {
|
||||||
this.mciCache = {};
|
this.mciCache = {};
|
||||||
|
|
|
@ -12,17 +12,22 @@ const moment = require('moment');
|
||||||
const hashids = require('hashids');
|
const hashids = require('hashids');
|
||||||
|
|
||||||
exports.getActiveConnections = getActiveConnections;
|
exports.getActiveConnections = getActiveConnections;
|
||||||
exports.getActiveNodeList = getActiveNodeList;
|
exports.getActiveConnectionList = getActiveConnectionList;
|
||||||
exports.addNewClient = addNewClient;
|
exports.addNewClient = addNewClient;
|
||||||
exports.removeClient = removeClient;
|
exports.removeClient = removeClient;
|
||||||
exports.getConnectionByUserId = getConnectionByUserId;
|
exports.getConnectionByUserId = getConnectionByUserId;
|
||||||
|
exports.getConnectionByNodeId = getConnectionByNodeId;
|
||||||
|
|
||||||
const clientConnections = [];
|
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) {
|
function getActiveConnectionList(authUsersOnly) {
|
||||||
|
|
||||||
if(!_.isBoolean(authUsersOnly)) {
|
if(!_.isBoolean(authUsersOnly)) {
|
||||||
authUsersOnly = true;
|
authUsersOnly = true;
|
||||||
|
@ -30,11 +35,7 @@ function getActiveNodeList(authUsersOnly) {
|
||||||
|
|
||||||
const now = moment();
|
const now = moment();
|
||||||
|
|
||||||
const activeConnections = getActiveConnections().filter(ac => {
|
return _.map(getActiveConnections(authUsersOnly), ac => {
|
||||||
return ((authUsersOnly && ac.user.isAuthenticated()) || !authUsersOnly);
|
|
||||||
});
|
|
||||||
|
|
||||||
return _.map(activeConnections, ac => {
|
|
||||||
const entry = {
|
const entry = {
|
||||||
node : ac.node,
|
node : ac.node,
|
||||||
authenticated : ac.user.isAuthenticated(),
|
authenticated : ac.user.isAuthenticated(),
|
||||||
|
@ -119,3 +120,7 @@ function removeClient(client) {
|
||||||
function getConnectionByUserId(userId) {
|
function getConnectionByUserId(userId) {
|
||||||
return getActiveConnections().find( ac => userId === ac.user.userId );
|
return getActiveConnections().find( ac => userId === ac.user.userId );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getConnectionByNodeId(nodeId) {
|
||||||
|
return getActiveConnections().find( ac => nodeId == ac.node );
|
||||||
|
}
|
||||||
|
|
|
@ -25,15 +25,19 @@ exports.MenuModule = class MenuModule extends PluginModule {
|
||||||
this.menuName = options.menuName;
|
this.menuName = options.menuName;
|
||||||
this.menuConfig = options.menuConfig;
|
this.menuConfig = options.menuConfig;
|
||||||
this.client = options.client;
|
this.client = options.client;
|
||||||
//this.menuConfig.options = options.menuConfig.options || {};
|
|
||||||
this.menuMethods = {}; // methods called from @method's
|
this.menuMethods = {}; // methods called from @method's
|
||||||
this.menuConfig.config = this.menuConfig.config || {};
|
this.menuConfig.config = this.menuConfig.config || {};
|
||||||
|
|
||||||
this.cls = _.get(this.menuConfig.config, 'cls', Config().menus.cls);
|
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 = {};
|
this.viewControllers = {};
|
||||||
|
this.interrupt = (_.get(this.menuConfig.config, 'interrupt', MenuModule.InterruptTypes.Queued)).toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
static get InterruptTypes() {
|
||||||
|
return {
|
||||||
|
Never : 'never',
|
||||||
|
Queued : 'queued',
|
||||||
|
Realtime : 'realtime',
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
enter() {
|
enter() {
|
||||||
|
@ -56,8 +60,11 @@ exports.MenuModule = class MenuModule extends PluginModule {
|
||||||
|
|
||||||
async.series(
|
async.series(
|
||||||
[
|
[
|
||||||
|
function beforeArtInterrupt(callback) {
|
||||||
|
return self.displayQueuedInterruptions(callback);
|
||||||
|
},
|
||||||
function beforeDisplayArt(callback) {
|
function beforeDisplayArt(callback) {
|
||||||
self.beforeArt(callback);
|
return self.beforeArt(callback);
|
||||||
},
|
},
|
||||||
function displayMenuArt(callback) {
|
function displayMenuArt(callback) {
|
||||||
if(!hasArt()) {
|
if(!hasArt()) {
|
||||||
|
@ -165,6 +172,47 @@ exports.MenuModule = class MenuModule extends PluginModule {
|
||||||
// nothing in base
|
// nothing in base
|
||||||
}
|
}
|
||||||
|
|
||||||
|
displayQueuedInterruptions(cb) {
|
||||||
|
if(MenuModule.InterruptTypes.Never === this.interrupt) {
|
||||||
|
return cb(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
let opts = { cls : true }; // clear screen for first message
|
||||||
|
|
||||||
|
async.whilst(
|
||||||
|
() => this.client.interruptQueue.hasItems(),
|
||||||
|
next => {
|
||||||
|
this.client.interruptQueue.displayNext(opts, err => {
|
||||||
|
opts = {};
|
||||||
|
return next(err);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
err => {
|
||||||
|
return cb(err);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
attemptInterruptNow(interruptItem, cb) {
|
||||||
|
if(MenuModule.InterruptTypes.Realtime !== this.interrupt) {
|
||||||
|
return cb(null, false); // don't eat up the item; queue for later
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Default impl: clear screen -> standard display -> reload menu
|
||||||
|
//
|
||||||
|
this.client.interruptQueue.displayWithItem(
|
||||||
|
Object.assign({}, interruptItem, { cls : true }),
|
||||||
|
err => {
|
||||||
|
if(err) {
|
||||||
|
return cb(err, false);
|
||||||
|
}
|
||||||
|
this.reload(err => {
|
||||||
|
return cb(err, err ? false : true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
getSaveState() {
|
getSaveState() {
|
||||||
// nothing in base
|
// nothing in base
|
||||||
}
|
}
|
||||||
|
@ -183,11 +231,15 @@ exports.MenuModule = class MenuModule extends PluginModule {
|
||||||
return this.prevMenu(cb); // no next, go to prev
|
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) {
|
prevMenu(cb) {
|
||||||
return this.client.menuStack.prev(cb);
|
this.displayQueuedInterruptions( () => {
|
||||||
|
return this.client.menuStack.prev(cb);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
gotoMenu(name, options, cb) {
|
gotoMenu(name, options, cb) {
|
||||||
|
@ -239,15 +291,15 @@ exports.MenuModule = class MenuModule extends PluginModule {
|
||||||
}
|
}
|
||||||
|
|
||||||
autoNextMenu(cb) {
|
autoNextMenu(cb) {
|
||||||
const self = this;
|
const gotoNextMenu = () => {
|
||||||
|
if(this.haveNext()) {
|
||||||
function gotoNextMenu() {
|
this.displayQueuedInterruptions( () => {
|
||||||
if(self.haveNext()) {
|
return menuUtil.handleNext(this.client, this.menuConfig.next, {}, cb);
|
||||||
return menuUtil.handleNext(self.client, self.menuConfig.next, {}, cb);
|
});
|
||||||
} else {
|
} else {
|
||||||
return self.prevMenu(cb);
|
return this.prevMenu(cb);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
if(_.has(this.menuConfig, 'runtime.autoNext') && true === this.menuConfig.runtime.autoNext) {
|
if(_.has(this.menuConfig, 'runtime.autoNext') && true === this.menuConfig.runtime.autoNext) {
|
||||||
if(this.hasNextTimeout()) {
|
if(this.hasNextTimeout()) {
|
||||||
|
@ -499,4 +551,20 @@ exports.MenuModule = class MenuModule extends PluginModule {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
validateMCIByViewIds(formName, viewIds, cb) {
|
||||||
|
if(!Array.isArray(viewIds)) {
|
||||||
|
viewIds = [ viewIds ];
|
||||||
|
}
|
||||||
|
const form = _.get(this, [ 'viewControllers', formName ] );
|
||||||
|
if(!form) {
|
||||||
|
return cb(Errors.DoesNotExist(`Form does not exist: ${formName}`));
|
||||||
|
}
|
||||||
|
for(let i = 0; i < viewIds.length; ++i) {
|
||||||
|
if(!form.hasView(viewIds[i])) {
|
||||||
|
return cb(Errors.MissingMci(`Missing MCI ${viewIds[i]}`));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cb(null);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,217 @@
|
||||||
|
/* jslint node: true */
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// ENiGMA½
|
||||||
|
const { MenuModule } = require('./menu_module.js');
|
||||||
|
const {
|
||||||
|
getActiveConnectionList,
|
||||||
|
getConnectionByNodeId,
|
||||||
|
} = require('./client_connections.js');
|
||||||
|
const UserInterruptQueue = require('./user_interrupt_queue.js');
|
||||||
|
const { getThemeArt } = require('./theme.js');
|
||||||
|
const { pipeToAnsi } = require('./color_codes.js');
|
||||||
|
const stringFormat = require('./string_format.js');
|
||||||
|
const { renderStringLength } = require('./string_util.js');
|
||||||
|
|
||||||
|
// deps
|
||||||
|
const series = require('async/series');
|
||||||
|
const _ = require('lodash');
|
||||||
|
const async = require('async');
|
||||||
|
const moment = require('moment');
|
||||||
|
|
||||||
|
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 = this.nodeList[formData.value.node].node; // index from from -> node!
|
||||||
|
const message = _.get(formData.value, 'message', '').trim();
|
||||||
|
|
||||||
|
if(0 === renderStringLength(message)) {
|
||||||
|
return this.prevMenu(cb);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.createInterruptItem(message, (err, interruptItem) => {
|
||||||
|
if(-1 === nodeId) {
|
||||||
|
// ALL nodes
|
||||||
|
UserInterruptQueue.queue(interruptItem, { omit : this.client });
|
||||||
|
} else {
|
||||||
|
const conn = getConnectionByNodeId(nodeId);
|
||||||
|
if(conn) {
|
||||||
|
UserInterruptQueue.queue(interruptItem, { clients : conn } );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.prevMenu(cb);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
mciReady(mciData, cb) {
|
||||||
|
super.mciReady(mciData, err => {
|
||||||
|
if(err) {
|
||||||
|
return cb(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
series(
|
||||||
|
[
|
||||||
|
(callback) => {
|
||||||
|
return this.prepViewController('sendMessage', FormIds.sendMessage, mciData.menu, callback);
|
||||||
|
},
|
||||||
|
(callback) => {
|
||||||
|
return this.validateMCIByViewIds(
|
||||||
|
'sendMessage',
|
||||||
|
[ MciViewIds.sendMessage.nodeSelect, MciViewIds.sendMessage.message ],
|
||||||
|
callback
|
||||||
|
);
|
||||||
|
},
|
||||||
|
(callback) => {
|
||||||
|
const nodeSelectView = this.viewControllers.sendMessage.getView(MciViewIds.sendMessage.nodeSelect);
|
||||||
|
this.prepareNodeList();
|
||||||
|
|
||||||
|
nodeSelectView.on('index update', idx => {
|
||||||
|
this.nodeListSelectionIndexUpdate(idx);
|
||||||
|
});
|
||||||
|
|
||||||
|
nodeSelectView.setItems(this.nodeList);
|
||||||
|
nodeSelectView.redraw();
|
||||||
|
this.nodeListSelectionIndexUpdate(0);
|
||||||
|
return callback(null);
|
||||||
|
},
|
||||||
|
(callback) => {
|
||||||
|
const previewView = this.viewControllers.sendMessage.getView(MciViewIds.sendMessage.preview);
|
||||||
|
if(!previewView) {
|
||||||
|
return callback(null); // preview is optional
|
||||||
|
}
|
||||||
|
|
||||||
|
const messageView = this.viewControllers.sendMessage.getView(MciViewIds.sendMessage.message);
|
||||||
|
let timerId;
|
||||||
|
messageView.on('key press', () => {
|
||||||
|
clearTimeout(timerId);
|
||||||
|
const focused = this.viewControllers.sendMessage.getFocusedView();
|
||||||
|
if(focused === messageView) {
|
||||||
|
previewView.setText(messageView.getData());
|
||||||
|
focused.setFocus(true);
|
||||||
|
}
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
],
|
||||||
|
err => {
|
||||||
|
return cb(err);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
createInterruptItem(message, cb) {
|
||||||
|
const dateTimeFormat = this.config.dateTimeFormat || this.client.currentTheme.helpers.getDateTimeFormat();
|
||||||
|
|
||||||
|
const textFormatObj = {
|
||||||
|
fromUserName : this.client.user.username,
|
||||||
|
fromRealName : this.client.user.properties.real_name,
|
||||||
|
fromNodeId : this.client.node,
|
||||||
|
message : message,
|
||||||
|
timestamp : moment().format(dateTimeFormat),
|
||||||
|
};
|
||||||
|
|
||||||
|
const messageFormat =
|
||||||
|
this.config.messageFormat ||
|
||||||
|
'Message from {fromUserName} on node {fromNodeId}:\r\n{message}';
|
||||||
|
|
||||||
|
const item = {
|
||||||
|
text : stringFormat(messageFormat, textFormatObj),
|
||||||
|
pause : true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const getArt = (name, callback) => {
|
||||||
|
const spec = _.get(this.config, `art.${name}`);
|
||||||
|
if(!spec) {
|
||||||
|
return callback(null);
|
||||||
|
}
|
||||||
|
const getArtOpts = {
|
||||||
|
name : spec,
|
||||||
|
client : this.client,
|
||||||
|
random : false,
|
||||||
|
};
|
||||||
|
getThemeArt(getArtOpts, (err, artInfo) => {
|
||||||
|
// ignore errors
|
||||||
|
return callback(artInfo ? artInfo.data : null);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
async.waterfall(
|
||||||
|
[
|
||||||
|
(callback) => {
|
||||||
|
getArt('header', headerArt => {
|
||||||
|
return callback(null, headerArt);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
(headerArt, callback) => {
|
||||||
|
getArt('footer', footerArt => {
|
||||||
|
return callback(null, headerArt, footerArt);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
(headerArt, footerArt, callback) => {
|
||||||
|
if(headerArt || footerArt) {
|
||||||
|
item.contents = `${headerArt || ''}\r\n${pipeToAnsi(item.text)}\r\n${footerArt || ''}`;
|
||||||
|
}
|
||||||
|
return callback(null);
|
||||||
|
}
|
||||||
|
],
|
||||||
|
err => {
|
||||||
|
return cb(err, item);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
prepareNodeList() {
|
||||||
|
// standard node list with {text} field added for compliance
|
||||||
|
this.nodeList = [{
|
||||||
|
text : '-ALL-',
|
||||||
|
// dummy fields:
|
||||||
|
node : -1,
|
||||||
|
authenticated : false,
|
||||||
|
userId : 0,
|
||||||
|
action : 'N/A',
|
||||||
|
userName : 'Everyone',
|
||||||
|
realName : 'All Users',
|
||||||
|
location : 'N/A',
|
||||||
|
affils : 'N/A',
|
||||||
|
timeOn : 'N/A',
|
||||||
|
}].concat(getActiveConnectionList(true)
|
||||||
|
.map(node => Object.assign(node, { text : -1 == node.node ? '-ALL-' : 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);
|
||||||
|
}
|
||||||
|
};
|
|
@ -0,0 +1,98 @@
|
||||||
|
/* jslint node: true */
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// ENiGMA½
|
||||||
|
const Art = require('./art.js');
|
||||||
|
const {
|
||||||
|
getActiveConnections
|
||||||
|
} = require('./client_connections.js');
|
||||||
|
const ANSI = require('./ansi_term.js');
|
||||||
|
const { pipeToAnsi } = require('./color_codes.js');
|
||||||
|
|
||||||
|
// deps
|
||||||
|
const _ = require('lodash');
|
||||||
|
|
||||||
|
module.exports = class UserInterruptQueue
|
||||||
|
{
|
||||||
|
constructor(client) {
|
||||||
|
this.client = client;
|
||||||
|
this.queue = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
static queue(interruptItem, opts) {
|
||||||
|
opts = opts || {};
|
||||||
|
if(!opts.clients) {
|
||||||
|
let omitNodes = [];
|
||||||
|
if(Array.isArray(opts.omit)) {
|
||||||
|
omitNodes = opts.omit;
|
||||||
|
} else if(opts.omit) {
|
||||||
|
omitNodes = [ opts.omit ];
|
||||||
|
}
|
||||||
|
omitNodes = omitNodes.map(n => _.isNumber(n) ? n : n.node);
|
||||||
|
opts.clients = getActiveConnections(true).filter(ac => !omitNodes.includes(ac.node));
|
||||||
|
}
|
||||||
|
if(!Array.isArray(opts.clients)) {
|
||||||
|
opts.clients = [ opts.clients ];
|
||||||
|
}
|
||||||
|
opts.clients.forEach(c => {
|
||||||
|
c.interruptQueue.queueItem(interruptItem);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
queueItem(interruptItem) {
|
||||||
|
if(!_.isString(interruptItem.contents) && !_.isString(interruptItem.text)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// pause defaulted on
|
||||||
|
interruptItem.pause = _.get(interruptItem, 'pause', true);
|
||||||
|
|
||||||
|
this.client.currentMenuModule.attemptInterruptNow(interruptItem, (err, ateIt) => {
|
||||||
|
if(err) {
|
||||||
|
// :TODO: Log me
|
||||||
|
} else if(true !== ateIt) {
|
||||||
|
this.queue.push(interruptItem);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
hasItems() {
|
||||||
|
return this.queue.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
displayNext(options, cb) {
|
||||||
|
if(!cb && _.isFunction(options)) {
|
||||||
|
cb = options;
|
||||||
|
options = {};
|
||||||
|
}
|
||||||
|
const interruptItem = this.queue.pop();
|
||||||
|
if(!interruptItem) {
|
||||||
|
return cb(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.assign(interruptItem, options);
|
||||||
|
return interruptItem ? this.displayWithItem(interruptItem, cb) : cb(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
displayWithItem(interruptItem, cb) {
|
||||||
|
if(interruptItem.cls) {
|
||||||
|
this.client.term.rawWrite(ANSI.clearScreen());
|
||||||
|
} else {
|
||||||
|
this.client.term.rawWrite('\r\n\r\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
if(interruptItem.contents) {
|
||||||
|
Art.display(this.client, interruptItem.contents, err => {
|
||||||
|
if(err) {
|
||||||
|
return cb(err);
|
||||||
|
}
|
||||||
|
//this.client.term.rawWrite('\r\n\r\n'); // :TODO: Prob optional based on contents vs text
|
||||||
|
this.client.currentMenuModule.pausePrompt( () => {
|
||||||
|
return cb(null);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return this.client.term.write(pipeToAnsi(`${interruptItem.text}\r\n\r\n`, this.client), cb);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
|
@ -431,6 +431,10 @@ ViewController.prototype.getView = function(id) {
|
||||||
return this.views[id];
|
return this.views[id];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
ViewController.prototype.hasView = function(id) {
|
||||||
|
return this.getView(id) ? true : false;
|
||||||
|
}
|
||||||
|
|
||||||
ViewController.prototype.getViewsByMciCode = function(mciCode) {
|
ViewController.prototype.getViewsByMciCode = function(mciCode) {
|
||||||
if(!Array.isArray(mciCode)) {
|
if(!Array.isArray(mciCode)) {
|
||||||
mciCode = [ mciCode ];
|
mciCode = [ mciCode ];
|
||||||
|
|
|
@ -2,9 +2,9 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
// ENiGMA½
|
// ENiGMA½
|
||||||
const { MenuModule } = require('./menu_module.js');
|
const { MenuModule } = require('./menu_module.js');
|
||||||
const { getActiveNodeList } = require('./client_connections.js');
|
const { getActiveConnectionList } = require('./client_connections.js');
|
||||||
const { Errors } = require('./enig_error.js');
|
const { Errors } = require('./enig_error.js');
|
||||||
|
|
||||||
// deps
|
// deps
|
||||||
const async = require('async');
|
const async = require('async');
|
||||||
|
@ -43,7 +43,7 @@ exports.getModule = class WhosOnlineModule extends MenuModule {
|
||||||
return cb(Errors.MissingMci(`Missing online list MCI ${MciViewIds.onlineList}`));
|
return cb(Errors.MissingMci(`Missing online list MCI ${MciViewIds.onlineList}`));
|
||||||
}
|
}
|
||||||
|
|
||||||
const onlineList = getActiveNodeList(true).slice(0, onlineListView.height).map(
|
const onlineList = getActiveConnectionList(true).slice(0, onlineListView.height).map(
|
||||||
oe => Object.assign(oe, { text : oe.userName, timeOn : _.upperFirst(oe.timeOn.humanize()) })
|
oe => Object.assign(oe, { text : oe.userName, timeOn : _.upperFirst(oe.timeOn.humanize()) })
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -79,6 +79,7 @@
|
||||||
- [Download Manager]({{ site.baseurl }}{% link modding/file-base-download-manager.md %})
|
- [Download Manager]({{ site.baseurl }}{% link modding/file-base-download-manager.md %})
|
||||||
- [Web Download Manager]({{ site.baseurl }}{% link modding/file-base-web-download-manager.md %})
|
- [Web Download Manager]({{ site.baseurl }}{% link modding/file-base-web-download-manager.md %})
|
||||||
- [Set Newscan Date]({{ site.baseurl }}{% link modding/set-newscan-date.md %})
|
- [Set Newscan Date]({{ site.baseurl }}{% link modding/set-newscan-date.md %})
|
||||||
|
- [Node to Node Messaging]({{ site.baseurl }}{% link modding/node-msg.md %})
|
||||||
|
|
||||||
- Administration
|
- Administration
|
||||||
- [oputil]({{ site.baseurl }}{% link admin/oputil.md %})
|
- [oputil]({{ site.baseurl }}{% link admin/oputil.md %})
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
---
|
||||||
|
layout: page
|
||||||
|
title: User Interruptions
|
||||||
|
---
|
||||||
|
## User Interruptions
|
||||||
|
ENiGMA½ provides functionality to "interrupt" a user for various purposes such as a [node-to-node message](/docs/modding/node-msg.md). User interruptions can be queued and displayed at the next opportune time such as when switching to a new menu, or realtime if appropriate.
|
||||||
|
|
||||||
|
## Standard Menu Behavior
|
||||||
|
Standard menus control interruption by the `interrupt` config block option, which may be set to one of the following values:
|
||||||
|
* `never`: Never interrupt the user when on this menu.
|
||||||
|
* `queued`: Queue interrupts for the next opportune time. Any queued message(s) will then be shown. This is the default.
|
||||||
|
* `realtime`: If possible, display messages in realtime. That is, show them right away. Standard menus that do not override default behavior will show the message then reload.
|
||||||
|
|
||||||
|
|
||||||
|
## See Also
|
||||||
|
See [user_interrupt_queue.js](/core/user_interrupt_queue.js) as well as usage within [menu_module.js](/core/menu_module.js).
|
||||||
|
|
|
@ -0,0 +1,41 @@
|
||||||
|
---
|
||||||
|
layout: page
|
||||||
|
title: Node to Node Messaging
|
||||||
|
---
|
||||||
|
## The Node to Node Messaging Module
|
||||||
|
The node to node messaging (`node_msg`) module allows users to send messages to one or more users on different nodes. Messages delivered to nodes follow standard [User Interruption](/docs/misc/user-interrupt.md) rules.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
### Config Block
|
||||||
|
Available `config` block entries:
|
||||||
|
* `dateTimeFormat`: [moment.js](https://momentjs.com) style format. Defaults to current theme → system `short` format.
|
||||||
|
* `messageFormat`: Format string for sent messages. Defaults to `Message from {fromUserName} on node {fromNodeId}:\r\n{message}`. The following format object members are available:
|
||||||
|
* `fromUserName`: Username who sent the message.
|
||||||
|
* `fromRealName`: Real name of user who sent the message.
|
||||||
|
* `fromNodeId`: Node ID where the message was sent from.
|
||||||
|
* `message`: User entered message. May contain pipe color codes.
|
||||||
|
* `timestamp`: A timestamp formatted using `dateTimeFormat` above.
|
||||||
|
* `art`: Block containing:
|
||||||
|
* `header`: Art spec for header to display with message.
|
||||||
|
* `footer`: Art spec for footer to display with message.
|
||||||
|
|
||||||
|
## Theming
|
||||||
|
### MCI Codes
|
||||||
|
1. Node selection. Must be a View that allows lists such as `SpinnerMenuView` (`%SM1`), `HorizontalMenuView` (`%HM1`), etc.
|
||||||
|
2. Message entry (`%ET2`).
|
||||||
|
3. Message preview (`%TL3`). A rendered (that is, pipe codes resolved) preview of the text in `%ET2`.
|
||||||
|
|
||||||
|
10+: Custom using `itemFormat`. See below.
|
||||||
|
|
||||||
|
### Item Format
|
||||||
|
The following `itemFormat` object is provided for MCI 1 and 10+ for the currently selected item/node:
|
||||||
|
* `text`: Node ID or "-ALL-" (All nodes).
|
||||||
|
* `node`: Node ID or `-1` in the case of all nodes.
|
||||||
|
* `userId`: User ID.
|
||||||
|
* `action`: User's action.
|
||||||
|
* `userName`: Username.
|
||||||
|
* `realName`: Real name.
|
||||||
|
* `location`: User's location.
|
||||||
|
* `affils`: Affiliations.
|
||||||
|
* `timeOn`: How long the user has been online (approx).
|
||||||
|
|
|
@ -991,8 +991,13 @@
|
||||||
prompt: menuCommand
|
prompt: menuCommand
|
||||||
config: {
|
config: {
|
||||||
font: cp437
|
font: cp437
|
||||||
|
interrupt: realtime
|
||||||
}
|
}
|
||||||
submit: [
|
submit: [
|
||||||
|
{
|
||||||
|
value: { command: "MSG" }
|
||||||
|
action: @menu:nodeMessage
|
||||||
|
}
|
||||||
{
|
{
|
||||||
value: { command: "G" }
|
value: { command: "G" }
|
||||||
action: @menu:fullLogoffSequence
|
action: @menu:fullLogoffSequence
|
||||||
|
@ -1064,6 +1069,46 @@
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
nodeMessage: {
|
||||||
|
desc: Node Messaging
|
||||||
|
module: node_msg
|
||||||
|
art: NODEMSG
|
||||||
|
config: {
|
||||||
|
cls: true
|
||||||
|
art: {
|
||||||
|
header: NODEMSGHDR
|
||||||
|
footer: NODEMSGFTR
|
||||||
|
}
|
||||||
|
}
|
||||||
|
form: {
|
||||||
|
0: {
|
||||||
|
mci: {
|
||||||
|
SM1: {
|
||||||
|
argName: node
|
||||||
|
}
|
||||||
|
ET2: {
|
||||||
|
argName: message
|
||||||
|
submit: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
actionKeys: [
|
||||||
|
{
|
||||||
|
keys: [ "escape" ]
|
||||||
|
action: @systemMethod:prevMenu
|
||||||
|
}
|
||||||
|
]
|
||||||
|
submit: {
|
||||||
|
*: [
|
||||||
|
{
|
||||||
|
value: { message: null }
|
||||||
|
action: @method:sendMessage
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
mainMenuLastCallers: {
|
mainMenuLastCallers: {
|
||||||
desc: Last Callers
|
desc: Last Callers
|
||||||
module: last_callers
|
module: last_callers
|
||||||
|
@ -1609,6 +1654,9 @@
|
||||||
desc: Doors Menu
|
desc: Doors Menu
|
||||||
art: DOORMNU
|
art: DOORMNU
|
||||||
prompt: menuCommand
|
prompt: menuCommand
|
||||||
|
config: {
|
||||||
|
interrupt: realtime
|
||||||
|
}
|
||||||
submit: [
|
submit: [
|
||||||
{
|
{
|
||||||
value: { command: "G" }
|
value: { command: "G" }
|
||||||
|
@ -1738,6 +1786,9 @@
|
||||||
art: MSGMNU
|
art: MSGMNU
|
||||||
desc: Message Area
|
desc: Message Area
|
||||||
prompt: messageMenuCommand
|
prompt: messageMenuCommand
|
||||||
|
config: {
|
||||||
|
interrupt: realtime
|
||||||
|
}
|
||||||
submit: [
|
submit: [
|
||||||
{
|
{
|
||||||
value: { command: "P" }
|
value: { command: "P" }
|
||||||
|
@ -2464,6 +2515,9 @@
|
||||||
art: MAILMNU
|
art: MAILMNU
|
||||||
desc: Mail Menu
|
desc: Mail Menu
|
||||||
prompt: menuCommand
|
prompt: menuCommand
|
||||||
|
config: {
|
||||||
|
interrupt: realtime
|
||||||
|
}
|
||||||
submit: [
|
submit: [
|
||||||
{
|
{
|
||||||
value: { command: "C" }
|
value: { command: "C" }
|
||||||
|
@ -2666,6 +2720,9 @@
|
||||||
desc: File Base
|
desc: File Base
|
||||||
art: FMENU
|
art: FMENU
|
||||||
prompt: fileMenuCommand
|
prompt: fileMenuCommand
|
||||||
|
config: {
|
||||||
|
interrupt: realtime
|
||||||
|
}
|
||||||
submit: [
|
submit: [
|
||||||
{
|
{
|
||||||
value: { menuOption: "L" }
|
value: { menuOption: "L" }
|
||||||
|
|
Loading…
Reference in New Issue