User interrupts & node module ready to rock. ...maybe with bugs?

This commit is contained in:
Bryan Ashby 2018-11-30 23:20:44 -07:00
parent d9238ee6a5
commit fe44f2c4d6
9 changed files with 198 additions and 68 deletions

View File

@ -364,15 +364,21 @@
} }
nodeMessage: { nodeMessage: {
config: {
messageFormat: "|00|08 :: |03message from |11{fromUserName} |08/ |03node |11{fromNodeId}|08 @ |11{timestamp} |08::\r\n|07 {message}"
}
0: { 0: {
mci: { mci: {
SM1: { SM1: {
width: 22 width: 25
itemFormat: "|00|07{text} |08(|07{userName}|08)" itemFormat: "|00|03node |07{text} |08(|07{userName}|08)"
focusItemFormat: "|00|15{text} |07(|15{userName}|07)" focusItemFormat: "|00|11node |15{text} |07(|15{userName}|07)"
} }
ET2: { ET2: {
width: 70 width: 65
}
TL3: {
width: 65
} }
} }
} }

View File

@ -29,9 +29,15 @@ exports.MenuModule = class MenuModule extends PluginModule {
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.viewControllers = {}; this.viewControllers = {};
this.interrupt = (_.get(this.menuConfig.config, 'interrupt', MenuModule.InterruptTypes.Queued)).toLowerCase();
}
// *initial* Interruptible state for this menu static get InterruptTypes() {
this.disableInterruption(); return {
Never : 'never',
Queued : 'queued',
Realtime : 'realtime',
};
} }
enter() { enter() {
@ -55,7 +61,7 @@ exports.MenuModule = class MenuModule extends PluginModule {
async.series( async.series(
[ [
function beforeArtInterrupt(callback) { function beforeArtInterrupt(callback) {
return self.toggleInterruptionAndDisplayQueued(callback); return self.displayQueuedInterruptions(callback);
}, },
function beforeDisplayArt(callback) { function beforeDisplayArt(callback) {
return self.beforeArt(callback); return self.beforeArt(callback);
@ -166,53 +172,38 @@ exports.MenuModule = class MenuModule extends PluginModule {
// nothing in base // nothing in base
} }
neverInterruptable() {
return this.menuConfig.config.Interruptible === 'never';
}
enableInterruption() {
if(!this.neverInterruptable()) {
this.Interruptible = true;
}
}
disableInterruption() {
if(!this.neverInterruptable()) {
this.Interruptible = false;
}
}
toggleInterruptionAndDisplayQueued(cb) {
this.enableInterruption();
this.displayQueuedInterruptions( () => {
this.disableInterruption();
return cb(null);
});
}
displayQueuedInterruptions(cb) { displayQueuedInterruptions(cb) {
if(true !== this.Interruptible) { if(MenuModule.InterruptTypes.Never === this.interrupt) {
return cb(null); return cb(null);
} }
let opts = { cls : true }; // clear screen for first message
async.whilst( async.whilst(
() => this.client.interruptQueue.hasItems(), () => this.client.interruptQueue.hasItems(),
next => this.client.interruptQueue.displayNext(next), next => {
this.client.interruptQueue.displayNext(opts, err => {
opts = {};
return next(err);
});
},
err => { err => {
return cb(err); return cb(err);
} }
) );
} }
attemptInterruptNow(interruptItem, cb) { attemptInterruptNow(interruptItem, cb) {
if(true !== this.Interruptible) { if(MenuModule.InterruptTypes.Realtime !== this.interrupt) {
return cb(null, false); // don't eat up the item; queue for later return cb(null, false); // don't eat up the item; queue for later
} }
// //
// Default impl: clear screen -> standard display -> reload menu // Default impl: clear screen -> standard display -> reload menu
// //
this.client.interruptQueue.displayWithItem(Object.assign({}, interruptItem, { cls : true }), err => { this.client.interruptQueue.displayWithItem(
Object.assign({}, interruptItem, { cls : true }),
err => {
if(err) { if(err) {
return cb(err, false); return cb(err, false);
} }
@ -308,7 +299,7 @@ exports.MenuModule = class MenuModule extends PluginModule {
} else { } else {
return this.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()) {
@ -318,8 +309,6 @@ exports.MenuModule = class MenuModule extends PluginModule {
} else { } else {
return gotoNextMenu(); return gotoNextMenu();
} }
} else {
this.enableInterruption();
} }
} }

View File

@ -3,7 +3,6 @@
// ENiGMA½ // ENiGMA½
const { MenuModule } = require('./menu_module.js'); const { MenuModule } = require('./menu_module.js');
const { Errors } = require('./enig_error.js');
const { const {
getActiveConnectionList, getActiveConnectionList,
getConnectionByNodeId, getConnectionByNodeId,
@ -12,6 +11,7 @@ const UserInterruptQueue = require('./user_interrupt_queue.js');
const { getThemeArt } = require('./theme.js'); const { getThemeArt } = require('./theme.js');
const { pipeToAnsi } = require('./color_codes.js'); const { pipeToAnsi } = require('./color_codes.js');
const stringFormat = require('./string_format.js'); const stringFormat = require('./string_format.js');
const { renderStringLength } = require('./string_util.js');
// deps // deps
const series = require('async/series'); const series = require('async/series');
@ -47,23 +47,27 @@ exports.getModule = class NodeMessageModule extends MenuModule {
this.menuMethods = { this.menuMethods = {
sendMessage : (formData, extraArgs, cb) => { sendMessage : (formData, extraArgs, cb) => {
const nodeId = this.nodeList[formData.value.node].node; // index from from -> node! const nodeId = this.nodeList[formData.value.node].node; // index from from -> node!
const message = formData.value.message; const message = _.get(formData.value, 'message', '').trim();
if(0 === renderStringLength(message)) {
return this.prevMenu(cb);
}
this.createInterruptItem(message, (err, interruptItem) => { this.createInterruptItem(message, (err, interruptItem) => {
if(-1 === nodeId) { if(-1 === nodeId) {
// ALL nodes // ALL nodes
UserInterruptQueue.queueGlobalOtherActive(interruptItem, this.client); UserInterruptQueue.queue(interruptItem, { omit : this.client });
} else { } else {
const conn = getConnectionByNodeId(nodeId); const conn = getConnectionByNodeId(nodeId);
if(conn) { if(conn) {
UserInterruptQueue.queueGlobal(interruptItem, [ conn ]); UserInterruptQueue.queue(interruptItem, { clients : conn } );
} }
} }
return this.prevMenu(cb); return this.prevMenu(cb);
}); });
}, },
} };
} }
mciReady(mciData, cb) { mciReady(mciData, cb) {
@ -123,12 +127,14 @@ exports.getModule = class NodeMessageModule extends MenuModule {
} }
createInterruptItem(message, cb) { createInterruptItem(message, cb) {
const dateTimeFormat = this.config.dateTimeFormat || this.client.currentTheme.helpers.getDateTimeFormat();
const textFormatObj = { const textFormatObj = {
fromUserName : this.client.user.username, fromUserName : this.client.user.username,
fromRealName : this.client.user.properties.real_name, fromRealName : this.client.user.properties.real_name,
fromNodeId : this.client.node, fromNodeId : this.client.node,
message : message, message : message,
timestamp : moment(), timestamp : moment().format(dateTimeFormat),
}; };
const messageFormat = const messageFormat =
@ -208,4 +214,4 @@ exports.getModule = class NodeMessageModule extends MenuModule {
} }
this.updateCustomViewTextsWithFilter('sendMessage', MciViewIds.sendMessage.customRangeStart, node); this.updateCustomViewTextsWithFilter('sendMessage', MciViewIds.sendMessage.customRangeStart, node);
} }
} };

View File

@ -19,16 +19,24 @@ module.exports = class UserInterruptQueue
this.queue = []; this.queue = [];
} }
static queueGlobal(interruptItem, connections) { static queue(interruptItem, opts) {
connections.forEach(conn => { opts = opts || {};
conn.interruptQueue.queueItem(interruptItem); 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);
// common shortcut: queue global, all active clients minus |client| opts.clients = getActiveConnections(true).filter(ac => !omitNodes.includes(ac.node));
static queueGlobalOtherActive(interruptItem, client) { }
const otherConnections = getActiveConnections(true).filter(ac => ac.node !== client.node); if(!Array.isArray(opts.clients)) {
return UserInterruptQueue.queueGlobal(interruptItem, otherConnections ); opts.clients = [ opts.clients ];
}
opts.clients.forEach(c => {
c.interruptQueue.queueItem(interruptItem);
});
} }
queueItem(interruptItem) { queueItem(interruptItem) {
@ -52,12 +60,17 @@ module.exports = class UserInterruptQueue
return this.queue.length > 0; return this.queue.length > 0;
} }
displayNext(cb) { displayNext(options, cb) {
if(!cb && _.isFunction(options)) {
cb = options;
options = {};
}
const interruptItem = this.queue.pop(); const interruptItem = this.queue.pop();
if(!interruptItem) { if(!interruptItem) {
return cb(null); return cb(null);
} }
Object.assign(interruptItem, options);
return interruptItem ? this.displayWithItem(interruptItem, cb) : cb(null); return interruptItem ? this.displayWithItem(interruptItem, cb) : cb(null);
} }

View File

@ -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 %})

View File

@ -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).

41
docs/modding/node-msg.md Normal file
View File

@ -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).

View File

@ -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" }