/* This is a message reader/lister door for Synchronet. Features include: * - Listing messages in the user's current message area with the ability to * navigate forwards & backwards through the list (and for ANSI users, a * lightbar interface will be used, or optionally can be set to use a more * traditional interface for ANSI users) * - The user can select a message from the list to read and optionally reply to * - For ANSI users, reading messages is done with an enhanced user interface, * with the ability to scroll up & down through the message, move to the next * or previous message using the right & left arrow keys, display the message * list to choose another message to read, etc. * - The ability to start up with the message list or reading messages in the * user's current message area (AKA sub-board) * - Message searching * * Author: Eric Oulashin (AKA Nightfox) * BBS: Digital Distortion * BBS address: digitaldistortionbbs.com (or digdist.bbsindex.com) * * Date Author Description * 2014-09-13 Eric Oulashin Started (based on my message lister script) * 2015-05-06 Eric Oulashin Version 1.0 * Finally releasing it, as it seems fairly stable * and has the basic features implemented. * 2015-05-17 Eric Oulashin Version 1.01 * Bug fix: Updated the setting of the enhanced reader * header width to use the longest line in the header * (rather than the length of only the first line) to * ensure that the header displays correctly. * 2015-06-10 Eric Oulashin Version 1.02 * Updated the version to reflect a bug fix in * DDScanMsgs.js. No change to the actual reader. * 2015-07-11 Eric Oulashin Version 1.03 Beta * Started looking into & fixing an issue in Linux * where after replying to a message, the number of * messages was not immediately refreshed, so for * instance, replying to the last message in read * mode, the reader would not be able to navigate * to the next message without first going to the * previous message. * 2015-07-12 Eric Oulashin Version 1.03 * Releasing this version after having done more testing. * 2015-07-19 Eric Oulashin Version 1.04 Beta * Updated to pause (wait for user keypress) after saving * a reply message to allow the user to see Synchronet's * success/fail message on saving a message. * 2015-08-09 Eric Oulashin Adding the ability for the sysop to save a message * to a file on the BBS machine * 2015-09-07 Eric Oulashin Updated so that in lightbar mode, pressing PageDown * on the last page will go to the last message, and * pressing PageUp on the first page will go to the * first message. Also, in the enhanced reader mode, * added a console pause after posting a message in * the sub-board so that the user can see the info * screen that Synchronet displays after saving a * message. * 2015-09-19 Eric Oulashin Started working on adding the ability to download file * attachments. Started working on a new function, * determineMsgAttachments(), which can parse message text * to save any base64-encoded attachments that might be * present, and also to check the message subject for a * filename for a file uploaded to the user's inbox. * 2015-10-10 Eric Oulashin Version 1.04 * Releasing this version after development & testing, * since attachment downloading and the other new features * seem to be working fairly well. * 2015-10-25 Eric Oulashin Version 1.05 Beta * Started updating the reader to display more header & * kludge lines. * 2015-10-28 Eric Oulashin Started working on updating the ANSIAttrsToSyncAttrs() * function to use Synchronet's ans2asc tool to convert * from ANSI to Synchronet codes, to get ANSI messsages * to look better. * 2015-11-07 Eric Oulashin Expanded the list of @-codes interpreted for message * headers. Also, renamed that method to ParseMsgAtCodes(). * Updated the ReadMessageEnhanced method to interpret * @-codes, but only when reading personal mail, to avoid * weird behavior on message networks from malicious users * on other BBSes. * 2015-11-24 Eric Oulashin Started working on using the Frame class (in frame.js) * to display messages with ANSI codes using a scrollable * user interface * 2015-12-06 Eric Oulashin Version 1.05 * Officially releasing this version, as it seems to be * fairly stable after testing. * 2015-12-10 Eric Oulashin Version 1.06 beta * Bug fix: The scriptFilename command-line argument was * not being referenced properly in the DigDistMsgReader * constructor; that has been fixed. * 2015-12-11 Eric Oulashin Updated DigDistMsgReader_MessageAreaScan() so that * the current sub-board newscan functionality can make * use of the -subBoard command-line option to scan a * specific sub-board, which may be different than the * user's current sub-board. * Bug fix: Updated DigDistMsgReader_MessageAreaScan() * to temporarily set bbs.curgrp and bbs.cursub so that * all @-codes for the sub-boards will be displayed * correctly by Synchronet. * 2015-12-12 Eric Oulashin Added a new configuration options, pauseAfterNewMsgScan, * which specifies whether or not to pause after doing * a new message scan. * 2015-12-13 Eric Oulashin Version 1.06 * Releasing this version after testing showed it's * working as expected * 2015-12-19 Eric Oulashin Version 1.07 Beta * Started working on a way of tagging message (i.e., to * do a batch delete). * 2015-12-24 Eric Oulashin Version 1.07 * Releasing this version, as it seems to be working as * it should after testing & development. */ /* Command-line arguments (in -arg=val format, or -arg format to enable an option): -search: A search type. Available options: keyword_search: Do a keyword search in message subject/body text (current message area) from_name_search: 'From' name search (current message area) to_name_search: 'To' name search (current message area) to_user_search: To user search (current message area) new_msg_scan: New message scan (prompt for current sub-board, current group, or all) new_msg_scan_all: New message scan (all sub-boards) new_msg_scan_cur_grp: New message scan (current message group only) new_msg_scan_cur_sub: New message scan (current sub-board only). This can (optionally) be used with the -subBoard command-line parameter, which specifies an internal code for a sub-board, which may be different from the user's currently selected sub-board. to_user_new_scan: Scan for new (unread) messages to the user (prompt for current sub-board, current group, or all) to_user_new_scan_all: Scan for new (unread) messages to the user (all sub-boards) to_user_new_scan_cur_grp: Scan for new (unread) messages to the user (current group) to_user_new_scan_cur_sub: Scan for new (unread) messages to the user (current sub-board) to_user_all_scan: Scan for all messages to the user (prompt for current sub-board, current group, or all) prompt: Prompt the user for one of several search/scan options to choose from Note that if the -personalEmail option is specified (to read personal email), the only valid search types are keyword_search and from_name_search. -suppressSearchTypeText: Disable the search type text that would appear above searches or scans (such as "New To You Message Scan", etc.) -startMode: Startup mode. Available options: list (or lister): Message list mode read (or reader): Message read mode -configFilename: Specifies the name of the configuration file to use. Defaults to DDMsgReader.cfg. -personalEmail: Read personal email to the user. This is a true/false value. It doesn't need to explicitly have a =true or =false afterward; simply including -personalEmail will enable it. If this is specified, the -chooseAreaFirst and -subBoard options will be ignored. -personalEmailSent: Read personal email to the user. This is a true/false value. It doesn't need to explicitly have a =true or =false afterward; simply including -personalEmailSent will enable it. -allPersonalEmail: Read all personal email (to/from all) -userNum: Specify a user number (for the personal email options) -chooseAreaFirst: Display the message area chooser before reading/listing messages. This is a true/false value. It doesn't need to explicitly have a =true or =false afterward; simply including -chooseAreaFirst will enable it. If -personalEmail or -subBoard is specified, then this option won't have any effect. -subBoard: The sub-board (internal code or number) to read, other than the user's current sub-board. If this is specified, the -chooseAreaFirst option will be ignored. -verboseLogging: Enable logging to the system log & node log. Currently, there isn't much that will be logged, but more log messages could be added in the future. */ // TODO: // - Idea for future release: Add some extra functionality to the enhanced // reader interface (perhaps accessible on their own small menu or via // CTRL hotkeys): // - Forward the current message (to username, user #, internet email address, // FTN email address, QWK email address, etc.) // - Save the message to the BBS computer (sysop only) load("sbbsdefs.js"); load("text.js"); // Text string definitions (referencing text.dat) // This script requires Synchronet version 3.15 or higher. // Exit if the Synchronet version is below the minimum. if (system.version_num < 31500) { var message = "\1n\1h\1y\1i* Warning:\1n\1h\1w Digital Distortion Message Reader " + "requires version \1g3.15\1w or\r\n" + "higher of Synchronet. This BBS is using version \1g" + system.version + "\1w. Please notify the sysop."; console.crlf(); console.print(message); console.crlf(); console.pause(); exit(); } // Reader version information var READER_VERSION = "1.07"; var READER_DATE = "2015-12-24"; // Keyboard key codes for displaying on the screen var UP_ARROW = ascii(24); var DOWN_ARROW = ascii(25); var LEFT_ARROW = ascii(17); var RIGHT_ARROW = ascii(16); // PageUp & PageDown keys - Not real key codes, but codes defined // to be used & recognized in this script var KEY_PAGE_UP = "\1PgUp"; var KEY_PAGE_DOWN = "\1PgDn"; // Ctrl keys for input var CTRL_A = "\x01"; var CTRL_B = "\x02"; var CTRL_C = "\x03"; var CTRL_D = "\x04"; var CTRL_E = "\x05"; var CTRL_F = "\x06"; var CTRL_G = "\x07"; var BEEP = CTRL_G; var CTRL_H = "\x08"; var BACKSPACE = CTRL_H; var CTRL_I = "\x09"; var TAB = CTRL_I; var CTRL_J = "\x0a"; var CTRL_K = "\x0b"; var CTRL_L = "\x0c"; var CTRL_M = "\x0d"; var CTRL_N = "\x0e"; var CTRL_O = "\x0f"; var CTRL_P = "\x10"; var CTRL_Q = "\x11"; var XOFF = CTRL_Q; var CTRL_R = "\x12"; var CTRL_S = "\x13"; var XON = CTRL_S; var CTRL_T = "\x14"; var CTRL_U = "\x15"; var CTRL_V = "\x16"; var KEY_INSERT = CTRL_V; var CTRL_W = "\x17"; var CTRL_X = "\x18"; var CTRL_Y = "\x19"; var CTRL_Z = "\x1a"; //var KEY_ESC = "\x1b"; var KEY_ESC = ascii(27); var KEY_ENTER = CTRL_M; // These are defined in sbbsdefs.js: //var KEY_UP ='\x1e'; // ctrl-^ (up arrow) //var KEY_DOWN ='\x0a'; // ctrl-j (dn arrow) //var KEY_RIGHT ='\x06'; // ctrl-f (rt arrow) //var KEY_LEFT ='\x1d'; // ctrl-] (lf arrow) //var KEY_HOME ='\x02'; // ctrl-b (home) //var KEY_END ='\x05'; // ctrl-e (end) //var KEY_DEL ='\x7f'; // (del) // Characters for display // Box-drawing/border characters: Single-line var UPPER_LEFT_SINGLE = "�"; var HORIZONTAL_SINGLE = "�"; var UPPER_RIGHT_SINGLE = "�"; var VERTICAL_SINGLE = "�"; var LOWER_LEFT_SINGLE = "�"; var LOWER_RIGHT_SINGLE = "�"; var T_SINGLE = "�"; var LEFT_T_SINGLE = "�"; var RIGHT_T_SINGLE = "�"; var BOTTOM_T_SINGLE = "�"; var CROSS_SINGLE = "�"; // Box-drawing/border characters: Double-line var UPPER_LEFT_DOUBLE = "�"; var HORIZONTAL_DOUBLE = "�"; var UPPER_RIGHT_DOUBLE = "�"; var VERTICAL_DOUBLE = "�"; var LOWER_LEFT_DOUBLE = "�"; var LOWER_RIGHT_DOUBLE = "�"; var T_DOUBLE = "�"; var LEFT_T_DOUBLE = "�"; var RIGHT_T_DOUBLE = "�"; var BOTTOM_T_DOUBLE = "�"; var CROSS_DOUBLE = "�"; // Box-drawing/border characters: Vertical single-line with horizontal double-line var UPPER_LEFT_VSINGLE_HDOUBLE = "�"; var UPPER_RIGHT_VSINGLE_HDOUBLE = "�"; var LOWER_LEFT_VSINGLE_HDOUBLE = "�"; var LOWER_RIGHT_VSINGLE_HDOUBLE = "�"; // Other special characters var DOT_CHAR = "�"; var CHECK_CHAR = "�"; var THIN_RECTANGLE_LEFT = "�"; var THIN_RECTANGLE_RIGHT = "�"; var BLOCK1 = "�"; // Dimmest block var BLOCK2 = "�"; var BLOCK3 = "�"; var BLOCK4 = "�"; // Brightest block const ERROR_PAUSE_WAIT_MS = 1500; // gIsSysop stores whether or not the user is a sysop. var gIsSysop = user.compare_ars("SYSOP"); // Whether or not the user is a sysop // Store whether or not the Synchronet compile date is at least May 12, 2013 // so that we don't have to call compileDateAtLeast2013_05_12() multiple times. var gSyncCompileDateAtLeast2013_05_12 = compileDateAtLeast2013_05_12(); // Reader mode definitions: const READER_MODE_LIST = 0; const READER_MODE_READ = 1; // Search types const SEARCH_NONE = -1; const SEARCH_KEYWORD = 2; const SEARCH_FROM_NAME = 3; const SEARCH_TO_NAME_CUR_MSG_AREA = 4; const SEARCH_TO_USER_CUR_MSG_AREA = 5; const SEARCH_MSG_NEWSCAN = 6; const SEARCH_MSG_NEWSCAN_CUR_SUB = 7; const SEARCH_MSG_NEWSCAN_CUR_GRP = 8; const SEARCH_MSG_NEWSCAN_ALL = 9; const SEARCH_TO_USER_NEW_SCAN = 10; const SEARCH_TO_USER_NEW_SCAN_CUR_SUB = 11; const SEARCH_TO_USER_NEW_SCAN_CUR_GRP = 12; const SEARCH_TO_USER_NEW_SCAN_ALL = 13; const SEARCH_ALL_TO_USER_SCAN = 14; // Message threading types const THREAD_BY_ID = 15; const THREAD_BY_TITLE = 16; const THREAD_BY_AUTHOR = 17; const THREAD_BY_TO_USER = 18; // Reader mode - Actions const ACTION_NONE = 19; const ACTION_GO_NEXT_MSG = 20; const ACTION_GO_PREVIOUS_MSG = 21; const ACTION_GO_SPECIFIC_MSG = 22; const ACTION_GO_FIRST_MSG = 23; const ACTION_GO_LAST_MSG = 24; const ACTION_DISPLAY_MSG_LIST = 25; const ACTION_CHG_MSG_AREA = 26; const ACTION_GO_PREV_MSG_AREA = 27; const ACTION_GO_NEXT_MSG_AREA = 28; const ACTION_QUIT = 29; // Strings for the various message attributes (used by makeAllAttrStr(), // makeMainMsgAttrStr(), makeAuxMsgAttrStr(), and makeNetMsgAttrStr()) var gMainMsgAttrStrs = new Object(); gMainMsgAttrStrs[MSG_DELETE] = "Del"; gMainMsgAttrStrs[MSG_PRIVATE] = "Priv"; gMainMsgAttrStrs[MSG_READ] = "Read"; gMainMsgAttrStrs[MSG_PERMANENT] = "Perm"; gMainMsgAttrStrs[MSG_LOCKED] = "Lock"; gMainMsgAttrStrs[MSG_ANONYMOUS] = "Anon"; gMainMsgAttrStrs[MSG_KILLREAD] = "Killread"; gMainMsgAttrStrs[MSG_MODERATED] = "Mod"; gMainMsgAttrStrs[MSG_VALIDATED] = "Valid"; gMainMsgAttrStrs[MSG_REPLIED] = "Repl"; gMainMsgAttrStrs[MSG_NOREPLY] = "NoRepl"; var gAuxMsgAttrStrs = new Object(); gAuxMsgAttrStrs[MSG_FILEREQUEST] = "Freq"; gAuxMsgAttrStrs[MSG_FILEATTACH] = "Attach"; gAuxMsgAttrStrs[MSG_TRUNCFILE] = "TruncFile"; gAuxMsgAttrStrs[MSG_KILLFILE] = "KillFile"; gAuxMsgAttrStrs[MSG_RECEIPTREQ] = "RctReq"; gAuxMsgAttrStrs[MSG_CONFIRMREQ] = "ConfReq"; gAuxMsgAttrStrs[MSG_NODISP] = "NoDisp"; var gNetMsgAttrStrs = new Object(); gNetMsgAttrStrs[MSG_LOCAL] = "FromLocal"; gNetMsgAttrStrs[MSG_INTRANSIT] = "Transit"; gNetMsgAttrStrs[MSG_SENT] = "Sent"; gNetMsgAttrStrs[MSG_KILLSENT] = "KillSent"; gNetMsgAttrStrs[MSG_ARCHIVESENT] = "ArcSent"; gNetMsgAttrStrs[MSG_HOLD] = "Hold"; gNetMsgAttrStrs[MSG_CRASH] = "Crash"; gNetMsgAttrStrs[MSG_IMMEDIATE] = "Now"; gNetMsgAttrStrs[MSG_DIRECT] = "Direct"; gNetMsgAttrStrs[MSG_GATE] = "Gate"; gNetMsgAttrStrs[MSG_ORPHAN] = "Orphan"; gNetMsgAttrStrs[MSG_FPU] = "FPU"; gNetMsgAttrStrs[MSG_TYPELOCAL] = "ForLocal"; gNetMsgAttrStrs[MSG_TYPEECHO] = "ForEcho"; gNetMsgAttrStrs[MSG_TYPENET] = "ForNetmail"; // Determine the script's startup directory. // This code is a trick that was created by Deuce, suggested by Rob Swindell // as a way to detect which directory the script was executed in. I've // shortened the code a little. var gStartupPath = '.'; try { throw dig.dist(dist); } catch(e) { gStartupPath = e.fileName; } gStartupPath = backslash(gStartupPath.replace(/[\/\\][^\/\\]*$/,'')); // See if we're running in Windows or not. Until early 2015, the word_wrap() // function seemed to have a bug where the wrapping length in Linux was one // less than what it uses in Windows). That seemed to be fixed in one of the // Synchronet 3.16 builds in early 2015. var gRunningInWindows = /^WIN/.test(system.platform.toUpperCase()); // Temporary directory (in the logged-in user's node directory) to store // file attachments, etc. var gFileAttachDir = backslash(system.node_dir + "DDMsgReader_Attachments"); // If the temporary attachments directory exists, then delete it (in case the last // user hung up while running this script, etc.) if (file_exists(gFileAttachDir)) deltree(gFileAttachDir); // See if frame.js and scrollbar.js exist in sbbs/exec/load on the BBS machine. // If so, load them. They will be used for displaying messages with ANSI content // with a scrollable user interface. var gFrameJSAvailable = file_exists(backslash(system.exec_dir) + "load/frame.js"); if (gFrameJSAvailable) load("frame.js"); var gScrollbarJSAvailable = file_exists(backslash(system.exec_dir) + "load/scrollbar.js"); if (gScrollbarJSAvailable) load("scrollbar.js"); ///////////////////////////////////////////// // Script execution code // Parse the command-line arguments var gCmdLineArgVals = parseArgs(argv); var gAllPersonalEmailOptSpecified = (gCmdLineArgVals.hasOwnProperty("allpersonalemail") && gCmdLineArgVals.allpersonalemail); // Check to see if the command-line argument for reading personal email is enabled var gListPersonalEmailCmdLineOpt = ((gCmdLineArgVals.hasOwnProperty("personalemail") && gCmdLineArgVals.personalemail) || (gCmdLineArgVals.hasOwnProperty("personalemailsent") && gCmdLineArgVals.personalemailsent) || gAllPersonalEmailOptSpecified); // If the command-line parameter "search" is specified as "prompt", then // prompt the user for the type of search to perform. var gDoDDMR = true; // If the user doesn't choose a search type, this will be set to false if (gCmdLineArgVals.hasOwnProperty("search") && (gCmdLineArgVals["search"].toLowerCase() == "prompt")) { console.print("\1n"); console.crlf(); console.print("\1cMessage search:"); console.crlf(); var allowedKeys = ""; if (!gListPersonalEmailCmdLineOpt) { allowedKeys = "ANKFTYUS"; console.print(" \1g\1hN\1y = \1n\1cNew message scan"); console.crlf(); console.print(" \1g\1hK\1y = \1n\1cKeyword"); console.crlf(); console.print(" \1h\1gF\1y = \1n\1cFrom name"); console.crlf(); console.print(" \1h\1gT\1y = \1n\1cTo name"); console.crlf(); console.print(" \1h\1gY\1y = \1n\1cTo you"); console.crlf(); console.print(" \1h\1gU\1y = \1n\1cUnread (new) messages to you"); console.crlf(); console.print(" \1h\1gS\1y = \1n\1cScan for msgs to you"); console.crlf(); } else { // Reading personal email - Allow fewer choices allowedKeys = "KF"; console.print(" \1g\1hK\1y = \1n\1cKeyword"); console.crlf(); console.print(" \1h\1gF\1y = \1n\1cFrom name"); console.crlf(); } console.print(" \1h\1gA\1y = \1n\1cAbort"); console.crlf(); console.print("\1n\1cMake a selection\1g\1h: \1c"); // TODO: Check to see if keyword & from name search work when reading // personal email switch (console.getkeys(allowedKeys)) { case "N": gCmdLineArgVals["search"] = "new_msg_scan"; break; case "K": gCmdLineArgVals["search"] = "keyword_search"; break; case "F": gCmdLineArgVals["search"] = "from_name_search"; break; case "T": gCmdLineArgVals["search"] = "to_name_search"; break; case "Y": gCmdLineArgVals["search"] = "to_user_search"; break; case "U": gCmdLineArgVals["search"] = "to_user_new_scan"; break; case "S": gCmdLineArgVals["search"] = "to_user_all_scan"; break; case "A": // Abort default: gDoDDMR = false; console.print("\1n\1h\1y\1iAborted\1n"); console.crlf(); console.pause(); break; } } if (gDoDDMR) { // Create an instance of the DigDistMsgReader class and use it to read/list the // messages in the user's current sub-board. Pass the parsed command-line // argument values object to its constructor. var readerSubCode = (gListPersonalEmailCmdLineOpt ? "mail" : bbs.cursub_code); // If the -subBoard option was specified and the "read personal email" option was // not specified, then use the sub-board specified by the -subBoard command-line // option. if (gCmdLineArgVals.hasOwnProperty("subboard") && !gListPersonalEmailCmdLineOpt) { // If the specified sub-board option is all digits, then treat it as the // sub-board number. Otherwise, treat it as an internal sub-board code. if (/^[0-9]+$/.test(gCmdLineArgVals["subboard"])) readerSubCode = getSubBoardCodeFromNum(Number(gCmdLineArgVals["subboard"])); else readerSubCode = gCmdLineArgVals["subboard"]; } var msgReader = new DigDistMsgReader(readerSubCode, gCmdLineArgVals); // If the option to choose a message area first was enabled on the command-line // (and neither the -subBoard nor the -personalEmail options were specified), // then let the user choose a sub-board now. if (gCmdLineArgVals.hasOwnProperty("chooseareafirst") && gCmdLineArgVals["chooseareafirst"] && !gCmdLineArgVals.hasOwnProperty("subboard") && !gListPersonalEmailCmdLineOpt) msgReader.SelectMsgArea(); // Back up the user's current sub-board so that we can change back // to it after searching is done, if a search is done. var originalMsgGrpIdx = bbs.curgrp; var originalSubBoardIdx = bbs.cursub; var restoreOriginalSubCode = true; // Based on the reader's start mode/search type, do the appropriate thing. switch (msgReader.searchType) { case SEARCH_NONE: restoreOriginalSubCode = false; msgReader.ReadOrListSubBoard(); break; case SEARCH_KEYWORD: msgReader.SearchMessages("keyword_search"); break; case SEARCH_FROM_NAME: msgReader.SearchMessages("from_name_search"); break; case SEARCH_TO_NAME_CUR_MSG_AREA: msgReader.SearchMessages("to_name_search"); break; case SEARCH_TO_USER_CUR_MSG_AREA: msgReader.SearchMessages("to_user_search"); break; case SEARCH_MSG_NEWSCAN: if (!gCmdLineArgVals.suppresssearchtypetext) { console.crlf(); console.print(msgReader.text.newMsgScanText); console.crlf(); } msgReader.MessageAreaScan(SCAN_CFG_NEW, SCAN_NEW); break; case SEARCH_MSG_NEWSCAN_CUR_SUB: msgReader.MessageAreaScan(SCAN_CFG_NEW, SCAN_NEW, "S"); break; case SEARCH_MSG_NEWSCAN_CUR_GRP: msgReader.MessageAreaScan(SCAN_CFG_NEW, SCAN_NEW, "G"); break; case SEARCH_MSG_NEWSCAN_ALL: msgReader.MessageAreaScan(SCAN_CFG_NEW, SCAN_NEW, "A"); break; case SEARCH_TO_USER_NEW_SCAN: if (!gCmdLineArgVals.suppresssearchtypetext) { console.crlf(); console.print(msgReader.text.newToYouMsgScanText); console.crlf(); } msgReader.MessageAreaScan(SCAN_CFG_TOYOU/*SCAN_CFG_YONLY*/, SCAN_UNREAD); break; case SEARCH_TO_USER_NEW_SCAN_CUR_SUB: msgReader.MessageAreaScan(SCAN_CFG_TOYOU/*SCAN_CFG_YONLY*/, SCAN_UNREAD, "S"); break; case SEARCH_TO_USER_NEW_SCAN_CUR_GRP: msgReader.MessageAreaScan(SCAN_CFG_TOYOU/*SCAN_CFG_YONLY*/, SCAN_UNREAD, "G"); break; case SEARCH_TO_USER_NEW_SCAN_ALL: msgReader.MessageAreaScan(SCAN_CFG_TOYOU/*SCAN_CFG_YONLY*/, SCAN_UNREAD, "A"); break; case SEARCH_ALL_TO_USER_SCAN: if (!gCmdLineArgVals.suppresssearchtypetext) { console.crlf(); console.print(msgReader.text.allToYouMsgScanText); console.crlf(); } msgReader.MessageAreaScan(SCAN_CFG_TOYOU, SCAN_TOYOU); break; } // If we should restore the user's original message area, then do so. if (restoreOriginalSubCode) { bbs.cursub = 0; bbs.curgrp = originalMsgGrpIdx; bbs.cursub = originalSubBoardIdx; } // Remove the temporary attachments directory if it exists deltree(gFileAttachDir); // Before this script finishes, make sure the terminal attributes are set back // to normal (in case there are any attributes left on, such as background, // blink, etc.) console.print("\1n"); if (console.term_supports(USER_ANSI)) console.print("[0m"); } // End of script execution. Functions below: /////////////////////////////////////////////////////////////////////////////////// // DigDistMsgReader class stuff // DigDistMsgReader class constructor: Constructs a // DigDistMsgReader object, to be used for listing messages // in a message area. // // Parameters: // pSubBoardCode: Optional - The Synchronet sub-board code, or "mail" // for personal email. // pScriptArgs: Optional - An object containing key/value pairs representing // the command-line arguments & values, as returned by parseArgs(). function DigDistMsgReader(pSubBoardCode, pScriptArgs) { // startMode specifies the mode for the reader to start in - List mode // or reader mode, etc. This is a setting that is read from the configuration // file. The configuration file can be either READER_MODE_READ or // READER_MODE_LIST, but the optional "mode" parameter in pArgv can specify // another mode. this.startMode = READER_MODE_LIST; // msgSearchHdrs is an object containing message headers found via searching. // It is indexed by internal message area code. Each internal code index // will specify an object containing the following properties: // indexed: A standard 0-based array containing message headers this.msgSearchHdrs = new Object(); this.searchString = ""; // To be used for message searching // this.searchType will specify the type of search: // SEARCH_NONE (-1): No search // SEARCH_KEYWORD: Keyword search in message subject & body // SEARCH_FROM_NAME: Search by 'from' name // SEARCH_TO_NAME_CUR_MSG_AREA: Search by 'to' name // SEARCH_TO_USER_CUR_MSG_AREA: Search by 'to' name, to the current user // SEARCH_MSG_NEWSCAN: New (unread) message scan (prompt the user for sub, group, or all) // SEARCH_MSG_NEWSCAN_CUR_SUB: New (unread) message scan (current sub-board) // SEARCH_MSG_NEWSCAN_CUR_GRP: New (unread) message scan (current message group) // SEARCH_MSG_NEWSCAN_ALL: New (unread) message scan (all message sub-boards) // SEARCH_TO_USER_NEW_SCAN: New (unread) messages to the current user (prompt the user for sub, group, or all) // SEARCH_TO_USER_NEW_SCAN_CUR_SUB: New (unread) messages to the current user (current sub-board) // SEARCH_TO_USER_NEW_SCAN_CUR_GRP: New (unread) messages to the current user (current group) // SEARCH_TO_USER_NEW_SCAN_ALL: New (unread) messages to the current user (all sub-board) // SEARCH_ALL_TO_USER_SCAN: All messages to the current user this.searchType = SEARCH_NONE; this.subBoardCode = bbs.cursub_code; // The message sub-board code this.readingPersonalEmail = false; // A method to set subBoardCode and readingPersonalEmail this.setSubBoardCode = DigDistMsgReader_SetSubBoardCode; // this.colors will be an array of colors to use in the message list this.colors = getDefaultColors(); this.msgbase = null; // Will be a MsgBase object. this.readingPersonalEmailFromUser = false; if (typeof(pSubBoardCode) == "string") { var subCodeLowerCase = pSubBoardCode.toLowerCase(); if (subBoardCodeIsValid(subCodeLowerCase)) { this.setSubBoardCode(subCodeLowerCase); this.msgbase = new MsgBase(this.subBoardCode); if (gCmdLineArgVals.hasOwnProperty("personalemailsent") && gCmdLineArgVals.personalemailsent) this.readingPersonalEmailFromUser = true; } } // This property controls whether or not the user will be prompted to // continue listing messages after selecting a message to read. this.promptToContinueListingMessages = false; // Whether or not to prompt the user to confirm to read a message this.promptToReadMessage = false; // String lengths for the columns to write // Fixed field widths: Message number, date, and time // TODO: It might be good to figure out the longest message number for a // sub-board and set the message number length dynamically. It would have // to change whenever the user changes to a different sub-board, and the // message list format string would have to change too. this.MSGNUM_LEN = 4; this.DATE_LEN = 10; // i.e., YYYY-MM-DD this.TIME_LEN = 8; // i.e., HH:MM:SS // Variable field widths: From, to, and subject (based on a screen width of // 80 columns) this.FROM_LEN = (console.screen_columns * (15/80)).toFixed(0); this.TO_LEN = (console.screen_columns * (15/80)).toFixed(0); this.SUBJ_LEN = (console.screen_columns * (22/80)).toFixed(0); // Whether or not the user chose to read a message this.readAMessage = false; // Whether or not the user denied confirmation to read a message this.deniedReadingMessage = false; // msgListUseLightbarListInterface specifies whether or not to use the lightbar // interface for the message list. The lightbar interface will only be used if // the user's terminal supports ANSI. this.msgListUseLightbarListInterface = true; // Whether or not to use the scrolling interface when reading a message // (will only be used for ANSI terminals). this.scrollingReaderInterface = true; // reverseListOrder stores whether or not to arrange the message list descending // by date. this.reverseListOrder = false; // displayBoardInfoInHeader specifies whether or not to display // the message group and sub-board lines in the header at the // top of the screen (an additional 2 lines). this.displayBoardInfoInHeader = false; // msgList_displayMessageDateImported specifies whether or not to use the // message import date as the date displayed in the message list. If false, // the message written date will be displayed. this.msgList_displayMessageDateImported = true; // The number of spaces to use for tab characters - Used in the // extended read mode this.numTabSpaces = 3; // Construct the header format string this.sHdrFormatStr = "%" + this.MSGNUM_LEN + "s %-" + this.FROM_LEN + "s %-" + this.TO_LEN + "s %-" + this.SUBJ_LEN + "s %-" + this.DATE_LEN + "s %-" + this.TIME_LEN + "s"; // If the user's terminal doesn't support ANSI, then append a newline to // the end of the format string (we won't be able to move the cursor). if (!canDoHighASCIIAndANSI()) this.sHdrFormatStr += "\r\n"; // this.text is an object containing text used for various functionality. this.text = new Object(); this.text.scrollbarBGChar = BLOCK1; this.text.scrollbarScrollBlockChar = BLOCK2; this.text.goToPrevMsgAreaPromptText = "\1n\1c\1hGo to the previous message area"; this.text.goToNextMsgAreaPromptText = "\1n\1c\1hGo to the next message area"; this.text.newMsgScanText = "\1c\1hN\1n\1cew \1hM\1n\1cessage \1hS\1n\1ccan"; this.text.newToYouMsgScanText = "\1c\1hN\1n\1cew \1hT\1n\1co \1hY\1n\1cou \1hM\1n\1cessage \1hS\1n\1ccan"; this.text.allToYouMsgScanText = "\1c\1hA\1n\1cll \1hM\1n\1cessages \1hT\1n\1co \1hY\1n\1cou \1hS\1n\1ccan"; this.text.scanScopePromptText = "\1n\1h\1wS\1n\1gub-board, \1h\1wG\1n\1group, or \1h\1wA\1n\1gll \1h(\1wENTER\1n\1g to cancel\1h)\1n\1g: \1h\1c"; this.text.goToMsgNumPromptText = "\1n\1cGo to message # (or \1hENTER\1n\1c to cancel)\1g\1h: \1c"; this.text.msgScanAbortedText = "\1n\1h\1cM\1n\1cessage scan \1h\1y\1iaborted\1n"; this.text.deleteMsgNumPromptText = "\1n\1cNumber of the message to be deleted (or \1hENTER\1n\1c to cancel)\1g\1h: \1c"; this.text.editMsgNumPromptText = "\1n\1cNumber of the message to be edited (or \1hENTER\1n\1c to cancel)\1g\1h: \1c"; this.text.searchingSubBoardAbovePromptText = "\1n\1cSearching (current sub-board: \1b\1h%s\1n\1c)"; this.text.searchingSubBoardText = "\1n\1cSearching \1h%s\1n\1c..."; this.text.noMessagesInSubBoardText = "\1n\1h\1bThere are no messages in the area \1w%s\1b."; this.text.noSearchResultsInSubBoardText = "\1n\1h\1bNo messages were found in the area \1w%s\1b with the given search criteria."; this.text.msgScanCompleteText = "\1n\1h\1cM\1n\1cessage scan complete\1h\1g.\1n"; this.text.invalidMsgNumText = "\1n\1y\1hInvalid message number: %d"; this.text.readMsgNumPromptText = "\1n\1g\1h\1i* \1n\1cRead message #: \1h"; this.text.msgHasBeenDeletedText = "\1n\1h\1g* \1yMessage #\1w%d \1yhas been deleted."; this.text.noKludgeLinesForThisMsgText = "\1n\1h\1yThere are no kludge lines for this message."; this.text.searchingPersonalMailText = "\1w\1hSearching personal mail\1n"; this.text.searchTextPromptText = "\1cEnter the search text\1g\1h:\1n\1c "; this.text.fromNamePromptText = "\1cEnter the 'from' name to search for\1g\1h:\1n\1c "; this.text.toNamePromptText = "\1cEnter the 'to' name to search for\1g\1h:\1n\1c "; this.text.abortedText = "\1n\1y\1h\1iAborted\1n"; this.text.loadingPersonalMailText = "\1n\1cLoading %s..."; this.text.msgDelConfirmText = "\1n\1h\1yDelete\1n\1c message #\1h%d\1n\1c: Are you sure"; this.text.delSelectedMsgsConfirmText = "\1n\1h\1yDelete selected messages: Are you sure"; this.text.msgDeletedText = "\1n\1cMessage #\1h%d\1n\1c has been marked for deletion."; this.text.selectedMsgsDeletedText = "\1n\1cSelected messages have been marked for deletion."; this.text.cannotDeleteMsgText_notYoursNotASysop = "\1n\1h\1wCannot delete message #\1y%d \1wbecause it's not yours or you're not a sysop."; this.text.cannotDeleteMsgText_notLastPostedMsg = "\1n\1h\1g* \1yCannot delete message #%d. You can only delete your last message in this area.\1n"; this.text.cannotDeleteAllSelectedMsgsText = "\1n\1y\1h* Cannot delete all selected messages"; this.text.msgEditConfirmText = "\1n\1cEdit message #\1h%d\1n\1c: Are you sure"; this.text.noPersonalEmailText = "\1n\1cYou have no messages."; // Set the methods for the object this.RefreshSearchResultMsgHdr = DigDistMsgReader_RefreshSearchResultMsgHdr; // Refreshes a message header in the search results this.SearchMessages = DigDistMsgReader_SearchMessages; // Prompts the user for search text, then lists/reads messages, performing the search this.ReadMessages = DigDistMsgReader_ReadMessages; this.DisplayEnhancedMsgReadHelpLine = DigDistMsgReader_DisplayEnhancedMsgReadHelpLine; this.GoToPrevSubBoardForEnhReader = DigDistMsgReader_GoToPrevSubBoardForEnhReader; this.GoToNextSubBoardForEnhReader = DigDistMsgReader_GoToNextSubBoardForEnhReader; this.SetUpTraditionalMsgListVars = DigDistMsgReader_SetUpTraditionalMsgListVars; this.SetUpLightbarMsgListVars = DigDistMsgReader_SetUpLightbarMsgListVars; this.ListMessages = DigDistMsgReader_ListMessages; this.ListMessages_Traditional = DigDistMsgReader_ListMessages_Traditional; this.ListMessages_Lightbar = DigDistMsgReader_ListMessages_Lightbar; this.ClearSearchData = DigDistMsgReader_ClearSearchData; this.ReadOrListSubBoard = DigDistMsgReader_ReadOrListSubBoard; this.PopulateHdrsIfSearch_DispErrorIfNoMsgs = DigDistMsgReader_PopulateHdrsIfSearch_DispErrorIfNoMsgs; this.SearchTypePopulatesSearchResults = DigDistMsgReader_SearchTypePopulatesSearchResults; this.SearchTypeRequiresSearchText = DigDistMsgReader_SearchTypeRequiresSearchText; this.MessageAreaScan = DigDistMsgReader_MessageAreaScan; this.PromptContinueOrReadMsg = DigDistMsgReader_PromptContinueOrReadMsg; this.WriteMsgListScreenTopHeader = DigDistMsgReader_WriteMsgListScreenTopHeader; this.ReadMessage = DigDistMsgReader_ReadMessage; this.ReadMessageEnhanced = DigDistMsgReader_ReadMessageEnhanced; this.EnhReaderPrepLast2LinesForPrompt = DigDistMsgReader_EnhReaderPrepLast2LinesForPrompt; this.LookForNextOrPriorNonDeletedMsg = DigDistMsgReader_LookForNextOrPriorNonDeletedMsg; this.PrintMessageInfo = DigDistMsgReader_PrintMessageInfo; this.ListScreenfulOfMessages = DigDistMsgReader_ListScreenfulOfMessages; this.DisplayMsgListHelp = DigDistMsgReader_DisplayMsgListHelp; this.DisplayTraditionalMsgListHelp = DigDistMsgReader_DisplayTraditionalMsgListHelp; this.DisplayLightbarMsgListHelp = DigDistMsgReader_DisplayLightbarMsgListHelp; this.DisplayMessageListNotesHelp = DigDistMsgReader_DisplayMessageListNotesHelp; this.SetMsgListPauseTextAndLightbarHelpLine = DigDistMsgReader_SetMsgListPauseTextAndLightbarHelpLine; this.SetEnhancedReaderHelpLine = DigDistMsgReader_SetEnhancedReaderHelpLine; this.EditExistingMsg = DigDistMsgReader_EditExistingMsg; this.CanDelete = DigDistMsgReader_CanDelete; this.CanDeleteLastMsg = DigDistMsgReader_CanDeleteLastMsg; this.CanEdit = DigDistMsgReader_CanEdit; this.CanQuote = DigDistMsgReader_CanQuote; this.ReadConfigFile = DigDistMsgReader_ReadConfigFile; this.DisplaySyncMsgHeader = DigDistMsgReader_DisplaySyncMsgHeader; this.GetMsgHdrFilenameFull = DigDistMsgReader_GetMsgHdrFilenameFull; this.GetMsgHdrByIdx = DigDistMsgReader_GetMsgHdrByIdx; this.GetMsgHdrByMsgNum = DigDistMsgReader_GetMsgHdrByMsgNum; this.GetMsgHdrByAbsoluteNum = DigDistMsgReader_GetMsgHdrByAbsoluteNum; this.AbsMsgNumToIdx = DigDistMsgReader_AbsMsgNumToIdx; this.IdxToAbsMsgNum = DigDistMsgReader_IdxToAbsMsgNum; this.NumMessages = DigDistMsgReader_NumMessages; this.NonDeletedMessagesExist = DigDistMsgReader_NonDeletedMessagesExist; this.HighestMessageNum = DigDistMsgReader_HighestMessageNum; this.IsValidMessageNum = DigDistMsgReader_IsValidMessageNum; this.PromptForMsgNum = DigDistMsgReader_PromptForMsgNum; this.ParseMsgAtCodes = DigDistMsgReader_ParseMsgAtCodes; this.ReplaceMsgAtCodeFormatStr = DigDistMsgReader_ReplaceMsgAtCodeFormatStr; this.FindNextNonDeletedMsgIdx = DigDistMsgReader_FindNextNonDeletedMsgIdx; this.ChangeSubBoard = DigDistMsgReader_ChangeSubBoard; this.EnhancedReaderChangeSubBoard = DigDistMsgReader_EnhancedReaderChangeSubBoard; this.ReplyToMsg = DigDistMsgReader_ReplyToMsg; this.DoPrivateReply = DigDistMsgReader_DoPrivateReply; this.DisplayEnhancedReaderHelp = DigDistMsgReader_DisplayEnhancedReaderHelp; this.DisplayEnhancedMsgHdr = DigDistMsgReader_DisplayEnhancedMsgHdr; this.DisplayEnhancedReaderWholeScrollbar = DigDistMsgReader_DisplayEnhancedReaderWholeScrollbar; this.UpdateEnhancedReaderScollbar = DigDistMsgReader_UpdateEnhancedReaderScollbar; this.MessageIsDeleted = DigDistMsgReader_MessageIsDeleted; this.MessageIsLastFromUser = DigDistMsgReader_MessageIsLastFromUser; this.DisplayEnhReaderError = DigDistMsgReader_DisplayEnhReaderError; this.EnhReaderPromptYesNo = DigDistMsgReader_EnhReaderPromptYesNo; this.PromptAndDeleteMessage = DigDistMsgReader_PromptAndDeleteMessage; this.PromptAndDeleteSelectedMessages = DigDistMsgReader_PromptAndDeleteSelectedMessages; this.GetExtdMsgHdrInfo = DigDistMsgReader_GetExtdMsgHdrInfo; this.GetMsgInfoForEnhancedReader = DigDistMsgReader_GetMsgInfoForEnhancedReader; this.GetLastReadMsgIdx = DigDistMsgReader_GetLastReadMsgIdx; this.GetScanPtrMsgIdx = DigDistMsgReader_GetScanPtrMsgIdx; this.SearchingAndResultObjsDefinedForCurSub = DigDistMsgReader_SearchingAndResultObjsDefinedForCurSub; this.RemoveFromSearchResults = DigDistMsgReader_RemoveFromSearchResults; this.FindThreadNextOffset = DigDistMsgReader_FindThreadNextOffset; this.FindThreadPrevOffset = DigDistMsgReader_FindThreadPrevOffset; this.SaveMsgToFile = DigDistMsgReader_SaveMsgToFile; this.ToggleSelectedMessage = DigDistMsgReader_ToggleSelectedMessage; this.MessageIsSelected = DigDistMsgReader_MessageIsSelected; this.AllSelectedMessagesCanBeDeleted = DigDistMsgReader_AllSelectedMessagesCanBeDeleted; this.DeleteSelectedMessages = DigDistMsgReader_DeleteSelectedMessages; this.NumSelectedMessages = DigDistMsgReader_NumSelectedMessages; // These two variables keep track of whether we're doing a message scan that spans // multiple sub-boards so that the enhanced reader function can enable use of // the > key to go to the next sub-board. this.doingMultiSubBoardScan = false; // An option for using the scrollable interface for messages with ANSI // content - The sysop can set this to false if the sysop thinks the // scrolling ANSI interface (using frame.js and scrollbar.js) doesn't // look good enough this.useScrollingInterfaceForANSIMessages = true; // Whether or not to pause (with a message) after doing a new message scan this.pauseAfterNewMsgScan = true; this.cfgFilename = "DDMsgReader.cfg"; // Check the command-line arguments for a custom configuration file name // before reading the configuration file. var scriptArgsIsValid = (typeof(pScriptArgs) == "object"); if (scriptArgsIsValid && pScriptArgs.hasOwnProperty("configfilename")) this.cfgFilename = pScriptArgs["configfilename"]; // Read the settings from the config file this.cfgFileSuccessfullyRead = false; this.ReadConfigFile(); // Set any other values specified by the command-line parameters // Reader start mode - Read or list mode if (scriptArgsIsValid) { if (pScriptArgs.hasOwnProperty("startmode")) { var readerStartMode = readerModeStrToVal(pScriptArgs["startmode"]); if (readerStartMode != -1) this.startMode = readerStartMode; } // Search mode if (pScriptArgs.hasOwnProperty("search")) { var searchType = searchTypeStrToVal(pScriptArgs["search"]); if (searchType != SEARCH_NONE) this.searchType = searchType; } } // Color value adjusting (must be done after reading the config file in case // the color settings were changed from defaults) // Message list highlight colors: For each (except for the background), // prepend the normal attribute and append the background attribute to the end. // This is to ensure that high attributes don't affect the rest of the line and // the background attribute stays for the rest of the line. this.colors.msgListMsgNumHighlightColor = "\1n" + this.colors.msgListMsgNumHighlightColor + this.colors.msgListHighlightBkgColor; this.colors.msgListFromHighlightColor = "\1n" + this.colors.msgListFromHighlightColor + this.colors.msgListHighlightBkgColor; this.colors.msgListToHighlightColor = "\1n" + this.colors.msgListToHighlightColor + this.colors.msgListHighlightBkgColor; this.colors.msgListSubjHighlightColor = "\1n" + this.colors.msgListSubjHighlightColor + this.colors.msgListHighlightBkgColor; this.colors.msgListDateHighlightColor = "\1n" + this.colors.msgListDateHighlightColor + this.colors.msgListHighlightBkgColor; this.colors.msgListTimeHighlightColor = "\1n" + this.colors.msgListTimeHighlightColor + this.colors.msgListHighlightBkgColor; // Similar for the area chooser lightbar highlight colors this.colors.areaChooserMsgAreaNumHighlightColor = "\1n" + this.colors.areaChooserMsgAreaNumHighlightColor + this.colors.areaChooserMsgAreaBkgHighlightColor; this.colors.areaChooserMsgAreaDescHighlightColor = "\1n" + this.colors.areaChooserMsgAreaDescHighlightColor + this.colors.areaChooserMsgAreaBkgHighlightColor; this.colors.areaChooserMsgAreaDateHighlightColor = "\1n" + this.colors.areaChooserMsgAreaDateHighlightColor + this.colors.areaChooserMsgAreaBkgHighlightColor; this.colors.areaChooserMsgAreaTimeHighlightColor = "\1n" + this.colors.areaChooserMsgAreaTimeHighlightColor + this.colors.areaChooserMsgAreaBkgHighlightColor; this.colors.areaChooserMsgAreaNumItemsHighlightColor = "\1n" + this.colors.areaChooserMsgAreaNumItemsHighlightColor + this.colors.areaChooserMsgAreaBkgHighlightColor; // Similar for the enhanced reader help line colors this.colors.enhReaderHelpLineGeneralColor = "\1n" + this.colors.enhReaderHelpLineGeneralColor + this.colors.enhReaderHelpLineBkgColor; this.colors.enhReaderHelpLineHotkeyColor = "\1n" + this.colors.enhReaderHelpLineHotkeyColor + this.colors.enhReaderHelpLineBkgColor; this.colors.enhReaderHelpLineParenColor = "\1n" + this.colors.enhReaderHelpLineParenColor + this.colors.enhReaderHelpLineBkgColor; // Similar for the lightbar message list help line colors this.colors.lightbarMsgListHelpLineGeneralColor = "\1n" + this.colors.lightbarMsgListHelpLineGeneralColor + this.colors.lightbarMsgListHelpLineBkgColor; this.colors.lightbarMsgListHelpLineHotkeyColor = "\1n" + this.colors.lightbarMsgListHelpLineHotkeyColor + this.colors.lightbarMsgListHelpLineBkgColor; this.colors.lightbarMsgListHelpLineParenColor = "\1n" + this.colors.lightbarMsgListHelpLineParenColor + this.colors.lightbarMsgListHelpLineBkgColor; // Similar for the lightbar area chooser help line colors this.colors.lightbarAreaChooserHelpLineGeneralColor = "\1n" + this.colors.lightbarAreaChooserHelpLineGeneralColor + this.colors.lightbarAreaChooserHelpLineBkgColor; this.colors.lightbarAreaChooserHelpLineHotkeyColor = "\1n" + this.colors.lightbarAreaChooserHelpLineHotkeyColor + this.colors.lightbarAreaChooserHelpLineBkgColor; this.colors.lightbarAreaChooserHelpLineParenColor = "\1n" + this.colors.lightbarAreaChooserHelpLineParenColor + this.colors.lightbarAreaChooserHelpLineBkgColor; // Prepend most of the text strings with the normal attribute (if they don't // have it already) to make sure the correct colors are used. for (var prop in this.text) { if ((prop != "scrollbarBGChar") && (prop != "scrollbarScrollBlockChar")) { if ((this.text[prop].length > 0) && (this.text[prop].charAt(0) != "\1n")) this.text[prop] = "\1n" + this.text[prop]; } } // this.tabReplacementText will be the text that tabs will be replaced // with in enhanced reader mode this.tabReplacementText = format("%" + this.numTabSpaces + "s", ""); // Construct the message information format string. These must be done after // reading the configuration file, because the configuration file specifies the // colors to use. this.sMsgInfoFormatStr = this.colors.msgListMsgNumColor + "%" + this.MSGNUM_LEN + "d%s" + this.colors.msgListFromColor + "%-" + this.FROM_LEN + "s " + this.colors.msgListToColor + "%-" + this.TO_LEN + "s " + this.colors.msgListSubjectColor + "%-" + this.SUBJ_LEN + "s " + this.colors.msgListDateColor + "%-" + this.DATE_LEN + "s " + this.colors.msgListTimeColor + "%-" + this.TIME_LEN + "s"; // Message information format string with colors to use when the message is // written to the user. this.sMsgInfoToUserFormatStr = this.colors.msgListToUserMsgNumColor + "%" + this.MSGNUM_LEN + "d%s" + this.colors.msgListToUserFromColor + "%-" + this.FROM_LEN + "s " + this.colors.msgListToUserToColor + "%-" + this.TO_LEN + "s " + this.colors.msgListToUserSubjectColor + "%-" + this.SUBJ_LEN + "s " + this.colors.msgListToUserDateColor + "%-" + this.DATE_LEN + "s " + this.colors.msgListToUserTimeColor + "%-" + this.TIME_LEN + "s"; // Message information format string with colors to use when the message is // from the user. this.sMsgInfoFromUserFormatStr = this.colors.msgListFromUserMsgNumColor + "%" + this.MSGNUM_LEN + "d%s" + this.colors.msgListFromUserFromColor + "%-" + this.FROM_LEN + "s " + this.colors.msgListFromUserToColor + "%-" + this.TO_LEN + "s " + this.colors.msgListFromUserSubjectColor + "%-" + this.SUBJ_LEN + "s " + this.colors.msgListFromUserDateColor + "%-" + this.DATE_LEN + "s " + this.colors.msgListFromUserTimeColor + "%-" + this.TIME_LEN + "s"; // Highlighted message information line for the message list (used for the // lightbar interface) this.sMsgInfoFormatHighlightStr = this.colors.msgListMsgNumHighlightColor + "%" + this.MSGNUM_LEN + "d%s" + this.colors.msgListFromHighlightColor + "%-" + this.FROM_LEN + "s " + this.colors.msgListToHighlightColor + "%-" + this.TO_LEN + "s " + this.colors.msgListSubjHighlightColor + "%-" + this.SUBJ_LEN + "s " + this.colors.msgListDateHighlightColor + "%-" + this.DATE_LEN + "s " + this.colors.msgListTimeHighlightColor + "%-" + this.TIME_LEN + "s"; // If the user's terminal doesn't support ANSI, then append a newline to // the end of the format string (we won't be able to move the cursor). if (!canDoHighASCIIAndANSI()) { this.sMsgInfoFormatStr += "\r\n"; this.sMsgInfoToUserFormatStr += "\r\n"; this.sMsgInfoFromUserFormatStr += "\r\n"; this.sMsgInfoFormatHighlightStr += "\r\n"; } // Enhanced reader help line (will be set up in // DigDistMsgReader_SetEnhancedReaderHelpLine()) this.enhReadHelpLine = ""; // Read the enhanced message header file and populate this.enhMsgHeaderLines, // the header text for enhanced reader mode. The enhanced reader header file // name will start with 'enhMsgHeader', and there can be multiple versions for // different terminal widths (i.e., msgHeader_80.ans for an 80-column console // and msgHeader_132 for a 132-column console). this.enhMsgHeaderLines = new Array(); var enhHsgHdrFileExists = true; var enhMsgHdrFilenameBase = "enhMsgHeader"; var enhMsgHdrFilenameBaseFullPath = gStartupPath + enhMsgHdrFilenameBase; // See if there is a header file that is made for the user's terminal // width (msgHeader-<width>.ans/asc). If not, then just go with // msgHeader.ans/asc. var enhMsgHdrFilename = ""; if (file_exists(enhMsgHdrFilenameBaseFullPath + "-" + console.screen_columns + ".ans")) enhMsgHdrFilename = enhMsgHdrFilenameBaseFullPath + "-" + console.screen_columns + ".ans"; else if (file_exists(enhMsgHdrFilenameBaseFullPath + "-" + console.screen_columns + ".asc")) enhMsgHdrFilename = enhMsgHdrFilenameBaseFullPath + "-" + console.screen_columns + ".asc"; else if (file_exists(enhMsgHdrFilenameBaseFullPath + ".ans")) enhMsgHdrFilename = enhMsgHdrFilenameBaseFullPath + ".ans"; else if (file_exists(enhMsgHdrFilenameBaseFullPath + ".asc")) enhMsgHdrFilename = enhMsgHdrFilenameBaseFullPath + ".asc"; else { // The enhanced reader header file doesn't exist, so provide some default // header lines. enhHsgHdrFileExists = false; // Group name: 20% of console width // Sub-board name: 34% of console width var msgGrpNameLen = Math.floor(console.screen_columns * 0.2); var subBoardNameLen = Math.floor(console.screen_columns * 0.34); var hdrLine1 = "\1n\1h\1c" + UPPER_LEFT_SINGLE + HORIZONTAL_SINGLE + "\1n\1c" + HORIZONTAL_SINGLE + " \1h@GRP-L"; var numChars = msgGrpNameLen - 7; for (var i = 0; i < numChars; ++i) hdrLine1 += "#"; hdrLine1 += "@ @SUB-L"; numChars = subBoardNameLen - 7; for (var i = 0; i < numChars; ++i) hdrLine1 += "#"; hdrLine1 += "@\1k"; numChars = console.screen_columns - console.strlen(hdrLine1) - 4; for (var i = 0; i < numChars; ++i) hdrLine1 += HORIZONTAL_SINGLE; hdrLine1 += "\1n\1c" + HORIZONTAL_SINGLE + HORIZONTAL_SINGLE + "\1h" + HORIZONTAL_SINGLE + UPPER_RIGHT_SINGLE; this.enhMsgHeaderLines.push(hdrLine1); var hdrLine2 = "\1n\1c" + VERTICAL_SINGLE + "\1h\1k" + BLOCK1 + BLOCK2 + BLOCK3 + "\1gM\1n\1gsg#\1h\1c: \1b@MSG_NUM_AND_TOTAL-L"; numChars = console.screen_columns - 32; for (var i = 0; i < numChars; ++i) hdrLine2 += "#"; hdrLine2 += "@\1n\1c" + VERTICAL_SINGLE; this.enhMsgHeaderLines.push(hdrLine2); var hdrLine3 = "\1n\1h\1k" + VERTICAL_SINGLE + BLOCK1 + BLOCK2 + BLOCK3 + "\1gF\1n\1grom\1h\1c: \1b@MSG_FROM-L"; numChars = console.screen_columns - 23; for (var i = 0; i < numChars; ++i) hdrLine3 += "#"; hdrLine3 += "@\1k" + VERTICAL_SINGLE; this.enhMsgHeaderLines.push(hdrLine3); var hdrLine4 = "\1n\1h\1k" + VERTICAL_SINGLE + BLOCK1 + BLOCK2 + BLOCK3 + "\1gT\1n\1go \1h\1c: \1b@MSG_TO-L"; numChars = console.screen_columns - 21; for (var i = 0; i < numChars; ++i) hdrLine4 += "#"; hdrLine4 += "@\1k" + VERTICAL_SINGLE; this.enhMsgHeaderLines.push(hdrLine4); var hdrLine5 = "\1n\1h\1k" + VERTICAL_SINGLE + BLOCK1 + BLOCK2 + BLOCK3 + "\1gS\1n\1gubj\1h\1c: \1b@MSG_SUBJECT-L"; numChars = console.screen_columns - 26; for (var i = 0; i < numChars; ++i) hdrLine5 += "#"; hdrLine5 += "@\1k" + VERTICAL_SINGLE; this.enhMsgHeaderLines.push(hdrLine5); var hdrLine6 = "\1n\1c" + VERTICAL_SINGLE + "\1h\1k" + BLOCK1 + BLOCK2 + BLOCK3 + "\1gD\1n\1gate\1h\1c: \1b@MSG_DATE-L"; numChars = console.screen_columns - 23; for (var i = 0; i < numChars; ++i) hdrLine6 += "#"; hdrLine6 += "@\1n\1c" + VERTICAL_SINGLE; this.enhMsgHeaderLines.push(hdrLine6); var hdrLine7 = "\1n\1h\1c" + BOTTOM_T_SINGLE + HORIZONTAL_SINGLE + "\1n\1c" + HORIZONTAL_SINGLE + HORIZONTAL_SINGLE + "\1h\1k"; numChars = console.screen_columns - 8; for (var i = 0; i < numChars; ++i) hdrLine7 += HORIZONTAL_SINGLE; hdrLine7 += "\1c" + HORIZONTAL_SINGLE + HORIZONTAL_SINGLE + "\1h" + HORIZONTAL_SINGLE + BOTTOM_T_SINGLE; this.enhMsgHeaderLines.push(hdrLine7); } if (enhHsgHdrFileExists) { // If the header file is ANSI, then convert it to Synchronet attribute // codes and read that file instead. This is done so that this script can // accurately get the file line lengths using console.strlen(). var syncConvertedHdrFilename = enhMsgHdrFilenameBaseFullPath + "_converted.asc"; if (!file_exists(syncConvertedHdrFilename)) { var dotIdx = enhMsgHdrFilename.lastIndexOf("."); if (dotIdx > -1) { // If header file is ANSI, then convert it to Synchronet attribute // format and save it as an .asc file. Otherwise, just use the // header file without conversion since it's already ASCII or // Synchronet attribute code format. var isANSI = (enhMsgHdrFilename.substr(dotIdx+1).toUpperCase() == "ANS"); if (isANSI) { var filenameBase = enhMsgHdrFilename.substr(0, dotIdx); var cmdLine = system.exec_dir + "ans2asc \"" + enhMsgHdrFilename + "\" \"" + syncConvertedHdrFilename + "\""; // Note: Both system.exec(cmdLine) and // bbs.exec(cmdLine, EX_NATIVE, gStartupPath) could be used to // execute the command, but system.exec() seems noticeably faster. system.exec(cmdLine); } else syncConvertedHdrFilename = enhMsgHdrFilename; } } // Read the header file into this.enhMsgHeaderLines var hdrFile = new File(syncConvertedHdrFilename); if (hdrFile.open("r")) { var fileLine = null; while (!hdrFile.eof && (this.enhMsgHeaderLines.length <= 10)) { // Read the next line from the header file. fileLine = hdrFile.readln(2048); // fileLine should be a string, but I've seen some cases // where it isn't, so check its type. if (typeof(fileLine) != "string") continue; // Make sure the line isn't longer than the user's terminal //if (fileLine.length > console.screen_columns) // fileLine = fileLine.substr(0, console.screen_columns); this.enhMsgHeaderLines.push(fileLine); } } } // Save the enhanced reader header width. This will be the length of the longest // line in the header. this.enhMsgHeaderWidth = 0; if (this.enhMsgHeaderLines.length > 0) { var lineLen = 0; for (var i = 0; i < this.enhMsgHeaderLines.length; ++i) { lineLen = console.strlen(this.enhMsgHeaderLines[i]); if (lineLen > this.enhMsgHeaderWidth) this.enhMsgHeaderWidth = lineLen; } } // Message display area information this.msgAreaTop = this.enhMsgHeaderLines.length + 1; this.msgAreaBottom = console.screen_rows-1; // The last line of the message area // msgAreaLeft and msgAreaRight are the rightmost and leftmost columns of the // message area, respectively. These are 1-based. 1 is subtracted from // msgAreaRight to leave room for the scrollbar in enhanced reader mode. this.msgAreaLeft = 1; this.msgAreaRight = console.screen_columns - 1; this.msgAreaWidth = this.msgAreaRight - this.msgAreaLeft + 1; this.msgAreaHeight = this.msgAreaBottom - this.msgAreaTop + 1; ////////////////////////////////////////////// // Things related to changing to a different message group & sub-board // In the message area lists (for changing to another message area), the // date & time of the last-imported message will be shown. // msgAreaList_lastImportedMsg_showImportTime is a boolean to specify // whether or not to use the import time for the last-imported message. // If false, the message written time will be used. this.msgAreaList_lastImportedMsg_showImportTime = true; // These variables store the lengths of the various columns displayed in // the message group/sub-board lists. // Sub-board info field lengths this.areaNumLen = 4; this.numItemsLen = 4; this.dateLen = 10; // i.e., YYYY-MM-DD this.timeLen = 8; // i.e., HH:MM:SS // Sub-board name length - This should be 47 for an 80-column display. this.subBoardNameLen = console.screen_columns - this.areaNumLen - this.numItemsLen - this.dateLen - this.timeLen - 7; // Message group description length (67 chars on an 80-column screen) this.msgGrpDescLen = console.screen_columns - this.areaNumLen - this.numItemsLen - 5; // Some methods for choosing the message area this.WriteChgMsgAreaKeysHelpLine = DigDistMsgReader_WriteLightbarChgMsgAreaKeysHelpLine; this.WriteGrpListHdrLine = DigDistMsgReader_WriteGrpListTopHdrLine; this.WriteSubBrdListHdr1Line = DMsgAreaChooser_WriteSubBrdListHdr1Line; this.SelectMsgArea = DigDistMsgReader_SelectMsgArea; this.SelectMsgArea_Lightbar = DigDistMsgReader_SelectMsgArea_Lightbar; this.SelectSubBoard_Lightbar = DigDistMsgReader_SelectSubBoard_Lightbar; this.SelectMsgArea_Traditional = DigDistMsgReader_SelectMsgArea_Traditional; this.ListMsgGrps = DigDistMsgReader_ListMsgGrps_Traditional; this.ListSubBoardsInMsgGroup = DigDistMsgReader_ListSubBoardsInMsgGroup_Traditional; // Lightbar-specific methods this.ListScreenfulOfMsgGrps = DigDistMsgReader_listScreenfulOfMsgGrps; this.WriteMsgGroupLine = DigDistMsgReader_writeMsgGroupLine; this.UpdateMsgAreaPageNumInHeader = DigDistMsgReader_updateMsgAreaPageNumInHeader; this.ListScreenfulOfSubBrds = DigDistMsgReader_ListScreenfulOfSubBrds; this.WriteMsgSubBoardLine = DigDistMsgReader_WriteMsgSubBrdLine; // Choose Message Area help screen this.ShowChooseMsgAreaHelpScreen = DigDistMsgReader_showChooseMsgAreaHelpScreen; // Method to build the sub-board printf information for a message // group this.BuildSubBoardPrintfInfoForGrp = DigDistMsgReader_BuildSubBoardPrintfInfoForGrp; // Methods for calculating a page number for a message list item this.CalcTraditionalMsgListTopIdx = DigDistMsgReader_CalcTraditionalMsgListTopIdx; this.CalcLightbarMsgListTopIdx = DigDistMsgReader_CalcLightbarMsgListTopIdx; this.CalcMsgListScreenIdxVarsFromMsgNum = DigDistMsgReader_CalcMsgListScreenIdxVarsFromMsgNum; // A method for validating a user's choice of message area this.ValidateMsgAreaChoice = DigDistMsgReader_ValidateMsgAreaChoice; // printf strings for message group/sub-board lists // Message group information (printf strings) this.msgGrpListPrintfStr = "\1n " + this.colors.areaChooserMsgAreaNumColor + "%" + this.areaNumLen + "d " + this.colors.areaChooserMsgAreaDescColor + "%-" + this.msgGrpDescLen + "s " + this.colors.areaChooserMsgAreaNumItemsColor + "%" + this.numItemsLen + "d"; this.msgGrpListHilightPrintfStr = "\1n" + this.colors.areaChooserMsgAreaBkgHighlightColor + " " + "\1n" + this.colors.areaChooserMsgAreaBkgHighlightColor + this.colors.areaChooserMsgAreaNumHighlightColor + "%" + this.areaNumLen + "d \1n" + this.colors.areaChooserMsgAreaBkgHighlightColor + this.colors.areaChooserMsgAreaDescHighlightColor + "%-" + this.msgGrpDescLen + "s \1n" + this.colors.areaChooserMsgAreaBkgHighlightColor + this.colors.areaChooserMsgAreaNumItemsHighlightColor + "%" + this.numItemsLen + "d"; // Message group list header (printf string) this.msgGrpListHdrPrintfStr = this.colors.areaChooserMsgAreaHeaderColor + "%6s %-" + +(this.msgGrpDescLen-8) + "s %-12s"; // Sub-board information header (printf string) this.subBoardListHdrPrintfStr = this.colors.areaChooserMsgAreaHeaderColor + " %5s %-" + +(this.subBoardNameLen-3) + "s %-7s %-19s"; // Lightbar area chooser help line text // TODO: Account for wide terminals? this.lightbarAreaChooserHelpLine = "\1n" + this.colors.lightbarAreaChooserHelpLineHotkeyColor + "" + this.colors.lightbarAreaChooserHelpLineGeneralColor + ", " + this.colors.lightbarAreaChooserHelpLineHotkeyColor + "" + this.colors.lightbarAreaChooserHelpLineGeneralColor + ", " + this.colors.lightbarAreaChooserHelpLineHotkeyColor + "ENTER" + this.colors.lightbarAreaChooserHelpLineGeneralColor + ", " + this.colors.lightbarAreaChooserHelpLineHotkeyColor + "HOME" + this.colors.lightbarAreaChooserHelpLineGeneralColor + ", " + this.colors.lightbarAreaChooserHelpLineHotkeyColor + "END" + this.colors.lightbarAreaChooserHelpLineGeneralColor + ", " + this.colors.lightbarAreaChooserHelpLineHotkeyColor + "#" + this.colors.lightbarAreaChooserHelpLineGeneralColor + ", " + this.colors.lightbarAreaChooserHelpLineHotkeyColor + "N" + this.colors.lightbarAreaChooserHelpLineParenColor + ")" + this.colors.lightbarAreaChooserHelpLineGeneralColor + "ext pg, " + this.colors.lightbarAreaChooserHelpLineHotkeyColor + "P" + this.colors.lightbarAreaChooserHelpLineParenColor + ")" + this.colors.lightbarAreaChooserHelpLineGeneralColor + "rev pg, " + this.colors.lightbarAreaChooserHelpLineHotkeyColor + "F" + this.colors.lightbarAreaChooserHelpLineParenColor + ")" + this.colors.lightbarAreaChooserHelpLineGeneralColor + "irst pg, " + this.colors.lightbarAreaChooserHelpLineHotkeyColor + "L" + this.colors.lightbarAreaChooserHelpLineParenColor + ")" + this.colors.lightbarAreaChooserHelpLineGeneralColor + "ast pg, " + this.colors.lightbarAreaChooserHelpLineHotkeyColor + "Q" + this.colors.lightbarAreaChooserHelpLineParenColor + ")" + this.colors.lightbarAreaChooserHelpLineGeneralColor + "uit, " + this.colors.lightbarAreaChooserHelpLineHotkeyColor + "? "; // this.subBoardListPrintfInfo will be an array of printf strings // for the sub-boards in the message groups. The index is the // message group index. The sub-board printf information is created // on the fly the first time the user lists sub-boards for a message // group. this.subBoardListPrintfInfo = new Array(); // Variables to save the top message index for the traditional & lightbar // message lists. Initialize them to -1 to mean the message list hasn't been // displayed yet - In that case, the lister will use the user's last // read pointer. this.tradListTopMsgIdx = -1; this.tradMsgListNumLines = console.screen_rows-3; if (this.displayBoardInfoInHeader) this.tradMsgListNumLines -= 2; this.lightbarListTopMsgIdx = -1; this.lightbarMsgListNumLines = console.screen_rows-2; this.lightbarMsgListStartScreenRow = 2; // The first line number on the screen for the message list // If we will be displaying the message group and sub-board in the // header at the top of the screen (an additional 2 lines), then // update this.lightbarMsgListNumLines and this.lightbarMsgListStartScreenRow to // account for this. if (this.displayBoardInfoInHeader) { this.lightbarMsgListNumLines -= 2; this.lightbarMsgListStartScreenRow += 2; } // The selected message index for the lightbar message list (initially -1, will // be set in the lightbar list method) this.lightbarListSelectedMsgIdx = -1; // The selected message cursor position for the lightbar message list (initially // null, will be set in the lightbar list message) this.lightbarListCurPos = null; // selectedMessages will be an object (indexed by sub-board internal code) // containing objects that contain message indexes (as properties) for the // sub-boards. Messages can be selected by the user for doing things such // as a batch delete, etc. this.selectedMessages = new Object(); } // For the DigDistMsgReader class: Sets the subBoardCode property and also // sets the readingPersonalEmail property, a boolean for whether or not // personal email is being read (whether the sub-board code is "mail") // // Parameters: // pSubCode: The sub-board code to set in the object function DigDistMsgReader_SetSubBoardCode(pSubCode) { this.subBoardCode = pSubCode; this.readingPersonalEmail = (this.subBoardCode == "mail"); } // Refreshes a message header in the message header arrays in this.msgSearchHdrs. // // Parameters: // pMsgIndex: The index (0-based) of the message header // pAttrib: Optional - An attribute to apply. If this is is not specified, // then the message header will be retrieved from the message base. // pSubBoardCode: Optional - An internal sub-board code. If not specified, then // this method will default to this.subBoardCode. function DigDistMsgReader_RefreshSearchResultMsgHdr(pMsgIndex, pAttrib, pSubBoardCode) { if (typeof(pMsgIndex) != "number") return; var subCode = (typeof(pSubBoardCode) == "string" ? pSubBoardCode : this.subBoardCode); if (this.msgSearchHdrs.hasOwnProperty(subCode)) { var msgNum = pMsgIndex + 1; if (typeof(pAttrib) != "undefined") { if (this.msgSearchHdrs[this.subBoardCode].indexed.hasOwnProperty(pMsgIndex)) { this.msgSearchHdrs[this.subBoardCode].indexed[pMsgIndex].attr = this.msgSearchHdrs[this.subBoardCode].indexed[pMsgIndex].attr | pAttrib; var msgOffsetFromHdr = this.msgSearchHdrs[this.subBoardCode].indexed[pMsgIndex].offset; this.msgbase.put_msg_header(true, msgOffsetFromHdr, this.msgSearchHdrs[this.subBoardCode].indexed[pMsgIndex]); } } else { var msgHeader = this.GetMsgHdrByIdx(pMsgIndex); if (this.msgSearchHdrs[this.subBoardCode].indexed.hasOwnProperty(pMsgIndex)) { this.msgSearchHdrs[this.subBoardCode].indexed[pMsgIndex] = msgHeader; this.msgbase.put_msg_header(true, msgHeader.offset, msgHeader); } } } } // For the DigDistMsgReader class: Inputs search text from the user, then reads/lists // messages, which will perform the search. // // Paramters: // pSearchModeStr A string to specify the lister mode to use - This can // be one of the search modes to specify how to search: // "keyword_search": Search the message subjects & bodies by keyword // "from_name_search": Search messages by from name // "to_name_search": Search messages by to name // "to_user_search": Search messages by to name, to the logged-in user // pSubBoardCode: Optional - The Synchronet sub-board code, or "mail" // for personal email. function DigDistMsgReader_SearchMessages(pSearchModeStr, pSubBoardCode) { // Convert the search mode string to an integer representing the search // mode. If we get back -1, that means the search mode string was invalid. // If that's the case, simply list messages. Otherwise, do the search. this.searchType = searchTypeStrToVal(pSearchModeStr); if (this.searchType == SEARCH_NONE) // No search; search mode string was invalid { // Clear the search information and read/list messages. this.ClearSearchData(); this.ReadOrListSubBoard(pSubBoardCode); } else { // The search mode string was valid, so go ahead and search. console.print("\1n"); console.crlf(); var subCode = (typeof(pSubBoardCode) == "string" ? pSubBoardCode : this.subBoardCode); if (subCode == "mail") console.print("\1n" + this.text.searchingPersonalMailText); else console.print("\1n" + this.text.searchingSubBoardAbovePromptText.replace("%s", subBoardGrpAndName(bbs.cursub_code)) + "\1n"); console.crlf(); // Output the prompt text to the user (for modes where a prompt is needed) switch (this.searchType) { case SEARCH_KEYWORD: console.print("\1n" + this.text.searchTextPromptText); break; case SEARCH_FROM_NAME: console.print("\1n" + this.text.fromNamePromptText); break; case SEARCH_TO_NAME_CUR_MSG_AREA: console.print("\1n" + this.text.toNamePromptText); break; case SEARCH_TO_USER_CUR_MSG_AREA: // Note: No prompt needed for this - Will search for the user's name/handle console.line_counter = 0; // To prevent a pause before the message list comes up break; default: break; } //var promptUserForText = this.SearchTypePopulatesSearchResults(); var promptUserForText = this.SearchTypeRequiresSearchText(); // Get the search text from the user if (promptUserForText) this.searchString = console.getstr(512, K_UPPER); // If the user was prompted for search text but no search text was entered, // then show an abort message and don't do anything. Otherwise, go ahead // and list/read messages. if (promptUserForText && (this.searchString.length == 0)) { this.ClearSearchData(); console.print("\1n" + this.text.abortedText); console.crlf(); console.pause(); return; } else { // List/read messages this.ReadOrListSubBoard(pSubBoardCode); // Clear the search data so that subsequent listing or reading sessions // don't repeat the same search this.ClearSearchData(); } } } // This function clears the search data from the object. function DigDistMsgReader_ClearSearchData() { this.searchType = SEARCH_NONE; this.searchString == ""; if (this.msgSearchHdrs != null) { for (var subCode in this.msgSearchHdrs) { delete this.msgSearchHdrs[subCode].indexed; delete this.msgSearchHdrs[subCode]; } delete this.msgSearchHdrs; this.msgSearchHdrs = new Object(); } } // For the DigDistMsgReader class: Performs message reading/listing. // Depending on the value of this.startMode, starts in either reader // mode or lister mode. Uses an input loop to let the user switch // between the two modes. // // Parameters: // pSubBoardCode: Optional - The internal code of a sub-board to read. // If not specified, the internal sub-board code specified // when creating the object will be used. // pStartingMsgOffset: Optional - The offset of a message to start at // pAllowChgArea: Optional boolean - Whether or not to allow changing the // message area // pReturnOnNextAreaNav: Optional boolean - Whether or not this method should // return when it would move to the next message area due // navigation from the user (i.e., with the right arrow key) // pPauseOnNoMsgSrchResults: Optional boolean - Whether or not to pause when // a message search doesn't find any search results // in the current sub-board. Defaults to true. // // Return value: An object with the following properties: // stoppedReading: Boolean - Whether or not the user stopped reading. // This can also be true if there is an error. function DigDistMsgReader_ReadOrListSubBoard(pSubBoardCode, pStartingMsgOffset, pAllowChgArea, pReturnOnNextAreaNav, pPauseOnNoMsgSrchResults) { var retObj = new Object(); retObj.stoppedReading = false; // Set the sub-board code if applicable var previousSubBoardCode = this.subBoardCode; if (typeof(pSubBoardCode) == "string") { if (subBoardCodeIsValid(pSubBoardCode)) this.setSubBoardCode(pSubBoardCode); else { console.print("\1n\1h\1yWarning: \1wThe Message Reader connot continue because an invalid"); console.crlf(); console.print("sub-board code was specified (" + pSubBoardCode + "). Please notify the sysop."); console.crlf(); console.pause(); retObj.stoppedReading = true; return retObj; } } // (re)-open the message base if (previousSubBoardCode != this.subBoardCode) { if ((this.msgbase != null) && (this.msgbase.is_open)) this.msgbase.close(); this.msgbase = new MsgBase(this.subBoardCode); } else if (this.msgbase == null) this.msgbase = new MsgBase(this.subBoardCode); // Open the sub-board. If the message base was not opened, then output // an error and return. if (!this.msgbase.is_open && !this.msgbase.open()) { console.print("\1n"); console.crlf(); console.print("\1h\1y* \1wUnable to open message sub-board:"); console.crlf(); console.print(subBoardGrpAndName(this.subBoardCode)); console.crlf(); console.pause(); retObj.stoppedReading = true; return retObj; } // Populate this.msgSearchHdrs for the current sub-board if there is a search // specified. If there are no messages to read in the current sub-board, then // just return. var pauseOnNoSearchResults = (typeof(pPauseOnNoMsgSrchResults) == "boolean" ? pPauseOnNoMsgSrchResults : true); if (!this.PopulateHdrsIfSearch_DispErrorIfNoMsgs(true, true, pauseOnNoSearchResults)) { retObj.stoppedReading = false; return retObj; } // Check the pAllowChgArea parameter. If it's a boolean, then use it. If // not, then check to see if we're reading personal mail - If not, then allow // the user to change to a different message area. var allowChgMsgArea = true; if (typeof(pAllowChgArea) == "boolean") allowChgMsgArea = pAllowChgArea; else allowChgMsgArea = (this.subBoardCode != "mail"); // If reading personal email and messages haven't been collected (searched) // yet, then do so now. if (this.readingPersonalEmail && (!this.msgSearchHdrs.hasOwnProperty(this.subBoardCode))) this.msgSearchHdrs[this.subBoardCode] = searchMsgbase(this.subBoardCode, this.msgbase, this.searchType, this.searchString, this.readingPersonalEmailFromUser); // Determine whether to start in list or reader mode, depending // on the value of this.startMode. var readerMode = this.startMode; // User input loop var selectedMessageOffset = 0; if (typeof(pStartingMsgOffset) == "number") selectedMessageOffset = pStartingMsgOffset; else if (this.SearchingAndResultObjsDefinedForCurSub()) { // If reading personal mail, start at the first unread message index // (or the last message, if all messages have been read) if (this.readingPersonalEmail) { selectedMessageOffset = this.GetLastReadMsgIdx(false); // Used to be true if ((selectedMessageOffset > -1) && (selectedMessageOffset < this.NumMessages() - 1)) ++selectedMessageOffset; } else selectedMessageOffset = 0; } else selectedMessageOffset = -1; var otherRetObj = null; var continueOn = true; while (continueOn) { switch (readerMode) { case READER_MODE_READ: // Call the ReadMessages method - DOn't change the sub-board, // and pass the selected index of the message to read. If that // index is -1, the ReadMessages method will use the user's // last-read message index. otherRetObj = this.ReadMessages(null, selectedMessageOffset, true, allowChgMsgArea, pReturnOnNextAreaNav); // If the user wants to quit or if there was an error, then stop // the input loop. if (otherRetObj.stoppedReading) { retObj.stoppedReading = true; continueOn = false; } // If we're set to return on navigation to the next message area and // the user's last keypress was the right arrow key or next action // was to go to the next message area, then don't continue the input // loop, and also say that the user didn't stop reading. else if (pReturnOnNextAreaNav && ((otherRetObj.lastUserInput == KEY_RIGHT) || (otherRetObj.lastUserInput == KEY_ENTER) || (otherRetObj.lastAction == ACTION_GO_NEXT_MSG_AREA))) { retObj.stoppedReading = false; continueOn = false; } else if (otherRetObj.messageListReturn) readerMode = READER_MODE_LIST; break; case READER_MODE_LIST: // Note: Doing the message list is also handled in this.ReadMessages(). // This code is here in case the reader is configured to start up // in list mode first. // Call the ListMessages method - Don't change the sub-board, and // have it return if the user chooses a message to read. otherRetObj = this.ListMessages(null, true, pAllowChgArea); // If the user wants to quit, set continueOn to false to get out // of the loop. Otherwise, set the selected message offset to // what the user chose from the list. if (otherRetObj.lastUserInput == "Q") { retObj.stoppedReading = true; continueOn = false; } else { selectedMessageOffset = otherRetObj.selectedMsgOffset; readerMode = READER_MODE_READ; } break; default: break; } } // Close the message base object (if it has not been closed already), // re-enable the normal text attribute, and clear the screen. if (this.msgbase != null) { this.msgbase.close(); this.msgbase = null; } console.clear("\1n"); return retObj; } // Helper for DigDistMsgReader_ReadOrListSubBoard(): Populates this.msgSearchHdrs // if an applicable search type is specified; also, if there are no messages in // the current sub-board, outputs an error to the user. // // Parameters: // pCloseMsgbaseAndSetNullIfNoMsgs: Optional boolean - Whether or not to close the message // base if there are no messages. Defaults to true. // pOutputMessages: Boolean - Whether or not to output messages to the screen. // Defaults to true. // pPauseOnNoMsgError: Optional boolean - Whether or not to pause for a keypress // after displaying the "no messages" error. Defaults to true. // // Return value: Boolean - Whether or not there are messages to read in the current // sub-board function DigDistMsgReader_PopulateHdrsIfSearch_DispErrorIfNoMsgs(pCloseMsgbaseAndSetNullIfNoMsgs, pOutputMessages, pPauseOnNoMsgError) { var thereAreMessagesToRead = true; var outputMessages = (typeof(pOutputMessages) == "boolean" ? pOutputMessages : true); // If a search is is specified that would populate the search results, then // perform the message search for the current sub-board. if (this.SearchTypePopulatesSearchResults()) { if (!this.msgSearchHdrs.hasOwnProperty(this.subBoardCode)) { // TODO: In case new messages were posted in this sub-board, it might help // to check the current number of messages vs. the previous number of messages // and search the new messages if there are more. if (outputMessages) { console.crlf(); if (this.readingPersonalEmail) console.print("\1n" + this.text.loadingPersonalMailText.replace("%s", subBoardGrpAndName(this.subBoardCode))); else console.print(this.text.searchingSubBoardText.replace("%s", subBoardGrpAndName(this.subBoardCode))); } this.msgSearchHdrs[this.subBoardCode] = searchMsgbase(this.subBoardCode, this.msgbase, this.searchType, this.searchString, this.readingPersonalEmailFromUser); } } else { // There is no search is specified, so clear the search results for the // current sub-board to help ensure that there are messages to read. if (this.msgSearchHdrs.hasOwnProperty(this.subBoardCode)) { delete this.msgSearchHdrs[this.subBoardCode].indexed; delete this.msgSearchHdrs[this.subBoardCode]; } } // If there are no messages to display in the current sub-board, then set the // return value and let the user know (if outputMessages is true). if (this.NumMessages() == 0) { thereAreMessagesToRead = false; var closeMsgbase = (typeof(pCloseMsgbaseAndSetNullIfNoMsgs) == "boolean" ? pCloseMsgbaseAndSetNullIfNoMsgs : true); if (closeMsgbase) { this.msgbase.close(); this.msgbase = null; } if (outputMessages) { console.print("\1n"); console.crlf(); if (this.readingPersonalEmail) console.print(this.text.noPersonalEmailText); else { if (this.msgSearchHdrs.hasOwnProperty(this.subBoardCode)) console.print(this.text.noSearchResultsInSubBoardText.replace("%s", subBoardGrpAndName(this.subBoardCode))); else console.print(this.text.noMessagesInSubBoardText.replace("%s", subBoardGrpAndName(this.subBoardCode))); } console.crlf(); var pauseOnNoMsgsError = (typeof(pPauseOnNoMsgError) == "boolean" ? pPauseOnNoMsgError : true); if (pauseOnNoMsgsError) console.pause(); } } return thereAreMessagesToRead; } // For the DigDistMsgReader class: Returns whether the search type is a type // that would result in the search results structure being populated. Search // types where that wouldn't happen are SEARCH_NONE (no search) and any of the // message scan search types. function DigDistMsgReader_SearchTypePopulatesSearchResults() { return (this.readingPersonalEmail || searchTypePopulatesSearchResults(this.searchType)); } // For the DigDistMsgReader class: Returns whether the search type is a type // that requires search text. Search types that require search text are the // keyword search, from name search, and to name search. Search types that // don't require search text are SEARCH_NONE (no search) & the message scan search // types. function DigDistMsgReader_SearchTypeRequiresSearchText() { return searchTypeRequiresSearchText(this.searchType); } // Returns whether a search type value would populate search results. // // Parameters: // pSearchType: A search type integer value // // Return value: Boolean - Whether or not the search type would populate search // results function searchTypePopulatesSearchResults(pSearchType) { return ((pSearchType == SEARCH_KEYWORD) || (pSearchType == SEARCH_FROM_NAME) || (pSearchType == SEARCH_TO_NAME_CUR_MSG_AREA) || (pSearchType == SEARCH_TO_USER_CUR_MSG_AREA) || (pSearchType == SEARCH_TO_USER_NEW_SCAN) || (pSearchType == SEARCH_TO_USER_NEW_SCAN_CUR_SUB) || (pSearchType == SEARCH_TO_USER_NEW_SCAN_CUR_GRP) || (pSearchType == SEARCH_TO_USER_NEW_SCAN_ALL) || (pSearchType == SEARCH_ALL_TO_USER_SCAN)); } // Returns whether a search type value requires search text. // // Parameters: // pSearchType: A search type integer value // // Return value: Boolean - Whether or not the search type requires search text function searchTypeRequiresSearchText(pSearchType) { return ((pSearchType == SEARCH_KEYWORD) || (pSearchType == SEARCH_FROM_NAME) || (pSearchType == SEARCH_TO_NAME_CUR_MSG_AREA)); } // For the DigDistMsgReader class: Scans the message area(s) for new messages, // unread messages to the user, or all messages to the user. // // Parameters: // pScanCfgOpt: The scan configuration option to check for in the sub-boards // (from sbbsdefs.js). Supported values are SCAN_CFG_NEW (new // message scan) and SCAN_CFG_TOYOU (messages to the user). // pScanMode: The scan mode (from sbbsdefs.js). Supported values are SCAN_NEW // (new message scan), SCAN_TOYOU (scan for all messages to the // user), and SCAN_UNREAD (scan for new messages to the user). // pScanScopeChar: Optional - A character (as a string) representing the scan // scope: "S" for sub-board, "G" for group, or "A" for all. // If this is not specified, the user will be prompted for the // scan scope. function DigDistMsgReader_MessageAreaScan(pScanCfgOpt, pScanMode, pScanScopeChar) { var scanScopeChar = ""; if ((typeof(pScanScopeChar) == "string") && /^[SGA]$/.test(pScanScopeChar)) scanScopeChar = pScanScopeChar; else { // Prompt the user to scan in the current sub-board, the current message group, // or all. Default to all. console.print(this.text.scanScopePromptText); scanScopeChar = console.getkeys("SGAC").toString(); // If the user just pressed Enter without choosing anything, then abort and return. if (scanScopeChar.length == 0) { console.crlf(); console.print(this.text.msgScanAbortedText); console.crlf(); console.pause(); return; } } // Do some logging if verbose logging is enabled if (gCmdLineArgVals.verboselogging) { var logMessage = "Doing a message area scan ("; if (pScanCfgOpt == SCAN_CFG_NEW) { // The only valid value for pScanMode in this case is SCAN_NEW, so no // need to check pScanMode to append more to the log message. logMessage += "new"; } else if (pScanCfgOpt == SCAN_CFG_TOYOU) { // Valid values for pScanMode in this case are SCAN_UNREAD and SCAN_TOYOU. if (pScanMode == SCAN_UNREAD) logMessage += "unread messages to the user"; else if (pScanMode == SCAN_TOYOU) logMessage += "all messages to the user"; } if (scanScopeChar == "A") // All sub-boards logMessage += ", all sub-boards"; else if (scanScopeChar == "G") // Current message group { logMessage += ", current message group (" + msg_area.grp_list[bbs.curgrp].description + ")"; } else if (scanScopeChar == "S") // Current sub-board { logMessage += ", current sub-board (" + msg_area.grp_list[bbs.curgrp].description + " - " + msg_area.grp_list[bbs.curgrp].sub_list[bbs.cursub].description + ")"; } logMessage += ")"; writeToSysAndNodeLog(logMessage); } // Save the original search type, sub-board code, searched message headers, // etc. to be restored later var originalSearchType = this.searchType; var originalSubBoardCode = this.subBoardCode; var originalBBSCurGrp = bbs.curgrp; var originalBBSCurSub = bbs.cursub; var originalMsgSrchHdrs = this.msgSearchHdrs; // Make sure there is no search data this.ClearSearchData(); // If the object's message base is currently open, then close it. The object's // message base object will be used to open each sub-board to scan for & read // unread messages. if ((this.msgbase != null) && (this.msgbase.is_open)) this.msgbase.close(); // Perform the message scan var continueNewScan = true; var userAborted = false; if (scanScopeChar == "A") // All sub-board scan { this.doingMultiSubBoardScan = true; // Iterate through all message groups & sub-boards looking for ones with unread // messages. When a sub-board with unread messages is found, then let the user // read messages in that sub-board. for (var grpIndex = 0; (grpIndex < msg_area.grp_list.length) && continueNewScan; ++grpIndex) { // Group description: msg_area.grp_list[grpIndex].description // Iterate through the sub-boards in this message group looking for unread messages for (var subIndex = 0; (subIndex < msg_area.grp_list[grpIndex].sub_list.length) && continueNewScan; ++subIndex) { // Set the console line counter to 0 to prevent screen pausing // when the "Searching ..." and "No messages were found" text is // displayed repeatedly console.line_counter = 0; // If the sub-board's access requirements allows the user to read it // and it's enabled in the user's message scan configuration, then go // ahead with this sub-board. // Note: Used to use this to determine whether the user could access the // sub-board: //user.compare_ars(msg_area.grp_list[grpIndex].sub_list[subIndex].ars) // Now using the can_read property. this.setSubBoardCode(msg_area.grp_list[grpIndex].sub_list[subIndex].code); // Needs to be set before getting the last read/scan pointer index if (msg_area.sub[this.subBoardCode].can_read && ((msg_area.sub[this.subBoardCode].scan_cfg & pScanCfgOpt) == pScanCfgOpt)) { // Sub-board description: msg_area.grp_list[grpIndex].sub_list[subIndex].description // Open the sub-board and check for unread messages. If there are any, then let // the user read the messages in the sub-board. //this.msgbase = new MsgBase(msg_area.grp_list[grpIndex].sub_list[subIndex].code); this.msgbase = new MsgBase(this.subBoardCode); if (this.msgbase.open()) { //this.setSubBoardCode(msg_area.grp_list[grpIndex].sub_list[subIndex].code); // Needs to be set before getting the last read/scan pointer index // If the current sub-board contains only deleted messages, // then skip it. var scanPtrMsgIdx = this.GetScanPtrMsgIdx(); var nonDeletedMsgsExist = (this.FindNextNonDeletedMsgIdx(scanPtrMsgIdx-1, true) > -1); if (!nonDeletedMsgsExist) continue; // In the switch cases below, bbs.curgrp and bbs.cursub are // temporarily changed the user's sub-board to the current // sub-board so that certain @-codes (such as @GRP-L@, etc.) // are displayed by Synchronet correctly. // We might want the starting message index to be different // depending on the scan mode. switch (pScanMode) { case SCAN_NEW: // Make sure the sub-board has some messages. Let the user read it if // the scan pointer index is -1 (one unread message) or if it points to // a message within the number of messages in the sub-board. if ((this.msgbase.total_msgs > 0) && ((scanPtrMsgIdx == -1) || (scanPtrMsgIdx < this.msgbase.total_msgs-1))) { bbs.curgrp = grpIndex; bbs.cursub = subIndex; // Start at the first unread message. var startMsgIdx = scanPtrMsgIdx + 1; if (this.SearchingAndResultObjsDefinedForCurSub()) startMsgIdx = 0; // Allow the user to read messages in this sub-board. Don't allow // the user to change to a different message area, don't pause // when there's no search results in a sub-board, and return // instead of going to the next sub-board via navigation. var readRetObj = this.ReadOrListSubBoard(null, startMsgIdx, false, true, false); // If the user stopped reading & decided to quit, then exit the // message scan loops. if (readRetObj.stoppedReading) { continueNewScan = false; userAborted = true; } } break; case SCAN_TOYOU: // All messages to the user bbs.curgrp = grpIndex; bbs.cursub = subIndex; // Search for messages to the user in the current sub-board // and let the user read the sub-board if messages are // found. Don't allow the user to change to a different // message area, don't pause when there's no search results // in a sub-board, and return instead of going to the next // sub-board via navigation. this.searchType = SEARCH_TO_USER_CUR_MSG_AREA; var readRetObj = this.ReadOrListSubBoard(null, 0, false, true, false); // If the user stopped reading & decided to quit, then exit the // message scan loops. if (readRetObj.stoppedReading) { continueNewScan = false; userAborted = true; } break; case SCAN_UNREAD: // New (unread) messages to the user bbs.curgrp = grpIndex; bbs.cursub = subIndex; // Search for messages to the user in the current sub-board // and let the user read the sub-board if messages are // found. Don't allow the user to change to a different // message area, don't pause when there's no search results // in a sub-board, and return instead of going to the next // sub-board via navigation. this.searchType = SEARCH_TO_USER_NEW_SCAN; var readRetObj = this.ReadOrListSubBoard(null, 0, false, true, false); // If the user stopped reading & decided to quit, then exit the // message scan loops. if (readRetObj.stoppedReading) { continueNewScan = false; userAborted = true; } break; default: break; } if (this.msgbase != null) this.msgbase.close(); } } } } } else if (scanScopeChar == "G") // Group scan { this.doingMultiSubBoardScan = true; // Iterate through the sub-boards in the current message group looking for messages for (var subIndex = 0; (subIndex < msg_area.grp_list[bbs.curgrp].sub_list.length) && continueNewScan; ++subIndex) { // Set the console line counter to 0 to prevent screen pausing // when the "Searching ..." and "No messages were found" text is // displayed repeatedly console.line_counter = 0; // If the sub-board's access requirements allows the user to read it // and it's enabled in the user's message scan configuration, then go // ahead with this sub-board. this.setSubBoardCode(msg_area.grp_list[bbs.curgrp].sub_list[subIndex].code); // Needs to be set before the last read/scan pointer message if (msg_area.sub[this.subBoardCode].can_read && ((msg_area.sub[this.subBoardCode].scan_cfg & pScanCfgOpt) == pScanCfgOpt)) { // Sub-board description: msg_area.grp_list[bbs.curgrp].sub_list[subIndex].description // Open the sub-board and check for unread messages. If there are any, then let // the user read the messages in the sub-board. //this.msgbase = new MsgBase(msg_area.grp_list[bbs.curgrp].sub_list[subIndex].code); this.msgbase = new MsgBase(this.subBoardCode); if (this.msgbase.open()) { // The following line is now done before the 'if' statement above //this.setSubBoardCode(msg_area.grp_list[bbs.curgrp].sub_list[subIndex].code); // Needs to be set before the last read/scan pointer message // If the current sub-board contains only deleted messages, // then skip it. var scanPtrMsgIdx = this.GetScanPtrMsgIdx(); var nonDeletedMsgsExist = (this.FindNextNonDeletedMsgIdx(scanPtrMsgIdx-1, true) > -1); if (!nonDeletedMsgsExist) continue; // Temporarily change the user's sub-board to the current // sub-board so that certain @-codes (such as @GRP-L@, etc.) // are displayed by Synchronet correctly. bbs.curgrp = msg_area.sub[this.subBoardCode].grp_index; bbs.cursub = msg_area.sub[this.subBoardCode].index; // We might want the starting message index to be different // depending on the scan mode. switch (pScanMode) { case SCAN_NEW: // Make sure the sub-board has some messages. Let the user read it if // the scan pointer index is -1 (one unread message) or if it points to // a message within the number of messages in the sub-board. if ((this.msgbase.total_msgs > 0) && ((scanPtrMsgIdx == -1) || (scanPtrMsgIdx < this.msgbase.total_msgs-1))) { //bbs.cursub = subIndex; // Now done a bit earlier // Start at the first unread message. var startMsgIdx = scanPtrMsgIdx + 1; if (this.SearchingAndResultObjsDefinedForCurSub()) startMsgIdx = 0; // Allow the user to read messages in this sub-board. Don't allow // the user to change to a different message area, don't pause // when there's no search results in a sub-board, and return // instead of going to the next sub-board via navigation. var readRetObj = this.ReadOrListSubBoard(null, startMsgIdx, false, true, false); // If the user stopped reading & decided to quit, then exit the // message scan loops. if (readRetObj.stoppedReading) { continueNewScan = false; userAborted = true; } } break; case SCAN_TOYOU: // All messages to the user //bbs.cursub = subIndex; // Now done a bit earlier // Search for messages to the user in the current sub-board // and let the user read the sub-board if messages are found. // Don't allow the user to change to a different message // area, don't pause when there's no search results in a // sub-board, and return instead of going to the next sub-board // via navigation. this.searchType = SEARCH_TO_USER_CUR_MSG_AREA; var readRetObj = this.ReadOrListSubBoard(null, 0, false, true, false); // If the user stopped reading & decided to quit, then exit the // message scan loops. if (readRetObj.stoppedReading) { continueNewScan = false; userAborted = true; } break; case SCAN_UNREAD: // New (unread) messages to the user //bbs.cursub = subIndex; // Now done a bit earlier // Search for unread messages to the user in the current // sub-board and let the user read the sub-board if messages // are found. Don't allow the user to change to a different // message area, don't pause when there's no search results // in a sub-board, and return instead of going to the next // sub-board via navigation. this.searchType = SEARCH_TO_USER_NEW_SCAN_CUR_GRP; var readRetObj = this.ReadOrListSubBoard(null, 0, false, true, false); // If the user stopped reading & decided to quit, then exit the // message scan loops. if (readRetObj.stoppedReading) { continueNewScan = false; userAborted = true; } break; default: break; } if (this.msgbase != null) this.msgbase.close(); } } } } else if (scanScopeChar == "S") // Current sub-board scan { this.doingMultiSubBoardScan = false; // If the command-line arguments don't specify the sub-board code or // the user is reading personal email, then set the object's sub-board // code to the user's current sub-board code (bbs.cursub_code) to ensure // that we open the correct messagebase and so that @-codes, etc. display // for the correct sub-board. if (!gCmdLineArgVals.hasOwnProperty("subboard") || gListPersonalEmailCmdLineOpt) this.setSubBoardCode(bbs.cursub_code); // Make sure the user has access permissions for the current sub-board and // has it set up in their scan configuration before letting the user read // it. if (msg_area.sub[this.subBoardCode].can_read && ((msg_area.sub[this.subBoardCode].scan_cfg & pScanCfgOpt) == pScanCfgOpt)) { this.msgbase = new MsgBase(this.subBoardCode); //this.msgbase = new MsgBase(bbs.cursub_code); if (this.msgbase.open()) { // Temporarily change the user's sub-board to the current // sub-board so that certain @-codes (such as @GRP-L@, etc.) // are displayed by Synchronet correctly. bbs.curgrp = msg_area.sub[this.subBoardCode].grp_index; bbs.cursub = msg_area.sub[this.subBoardCode].index; //this.setSubBoardCode(bbs.cursub_code); // Needs to be set before getting the last read/scan pointer message // Only scan this sub-board if it contains messages that are not // marked as deleted. var scanPtrMsgIdx = this.GetScanPtrMsgIdx(); var nonDeletedMsgsExist = (this.FindNextNonDeletedMsgIdx(scanPtrMsgIdx-1, true) > -1); if (nonDeletedMsgsExist) { // We might want the starting message index to be different // depending on the scan mode. switch (pScanMode) { case SCAN_NEW: // Make sure the sub-board has some messages. Let the user read it if // the scan pointer index is -1 (one unread message) or if it points to // a message within the number of messages in the sub-board. if ((this.msgbase.total_msgs > 0) && ((scanPtrMsgIdx == -1) || (scanPtrMsgIdx < this.msgbase.total_msgs-1))) { if (this.subBoardCode != "mail") bbs.cursub = msg_area.sub[bbs.cursub_code].index; // Start at the first unread message. var startMsgIdx = scanPtrMsgIdx + 1; if (this.SearchingAndResultObjsDefinedForCurSub()) startMsgIdx = 0; // Allow the user to read messages in this sub-board. Don't allow // the user to change to a different message area, don't pause // when there's no search results in a sub-board, and return // instead of going to the next sub-board via navigation. var readRetObj = this.ReadOrListSubBoard(null, startMsgIdx, false, true, true); userAborted = readRetObj.stoppedReading; } break; case SCAN_TOYOU: // All messages to the user if (this.subBoardCode != "mail") bbs.cursub = msg_area.sub[bbs.cursub_code].index; // Set the search type to messages to the user and let the user // read the sub-board. ReadOrListSubBoard() will do the search. // Don't allow the user to change to a different message area. this.searchType = SEARCH_TO_USER_CUR_MSG_AREA; var readRetObj = this.ReadOrListSubBoard(null, 0, false, true, true); userAborted = readRetObj.stoppedReading; break; case SCAN_UNREAD: // New (unread) messages to the user bbs.cursub = msg_area.sub[bbs.cursub_code].index; // Set the search type to messages to the user and let the user // read the sub-board. ReadOrListSubBoard() will do the search. // Don't allow the user to change to a different message area. this.searchType = SEARCH_TO_USER_NEW_SCAN_CUR_SUB; bbs.cursub = msg_area.sub[bbs.cursub_code].index; var readRetObj = this.ReadOrListSubBoard(null, 0, false, true, true); userAborted = readRetObj.stoppedReading; break; default: break; } } if (this.msgbase != null) this.msgbase.close(); } } } // Restore the original sub-board code, searched message headers, etc. this.searchType = originalSearchType; this.setSubBoardCode(originalSubBoardCode); this.msgSearchHdrs = originalMsgSrchHdrs; bbs.curgrp = originalBBSCurGrp; bbs.cursub = originalBBSCurSub; if ((this.msgbase != null) && (this.msgbase.is_open)) this.msgbase.close(); this.msgbase = new MsgBase(this.subBoardCode); this.doingMultiSubBoardScan = false; if (this.pauseAfterNewMsgScan) { console.crlf(); if (userAborted) console.print("\1n" + this.text.msgScanAbortedText + "\1n"); else console.print("\1n" + this.text.msgScanCompleteText + "\1n"); console.crlf(); console.pause(); } } // For the DigDistMsgReader class: Performs the message reading activity. // // Parameters: // pSubBoardCode: Optional - The internal code of a sub-board to read. // If not specified, the internal sub-board code specified // when creating the object will be used. // pStartingMsgOffset: Optional - The offset of a message to start at // pReturnOnMessageList: Optional boolean - Whether or not to quit when the // user wants to list messages (used when this method // is called from ReadOrListSubBoard()). // pAllowChgArea: Optional boolean - Whether or not to allow changing the // message area // pReturnOnNextAreaNav: Optional boolean - Whether or not this method should // return when it would move to the next message area due // navigation from the user (i.e., with the right arrow // key or with < (go to previous message area) or > (go // to next message area)) // // Return value: An object that has the following properties: // lastUserInput: The user's last keypress/input // lastAction: The last action chosen by the user based on their // last keypress, etc. // stoppedReading: Boolean - Whether reading has stopped // (due to user quitting, error, or otherwise) // messageListReturn: Boolean - Whether this method is returning for // the caller to display the message list. This // will only be true when the pReturnOnMessageList // parameter is true and the user wants to list // messages. function DigDistMsgReader_ReadMessages(pSubBoardCode, pStartingMsgOffset, pReturnOnMessageList, pAllowChgArea, pReturnOnNextAreaNav) { var retObj = new Object(); retObj.lastUserInput = ""; retObj.lastAction = ACTION_NONE; retObj.stoppedReading = false; retObj.messageListReturn = false; // If the passed-in sub-board code was different than what was set in the object before, // then open the new message sub-board. var previousSubBoardCode = this.subBoardCode; if (typeof(pSubBoardCode) == "string") { if (subBoardCodeIsValid(pSubBoardCode)) this.setSubBoardCode(pSubBoardCode); else { console.print("\1n\1h\1yWarning: \1wThe Message Reader connot continue because an invalid"); console.crlf(); console.print("sub-board code was specified (" + pSubBoardCode + "). Please notify the sysop."); console.crlf(); console.pause(); retObj.stoppedReading = true; return retObj; } } if (this.subBoardCode.length == 0) { console.print("\1n\1h\1yWarning: \1wThe Message Reader connot continue because no message"); console.crlf(); console.print("sub-board was specified. Please notify the sysop."); console.crlf(); console.pause(); retObj.stoppedReading = true; return retObj; } if (previousSubBoardCode != this.subBoardCode) { if ((this.msgbase != null) && (this.msgbase.is_open)) this.msgbase.close(); this.msgbase = new MsgBase(this.subBoardCode); } else if (this.msgbase == null) this.msgbase = new MsgBase(this.subBoardCode); // If the message base was not opened, then output an error and return. if (!this.msgbase.is_open && !this.msgbase.open()) { console.print("\1n"); console.crlf(); console.print("\1h\1y* \1wUnable to open message sub-board:"); console.crlf(); console.print(subBoardGrpAndName(this.subBoardCode)); console.crlf(); console.pause(); retObj.stoppedReading = true; return retObj; } // If there are no messages to display in the current sub-board, then let the // user know and exit. if (this.NumMessages() == 0) { this.msgbase.close(); this.msgbase = null; console.clear("\1n"); console.center("\1n\1h\1yThere are no messages to display."); console.crlf(); console.pause(); retObj.stoppedReading = true; return retObj; } // Check the pAllowChgArea parameter. If it's a boolean, then use it. If // not, then check to see if we're reading personal mail - If not, then allow // the user to change to a different message area. var allowChgMsgArea = true; if (typeof(pAllowChgArea) == "boolean") allowChgMsgArea = pAllowChgArea; else allowChgMsgArea = (this.subBoardCode != "mail"); // If reading personal email and messages haven't been collected (searched) // yet, then do so now. if (this.readingPersonalEmail && (!this.msgSearchHdrs.hasOwnProperty(this.subBoardCode))) this.msgSearchHdrs[this.subBoardCode] = searchMsgbase(this.subBoardCode, this.msgbase, this.searchType, this.searchString, this.readingPersonalEmailFromUser); // Determine the index of the message to start at. This will be // pStartingMsgOffset if pStartingMsgOffset is valid, or the index // of the user's last-read message in this sub-board. var msgIndex = 0; if ((typeof(pStartingMsgOffset) == "number") && (pStartingMsgOffset >= 0) && (pStartingMsgOffset < this.NumMessages())) msgIndex = pStartingMsgOffset; else if (this.SearchingAndResultObjsDefinedForCurSub()) msgIndex = 0; else { msgIndex = this.GetLastReadMsgIdx(); if (msgIndex == -1) msgIndex = 0; } // If the current message index is for a message that has been // deleted, then find the next non-deleted message. var testMsgHdr = this.GetMsgHdrByIdx(msgIndex); if ((testMsgHdr == null) || ((testMsgHdr.attr & MSG_DELETE) == MSG_DELETE)) { // First try going forward var nonDeletedMsgIdx = this.FindNextNonDeletedMsgIdx(msgIndex, true); // If a non-deleted message was not found, then try going backward. if (nonDeletedMsgIdx == -1) nonDeletedMsgIdx = this.FindNextNonDeletedMsgIdx(msgIndex, false); // If a non-deleted message was found, then set msgIndex to it. // Otherwise, tell the user there are no messages in this sub-board // and return. if (nonDeletedMsgIdx > -1) msgIndex = nonDeletedMsgIdx; else { this.msgbase.close(); this.msgbase = null; console.clear("\1n"); console.center("\1h\1yThere are no messages to display."); console.crlf(); console.pause(); retObj.stoppedReading = true; return retObj; } } // Construct the hotkey help line (needs to be done after the message // base is open so that the delete & edit keys can be added correctly). this.SetEnhancedReaderHelpLine(); // Get the screen ready for reading messages - First, clear the screen. console.clear("\1n"); // Display the help line at the bottom of the screen if (this.scrollingReaderInterface && console.term_supports(USER_ANSI)) this.DisplayEnhancedMsgReadHelpLine(console.screen_rows, allowChgMsgArea); // Input loop var msgHdr = null; var dateTimeStr = null; var screenY = 1; // For screen updates requiring more than one line var continueOn = true; var readMsgRetObj = null; // previousNextAction will store the next action from the previous iteration. // It is useful for some checks, such as when the current message is deleted, // we'll want to see if the user wanted to go to the previous message/area // for navigation purposes. var previousNextAction = ACTION_NONE; while (continueOn && (msgIndex >= 0) && (msgIndex < this.NumMessages())) { // Display the message with the enhanced read method readMsgRetObj = this.ReadMessageEnhanced(msgIndex, allowChgMsgArea); retObj.lastUserInput = readMsgRetObj.lastKeypress; retObj.lastAction = readMsgRetObj.nextAction; // If we should refresh the enhanced reader help line on the screen (and // the returned message offset is valid and the user's terminal supports ANSI), // then refresh the help line. if (readMsgRetObj.refreshEnhancedRdrHelpLine && readMsgRetObj.offsetValid && console.term_supports(USER_ANSI)) this.DisplayEnhancedMsgReadHelpLine(console.screen_rows, allowChgMsgArea); // If the returned message offset is invalid, then quit. if (!readMsgRetObj.offsetValid) { continueOn = false; retObj.stoppedReading = true; break; } // If the message is marked as deleted (not by the user), then go to the // next/previous message else if (readMsgRetObj.msgDeleted) { // If the user's next action in the last iteration was to go to the // previous message, then go backwards; otherwise, go forward. if (previousNextAction == ACTION_GO_PREVIOUS_MSG) msgIndex = this.FindNextNonDeletedMsgIdx(msgIndex, false); else msgIndex = this.FindNextNonDeletedMsgIdx(msgIndex, true); continueOn = ((msgIndex >= 0) && (msgIndex < this.NumMessages())); } else if (readMsgRetObj.nextAction == ACTION_QUIT) // Quit { // Quit continueOn = false; retObj.stoppedReading = true; break; } else if (readMsgRetObj.lastKeypress == "R") { // Replying to the message is handled in ReadMessageEnhanced(). // The help line at the bottom of the screen needs to be redrawn though, // for ANSI users. if (this.scrollingReaderInterface && console.term_supports(USER_ANSI)) this.DisplayEnhancedMsgReadHelpLine(console.screen_rows, allowChgMsgArea); } else if (readMsgRetObj.nextAction == ACTION_GO_PREVIOUS_MSG) // Go to previous message/area { // TODO: There is some opportunity for screen redraw optimization - If // already at the first readable sub-board, this would redraw the // screen unnecessarily. Similar for the right arrow key too. // The newMsgOffset value will be 0 or more if a prior non-deleted // message was found. If it's -1, then allow going to the previous // message sub-board/group. if (readMsgRetObj.newMsgOffset > -1) msgIndex = readMsgRetObj.newMsgOffset; else { // The user is at the beginning of the current sub-board. if (allowChgMsgArea) { var goToPrevRetval = this.GoToPrevSubBoardForEnhReader(allowChgMsgArea); retObj.stoppedReading = goToPrevRetval.shouldStopReading; // If we're going to stop reading, then if (retObj.stoppedReading) msgIndex = 0; else if (goToPrevRetval.changedMsgArea) msgIndex = goToPrevRetval.msgIndex; } // If the caller wants this method to return instead of going to the next // sub-board with messages, then do so. if (pReturnOnNextAreaNav) return retObj; } } // Go to next message action - This can happen with the right arrow key or // if the user deletes the message in the ReadMessageEnhanced() method. else if (readMsgRetObj.nextAction == ACTION_GO_NEXT_MSG) { // The newMsgOffset value will be 0 or more if a later non-deleted // message was found. If it's -1, then allow going to the next // message sub-board/group. if (readMsgRetObj.newMsgOffset > -1) msgIndex = readMsgRetObj.newMsgOffset; else { // The user is at the end of the current sub-board. if (allowChgMsgArea && !pReturnOnNextAreaNav) { var goToNextRetval = this.GoToNextSubBoardForEnhReader(allowChgMsgArea); retObj.stoppedReading = goToNextRetval.shouldStopReading; // If we're going to stop reading, then if (retObj.stoppedReading) msgIndex = 0; else if (goToNextRetval.changedMsgArea) msgIndex = goToNextRetval.msgIndex; } // If the caller wants this method to return instead of going to the next // sub-board with messages, then do so. if (pReturnOnNextAreaNav) return retObj; } } else if (readMsgRetObj.nextAction == ACTION_GO_FIRST_MSG) // Go to the first message { // Go to the first message that's not marked as deleted. This passes -1 as the // starting message index because FindNextNonDeletedMsgIdx() will increment it // before searching in order to find the "next" message. msgIndex = this.FindNextNonDeletedMsgIdx(-1, true); } else if (readMsgRetObj.nextAction == ACTION_GO_LAST_MSG) // Go to the last message { // Go to the last message that's not marked as deleted msgIndex = this.FindNextNonDeletedMsgIdx(this.NumMessages(), false); } else if (readMsgRetObj.nextAction == ACTION_CHG_MSG_AREA) // Change message area, if allowed { if (allowChgMsgArea) { // Change message sub-board. If a different sub-board was // chosen, then change some variables to use the new // chosen sub-board. this.SelectMsgArea(); var chgSubBoardRetObj = this.EnhancedReaderChangeSubBoard(bbs.cursub_code); if (chgSubBoardRetObj.succeeded) { // Set the message index, etc. // If there are search results, then set msgIndex to the first // message. Otherwise (if there is no search specified), then // set the message index to the user's last read message. if (this.SearchingAndResultObjsDefinedForCurSub()) msgIndex = 0; else msgIndex = chgSubBoardRetObj.lastReadMsgIdx; // If the current message index is for a message that has been // deleted, then find the next non-deleted message. testMsgHdr = this.GetMsgHdrByIdx(msgIndex); if ((testMsgHdr == null) || ((testMsgHdr.attr & MSG_DELETE) == MSG_DELETE)) { // First try going forward var nonDeletedMsgIdx = this.FindNextNonDeletedMsgIdx(msgIndex, true); // If a non-deleted message was not found, then try going backward. if (nonDeletedMsgIdx == -1) nonDeletedMsgIdx = this.FindNextNonDeletedMsgIdx(msgIndex, false); // If a non-deleted message was found, then set msgIndex to it. // Otherwise, return. // Note: If there are no messages in the chosen sub-board at all, // then the error would have already been shown. if (nonDeletedMsgIdx > -1) msgIndex = nonDeletedMsgIdx; else { if (this.NumMessages() != 0) { // There are messages, but none that are not deleted. console.clear("\1n"); console.center("\1h\1yThere are no messages to display."); console.crlf(); console.pause(); } this.msgbase.close(); retObj.stoppedReading = true; return retObj; } } // Set the hotkey help line again, since the new sub-board might have // different settings for whether messages can be edited or deleted, // then refresh it on the screen. var oldHotkeyHelpLine = this.enhReadHelpLine; this.SetEnhancedReaderHelpLine(); if ((oldHotkeyHelpLine != this.enhReadHelpLine) && this.scrollingReaderInterface && console.term_supports(USER_ANSI)) this.DisplayEnhancedMsgReadHelpLine(console.screen_rows, allowChgMsgArea); } else { retObj.stoppedReading = false; return retObj; } } } else if (readMsgRetObj.nextAction == ACTION_GO_PREV_MSG_AREA) // Go to the previous message area { // The user is at the beginning of the current sub-board. if (allowChgMsgArea) { var goToPrevRetval = this.GoToPrevSubBoardForEnhReader(allowChgMsgArea); retObj.stoppedReading = goToPrevRetval.shouldStopReading; // If we're going to stop reading, then if (retObj.stoppedReading) msgIndex = 0; else if (goToPrevRetval.changedMsgArea) msgIndex = goToPrevRetval.msgIndex; } // If the caller wants this method to return instead of going to the next // sub-board with messages, then do so. if (pReturnOnNextAreaNav) return retObj; } else if (readMsgRetObj.nextAction == ACTION_GO_NEXT_MSG_AREA) // Go to the next message area { if (allowChgMsgArea && !pReturnOnNextAreaNav) { var goToNextRetval = this.GoToNextSubBoardForEnhReader(allowChgMsgArea); retObj.stoppedReading = goToNextRetval.shouldStopReading; if (retObj.stoppedReading) msgIndex = 0; else if (goToNextRetval.changedMsgArea) msgIndex = goToNextRetval.msgIndex; } // If the caller wants this method to return instead of going to the next // sub-board with messages, then do so. if (pReturnOnNextAreaNav) return retObj; } else if (readMsgRetObj.nextAction == ACTION_DISPLAY_MSG_LIST) // Display message list { // If we need to return to the caller for this, then do so. if (pReturnOnMessageList) { retObj.messageListReturn = true; return retObj; } else { // If this.reverseListOrder is the string "ASK", the user will be prompted // on the last line of the screen for whether they want to list the // messages in reverse order. So, erase the help line on the bottom of // the screen. if ((typeof(this.reverseListOrder) == "string") && (this.reverseListOrder.toUpperCase() == "ASK")) { if (this.scrollingReaderInterface && console.term_supports(USER_ANSI)) { console.gotoxy(1, console.screen_rows); console.cleartoeol("\1n"); } } // Call the ListMessages method - Don't change the sub-board, and // have it return if the user chooses a message to read. var listRetObj = this.ListMessages(null, true, pAllowChgArea); // If the user wants to quit, then stop the input loop. if (listRetObj.lastUserInput == "Q") { continueOn = false; retObj.stoppedReading = true; } // If the user chose a different message, then set the message index else if ((listRetObj.selectedMsgOffset > -1) && (listRetObj.selectedMsgOffset < this.NumMessages())) msgIndex = listRetObj.selectedMsgOffset; } } // Go to specific message & new message offset is valid: Read the new // message else if ((readMsgRetObj.nextAction == ACTION_GO_SPECIFIC_MSG) && (readMsgRetObj.newMsgOffset > -1)) { // The user selected a different message in this sub-board msgIndex = readMsgRetObj.newMsgOffset; } // Save this iteration's next action for the "previous" next action for the next iteration previousNextAction = readMsgRetObj.nextAction; } return retObj; } // For the DigDistMsgReader class: Performs the message listing, given a // sub-board code. // // Paramters: // pSubBoardCode: Optional - The internal sub-board code, or "mail" // for personal email. // pReturnOnMsgSelect: Optional - A boolean to specify whether or not to // return when a message is selected to read. Defaults // to false. // pAllowChgSubBoard: Optional - A boolean to specify whether or not to allow // changing to another sub-board. Defaults to true. // Return value: An object containing the following properties: // lastUserInput: The user's last keypress/input // selectedMsgOffset: The index of the message selected to read, // if one was selected. If none was selected, // this will be -1. function DigDistMsgReader_ListMessages(pSubBoardCode, pReturnOnMsgSelect, pAllowChgSubBoard) { var retObj = new Object(); retObj.lastUserInput = ""; retObj.selectedMsgOffset = -1; // If the passed-in sub-board code was different than what was set in the object before, // then open the new message sub-board. var previousSubBoardCode = this.subBoardCode; if (typeof(pSubBoardCode) == "string") { if (subBoardCodeIsValid(pSubBoardCode)) this.setSubBoardCode(pSubBoardCode); else { console.print("\1n\1h\1yWarning: \1wThe Message Reader connot continue because an invalid"); console.crlf(); console.print("sub-board code was specified (" + pSubBoardCode + "). Please notify the sysop."); console.crlf(); console.pause(); return retObj; } } if (this.subBoardCode.length == 0) { console.print("\1n\1h\1yWarning: \1wThe Message Reader connot continue because no message\r\n"); console.print("sub-board was specified. Please notify the sysop.\r\n\1p"); return retObj; } if (previousSubBoardCode != this.subBoardCode) { this.msgbase = null; this.msgbase = new MsgBase(this.subBoardCode); } var openSucceeded = true; if (!this.msgbase.is_open) openSucceeded = this.msgbase.open(); if (openSucceeded) { // If there are no messages to display in the current sub-board, then let the // user know and exit. if (this.NumMessages() == 0) { this.msgbase.close(); this.msgbase = null; console.clear("\1n"); console.center("\1n\1h\1yThere are no messages to display.\r\n\1p"); return retObj; } } // Construct the traditional UI pause text and the line of help text for lightbar // mode. This adds the delete and edit keys if the user is allowed to delete & edit // messages. this.SetMsgListPauseTextAndLightbarHelpLine(); // If this.reverseListOrder is the string "ASK", prompt the user for whether // they want to list the messages in reverse order. if ((typeof(this.reverseListOrder) == "string") && (this.reverseListOrder.toUpperCase() == "ASK")) { if (numMessages(bbs.cursub_code) > 0) this.reverseListOrder = !console.noyes("\1n\1cList in reverse (newest on top)"); } // List the messages using the lightbar or traditional interface, depending on // what this.msgListUseLightbarListInterface is set to. The lightbar interface requires ANSI. if (this.msgListUseLightbarListInterface && canDoHighASCIIAndANSI()) retObj = this.ListMessages_Lightbar(pReturnOnMsgSelect, pAllowChgSubBoard); else retObj = this.ListMessages_Traditional(pReturnOnMsgSelect, pAllowChgSubBoard); return retObj; } // For the DigDistMsgReader class: Performs the message listing, given a // sub-board code. This version uses a traditional user interface, prompting // the user at the end of each page to continue, quit, or read a message. // Note: This function requires this.msgbase to be valid and open. // // Parameters: // pReturnOnMsgSelect: Optional - A boolean to specify whether or not // to return when a message is selected to read. // pAllowChgSubBoard: Optional - A boolean to specify whether or not to allow // changing to another sub-board. Defaults to true. // // Return value: An object containing the following properties: // lastUserInput: The user's last keypress/input // selectedMsgOffset: The index of the message selected to read, // if one was selected. If none was selected, // this will be -1. function DigDistMsgReader_ListMessages_Traditional(pReturnOnMsgSelect, pAllowChgSubBoard) { var retObj = new Object(); retObj.lastUserInput = ""; retObj.selectedMsgOffset = -1; // Reset this.readAMessage and deniedReadingmessage to false, in case the // message listing has previously ended with them set to true. this.readAMessage = false; this.deniedReadingMessage = false; // this.msgbase must be valid before continuing. if ((typeof(this.msgbase) == "undefined") || (this.msgbase == null)) { console.center("\1n\1h\1yError: \1wUnable to list messages because the sub-board is not open.\r\n\1p"); return retObj; } else if (!this.msgbase.is_open) { console.center("\1n\1h\1yError: \1wUnable to list messages because the sub-board is not open.\r\n\1p"); return retObj; } var allowChgSubBoard = (typeof(pAllowChgSubBoard) == "boolean" ? pAllowChgSubBoard : true); // this.tradMsgListNumLines stores the maximum number of lines to write. It's the number // of rows on the user's screen - 3 to make room for the header line // at the top, the question line at the bottom, and 1 extra line at // the bottom of the screen so that displaying carriage returns // doesn't mess up the position of the header lines at the top. this.tradMsgListNumLines = console.screen_rows-3; var nListStartLine = 2; // The first line number on the screen for the message list // If we will be displaying the message group and sub-board in the // header at the top of the screen (an additional 2 lines), then // update this.tradMsgListNumLines and nListStartLine to account for this. if (this.displayBoardInfoInHeader) { this.tradMsgListNumLines -= 2; nListStartLine += 2; } // If the user's terminal doesn't support ANSI, then re-calculate // this.tradMsgListNumLines - we won't be keeping the headers at the top of the // screen. if (!canDoHighASCIIAndANSI()) // Could also be !console.term_supports(USER_ANSI) this.tradMsgListNumLines = console.screen_rows - 2; // Clear the screen and write the header at the top console.clear("\1n"); this.WriteMsgListScreenTopHeader(); // If this.tradListTopMsgIdx hasn't been set yet, then get the index of the user's // last read message and figure out which page it's on and set the top message // index accordingly. if (this.tradListTopMsgIdx == -1) this.SetUpTraditionalMsgListVars(); // Write the message list var continueOn = true; var retvalObj = null; var curpos = null; // Current character position var lastScreen = false; while (continueOn) { // Go to the top and write the current page of message information, // then update curpos. console.gotoxy(1, nListStartLine); lastScreen = this.ListScreenfulOfMessages(this.tradListTopMsgIdx, this.tradMsgListNumLines); curpos = console.getxy(); clearToEOS(curpos.y); console.gotoxy(curpos); // Prompt the user whether or not to continue or to read a message // (by message number). if (this.reverseListOrder) retvalObj = this.PromptContinueOrReadMsg((this.tradListTopMsgIdx == this.NumMessages()-1), lastScreen, pReturnOnMsgSelect, allowChgSubBoard); else retvalObj = this.PromptContinueOrReadMsg((this.tradListTopMsgIdx == 0), lastScreen, pReturnOnMsgSelect, allowChgSubBoard); retObj.lastUserInput = retvalObj.userInput; retObj.selectedMsgOffset = retvalObj.selectedMsgOffset; continueOn = retvalObj.continueOn; // TODO: Update this to use PageUp & PageDown keys for paging? It would // require updating PromptContinueOrReadMsg(), which would be non-trivial // because that method uses console.getkeys() with a list of allowed keys // and a message number limit. if (continueOn) { // If the user chose to go to the previous page of listings, // then subtract the appropriate number of messages from // this.tradListTopMsgIdx in order to do so. if (retvalObj.userInput == "P") { if (this.reverseListOrder) { this.tradListTopMsgIdx += this.tradMsgListNumLines; // If we go past the beginning, then we need to reset // msgNum so we'll be at the beginning of the list. var totalNumMessages = this.NumMessages(); if (this.tradListTopMsgIdx >= totalNumMessages) this.tradListTopMsgIdx = totalNumMessages - 1; } else { this.tradListTopMsgIdx -= this.tradMsgListNumLines; // If we go past the beginning, then we need to reset // msgNum so we'll be at the beginning of the list. if (this.tradListTopMsgIdx < 0) this.tradListTopMsgIdx = 0; } } // If the user chose to go to the next page, update // this.tradListTopMsgIdx appropriately. else if (retvalObj.userInput == "N") { if (this.reverseListOrder) this.tradListTopMsgIdx -= this.tradMsgListNumLines; else this.tradListTopMsgIdx += this.tradMsgListNumLines; } // First page else if (retvalObj.userInput == "F") { if (this.reverseListOrder) this.tradListTopMsgIdx = this.NumMessages() - 1; else this.tradListTopMsgIdx = 0; } // Last page else if (retvalObj.userInput == "L") { if (this.reverseListOrder) { this.tradListTopMsgIdx = (this.NumMessages() % this.tradMsgListNumLines) - 1; // If this.tradListTopMsgIdx is now invalid (below 0), then adjust it // to properly display the last page of messages. if (this.tradListTopMsgIdx < 0) this.tradListTopMsgIdx = this.tradMsgListNumLines - 1; } else { var totalNumMessages = this.NumMessages(); this.tradListTopMsgIdx = totalNumMessages - (totalNumMessages % this.tradMsgListNumLines); if (this.tradListTopMsgIdx >= totalNumMessages) this.tradListTopMsgIdx = totalNumMessages - this.tradMsgListNumLines; } } // D: Delete a message else if (retvalObj.userInput == "D") { if (this.CanDelete() || this.CanDeleteLastMsg()) { var msgNum = this.PromptForMsgNum({ x: curpos.x, y: curpos.y+1 }, this.text.deleteMsgNumPromptText, false, ERROR_PAUSE_WAIT_MS, false); // If the user enters a valid message number, then call the // DeleteMessage() method, which will prompt the user for // confirmation and delete the message if confirmed. if (msgNum > 0) this.PromptAndDeleteMessage(msgNum-1); // Refresh the top header on the screen for continuing to list // messages. console.clear("\1n"); this.WriteMsgListScreenTopHeader(); } } // E: Edit a message else if (retvalObj.userInput == "E") { if (this.CanEdit()) { var msgNum = this.PromptForMsgNum({ x: curpos.x, y: curpos.y+1 }, this.text.editMsgNumPromptText, false, ERROR_PAUSE_WAIT_MS, false); // If the user entered a valid message number, then let the // user edit the message. if (msgNum > 0) var returnObj = this.EditExistingMsg(msgNum-1); // Refresh the top header on the screen for continuing to list // messages. console.clear("\1n"); this.WriteMsgListScreenTopHeader(); } } // G: Go to a specific message by # (place that message on the top) else if (retvalObj.userInput == "G") { var msgNum = this.PromptForMsgNum(curpos, "\1n" + this.text.goToMsgNumPromptText, false, ERROR_PAUSE_WAIT_MS, false); if (msgNum > 0) this.tradListTopMsgIdx = msgNum - 1; // Refresh the top header on the screen for continuing to list // messages. console.clear("\1n"); this.WriteMsgListScreenTopHeader(); } // ?: Display help else if (retvalObj.userInput == "?") { console.clear("\1n"); this.DisplayMsgListHelp(allowChgSubBoard, true); console.clear("\1n"); this.WriteMsgListScreenTopHeader(); } // C: Change to another message area (sub-board) else if (retvalObj.userInput == "C") { if (allowChgSubBoard && (this.subBoardCode != "mail")) { // Store the current sub-board code so we can see if it changed var oldSubCode = bbs.cursub_code; // Let the user choose another message area. If they chose // a different message area, then set up the message base // object accordingly. this.SelectMsgArea(); if (bbs.cursub_code != oldSubCode) { var chgSubRetval = this.ChangeSubBoard(bbs.cursub_code); continueOn = chgSubRetval.succeeded; } // Update the traditional list variables and refresh the screen if (continueOn) { this.SetUpTraditionalMsgListVars(); console.clear("\1n"); this.WriteMsgListScreenTopHeader(); } } } // S: Select message(s) else if (retvalObj.userInput == "S") { // Input the message number list from the user console.print("\1n\1cNumber(s) of message(s) to select, (\1hA\1n\1c=All, \1hN\1n\1c=None, \1hENTER\1n\1c=cancel)\1g\1h: \1c"); var userNumberList = console.getstr(128, K_UPPER); // If the user entered A or N, then select/un-select all messages. // Otherwise, select only the messages that the user entered. if ((userNumberList == "A") || (userNumberList == "N")) { var messageSelectToggle = (userNumberList == "A"); var totalNumMessages = this.NumMessages(); for (var msgIdx = 0; msgIdx < totalNumMessages; ++msgIdx) this.ToggleSelectedMessage(this.subBoardCode, msgIdx, messageSelectToggle); } else { if (userNumberList.length > 0) { var numArray = parseNumberList(userNumberList); for (var numIdx = 0; numIdx < numArray.length; ++numIdx) this.ToggleSelectedMessage(this.subBoardCode, numArray[numIdx]-1); } } // Refresh the top header on the screen for continuing to list // messages. console.clear("\1n"); this.WriteMsgListScreenTopHeader(); } // Ctrl-D: Batch delete (for selected messages) else if (retvalObj.userInput == CTRL_D) { console.print("\1n"); console.crlf(); if (this.NumSelectedMessages() > 0) { // The PromptAndDeleteSelectedMessages() method will prompt the user for confirmation // to delete the message and then delete it if confirmed. this.PromptAndDeleteSelectedMessages(); // In case all messages were deleted, if that's the case, show // an appropriate message and don't continue listing messages. //if (this.NumMessages(true) == 0) if (!this.NonDeletedMessagesExist()) { continueOn = false; // Note: The following doesn't seem to be necessary, since // the ReadOrListSubBoard() method will show a message saying // there are no messages to read and then will quit out. //this.msgbase.close(); //this.msgbase = null; //console.clear("\1n"); //console.center("\1n\1h\1yThere are no messages to display."); //console.crlf(); //console.pause(); } else { // There are still messages to list, so refresh the top // header on the screen for continuing to list messages. console.clear("\1n"); this.WriteMsgListScreenTopHeader(); } } else { // There are no selected messages console.print("\1n\1h\1yThere are no selected messages."); mswait(ERROR_PAUSE_WAIT_MS); // Refresh the top header on the screen for continuing to list messages. console.clear("\1n"); this.WriteMsgListScreenTopHeader(); } } else { // If pReturnOnMsgSelect is true and the user selected a message to // read, then exit out of this input loop so we can return from // this method - The calling method will call the enhanced reader // method. if (pReturnOnMsgSelect && (retObj.selectedMsgOffset >= 0)) continueOn = false; } if (!pReturnOnMsgSelect) { // If the user chose to read a message or denied confirmation, then: // - Re-draw the column headers at the top of the screen. // - Subtract this.tradMsgListNumLines from msgNum so that this script displays // the same page where the user left off. if (this.readAMessage || this.deniedReadingMessage) { if (canDoHighASCIIAndANSI()) // Could also be console.term_supports(USER_ANSI) this.WriteMsgListScreenTopHeader(); } this.readAMessage = false; this.deniedReadingMessage = false; // If the user's terminal doesn't support ANSI, then adjust // this.tradMsgListNumLines to 1 less than the number of screen rows, because // after the first page, we no longer need to display the message // list header line. if (!canDoHighASCIIAndANSI()) // Could also be !console.term_supports(USER_ANSI) this.tradMsgListNumLines = console.screen_rows - 1; } } } return retObj; } // For the DigDistMsgReader class: Performs the message listing, given a // sub-board code. This verison uses a lightbar interface for message // navigation. Note: This function requires this.msgbase to be valid and // open. // // Parameters: // pReturnOnMsgSelect: Optional - A boolean to specify whether or not // to return when a message is selected to read. // pAllowChgSubBoard: Optional - A boolean to specify whether or not to allow // changing to another sub-board. Defaults to true. // // Return value: An object containing the following properties: // lastUserInput: The user's last keypress/input // selectedMsgOffset: The index of the message selected to read, // if one was selected. If none was selected, // this will be -1. function DigDistMsgReader_ListMessages_Lightbar(pReturnOnMsgSelect, pAllowChgSubBoard) { var retObj = new Object(); retObj.lastUserInput = ""; retObj.selectedMsgOffset = -1; // This method is only supported if the user's terminal supports // ANSI. if (!canDoHighASCIIAndANSI()) // Could also be !console.term_supports(USER_ANSI) { console.print("\r\n\1h\1ySorry, an ANSI terminal is required for this operation.\1n\1w\r\n"); console.pause(); return retObj; } // Reset this.readAMessage and deniedReadingMessage to false, in case the // message listing has previously ended with them set to true. this.readAMessage = false; this.deniedReadingMessage = false; // this.msgbase must be valid before continuing. if ((typeof(this.msgbase) == "undefined") || (this.msgbase == null)) { console.center("\1n\1h\1yError: \1wUnable to list messages because the sub-board is not open.\r\n\1p"); return retObj; } else if (!this.msgbase.is_open) { console.center("\1n\1h\1yError: \1wUnable to list messages because the sub-board is not open.\r\n\1p"); return retObj; } var allowChgSubBoard = (typeof(pAllowChgSubBoard) == "boolean" ? pAllowChgSubBoard : true); // This function will be used for displaying the help line at // the bottom of the screen. function DisplayHelpLine(pHelpLineText) { console.gotoxy(1, console.screen_rows); console.print(pHelpLineText); console.cleartoeol("\1n"); } // Clear the screen and write the header at the top console.clear("\1n"); this.WriteMsgListScreenTopHeader(); DisplayHelpLine(this.msgListLightbarModeHelpLine); // If the lightbar message list index & cursor position variables haven't been // set yet, then set them. if ((this.lightbarListTopMsgIdx == -1) || (this.lightbarListSelectedMsgIdx == -1) || (this.lightbarListCurPos == null)) { this.SetUpLightbarMsgListVars(); } // List a screenful of message headers console.gotoxy(1, this.lightbarMsgListStartScreenRow); var lastPage = this.ListScreenfulOfMessages(this.lightbarListTopMsgIdx, this.lightbarMsgListNumLines); // Move the cursor to where it needs to be console.gotoxy(this.lightbarListCurPos); // User input loop var bottomMsgIndex = 0; var userInput = ""; var msgHeader = null; var continueOn = true; while (continueOn) { bbs.command_str = ""; // To prevent weirdness retObj.selectedMsgOffset = -1; // Calculate the message number (0-based) of the message // appearing on the bottom of the screen. if (this.reverseListOrder) { bottomMsgIndex = this.lightbarListTopMsgIdx - this.lightbarMsgListNumLines + 1; if (bottomMsgIndex < 0) bottomMsgIndex = 0; } else { var totalNumMessages = this.NumMessages(); bottomMsgIndex = this.lightbarListTopMsgIdx + this.lightbarMsgListNumLines - 1; if (bottomMsgIndex >= totalNumMessages) bottomMsgIndex = totalNumMessages - 1; } // Write the current message information with highlighting colors msgHeader = this.GetMsgHdrByIdx(this.lightbarListSelectedMsgIdx); this.PrintMessageInfo(msgHeader, true, this.lightbarListSelectedMsgIdx+1); console.gotoxy(this.lightbarListCurPos); // Make sure the cursor is still in the right place // Get a key from the user (upper-case) and take appropriate action. userInput = getKeyWithESCChars(K_UPPER|K_NOCRLF|K_NOECHO|K_NOSPIN); retObj.lastUserInput = userInput; // Q: Quit if (userInput == "Q") { // Quit continueOn = false; break; } // ?: Show help else if (userInput == "?") { // Display help console.clear("\1n"); this.DisplayMsgListHelp(allowChgSubBoard, true); // Re-draw the message list on the screen console.clear("\1n"); this.WriteMsgListScreenTopHeader(); DisplayHelpLine(this.msgListLightbarModeHelpLine); console.gotoxy(1, this.lightbarMsgListStartScreenRow); lastPage = this.ListScreenfulOfMessages(this.lightbarListTopMsgIdx, this.lightbarMsgListNumLines); console.gotoxy(this.lightbarListCurPos); // Put the cursor back where it should be } // Up arrow: Highlight the previous message else if (userInput == KEY_UP) { // Make sure this.lightbarListSelectedMsgIdx is within bounds before moving down. if (this.reverseListOrder) { if (this.lightbarListSelectedMsgIdx >= this.NumMessages() - 1) continue; } else { if (this.lightbarListSelectedMsgIdx <= 0) continue; } // Print the current message information with regular colors this.PrintMessageInfo(msgHeader, false, this.lightbarListSelectedMsgIdx+1); if (this.reverseListOrder) ++this.lightbarListSelectedMsgIdx; else --this.lightbarListSelectedMsgIdx; // If the current screen row is above the first line allowed, then // move the cursor up one row. if (this.lightbarListCurPos.y > this.lightbarMsgListStartScreenRow) { console.gotoxy(1, this.lightbarListCurPos.y-1); this.lightbarListCurPos.x = 1; --this.lightbarListCurPos.y; } else { // Go onto the previous page, with the cursor highlighting // the last message on the page. if (this.reverseListOrder) this.lightbarListTopMsgIdx = this.lightbarListSelectedMsgIdx + this.lightbarMsgListNumLines - 1; else this.lightbarListTopMsgIdx = this.lightbarListSelectedMsgIdx - this.lightbarMsgListNumLines + 1; console.gotoxy(1, this.lightbarMsgListStartScreenRow); lastPage = this.ListScreenfulOfMessages(this.lightbarListTopMsgIdx, this.lightbarMsgListNumLines); console.gotoxy(1, this.lightbarMsgListStartScreenRow+this.lightbarMsgListNumLines-1); this.lightbarListCurPos.x = 1; this.lightbarListCurPos.y = this.lightbarMsgListStartScreenRow+this.lightbarMsgListNumLines-1; } } // Down arrow: Highlight the next message else if (userInput == KEY_DOWN) { // Make sure this.lightbarListSelectedMsgIdx is within bounds before moving down. if (this.reverseListOrder) { if (this.lightbarListSelectedMsgIdx <= 0) continue; } else { if (this.lightbarListSelectedMsgIdx >= this.NumMessages() - 1) continue; } // Print the current message information with regular colors this.PrintMessageInfo(msgHeader, false, this.lightbarListSelectedMsgIdx+1); if (this.reverseListOrder) --this.lightbarListSelectedMsgIdx; else ++this.lightbarListSelectedMsgIdx; // If the current screen row is below the last line allowed, then // move the cursor down one row. if (this.lightbarListCurPos.y < this.lightbarMsgListStartScreenRow+this.lightbarMsgListNumLines-1) { console.gotoxy(1, this.lightbarListCurPos.y+1); this.lightbarListCurPos.x = 1; ++this.lightbarListCurPos.y; } else { // Go onto the next page, with the cursor highlighting // the first message on the page. console.gotoxy(1, this.lightbarMsgListStartScreenRow); this.lightbarListTopMsgIdx = this.lightbarListSelectedMsgIdx; lastPage = this.ListScreenfulOfMessages(this.lightbarListTopMsgIdx, this.lightbarMsgListNumLines); // If we were on the last page, then clear the screen from // the current line to the end of the screen. if (lastPage) { this.lightbarListCurPos = console.getxy(); clearToEOS(this.lightbarListCurPos.y); // Make sure the help line is still there DisplayHelpLine(this.msgListLightbarModeHelpLine); } // Move the cursor to the top of the list console.gotoxy(1, this.lightbarMsgListStartScreenRow); this.lightbarListCurPos.x = 1; this.lightbarListCurPos.y = this.lightbarMsgListStartScreenRow; } } // HOME key: Go to the first message on the screen else if (userInput == KEY_HOME) { // Print the current message information with regular colors this.PrintMessageInfo(msgHeader, false, this.lightbarListSelectedMsgIdx+1); // Go to the first message of the current page if (this.reverseListOrder) this.lightbarListSelectedMsgIdx += (this.lightbarListCurPos.y - this.lightbarMsgListStartScreenRow); else this.lightbarListSelectedMsgIdx -= (this.lightbarListCurPos.y - this.lightbarMsgListStartScreenRow); // Move the cursor to the first message line console.gotoxy(1, this.lightbarMsgListStartScreenRow); this.lightbarListCurPos.x = 1; this.lightbarListCurPos.y = this.lightbarMsgListStartScreenRow; } // END key: Go to the last message on the screen else if (userInput == KEY_END) { // Print the current message information with regular colors this.PrintMessageInfo(msgHeader, false, this.lightbarListSelectedMsgIdx+1); // Update the selected message # this.lightbarListSelectedMsgIdx = bottomMsgIndex; // Go to the last message of the current page if (this.reverseListOrder) this.lightbarListCurPos.y = this.lightbarMsgListStartScreenRow + this.lightbarListTopMsgIdx - bottomMsgIndex; else this.lightbarListCurPos.y = this.lightbarMsgListStartScreenRow + bottomMsgIndex - this.lightbarListTopMsgIdx; console.gotoxy(this.lightbarListCurPos); } // Enter key: Select a message to read else if (userInput == KEY_ENTER) { var originalCurpos = console.getxy(); // Allow the user to read the current message. var readMsg = true; if (this.promptToReadMessage) { // Confirm with the user whether to read the message. var sReadMsgConfirmText = this.colors["readMsgConfirmColor"] + "Read message " + this.colors["readMsgConfirmNumberColor"] + +(msgHeader.offset+1) + this.colors["readMsgConfirmColor"] + ": Are you sure"; console.gotoxy(1, console.screen_rows); console.print("\1n"); console.clearline(); readMsg = console.yesno(sReadMsgConfirmText); } var repliedToMessage = false; if (readMsg) { // If there is a search specified and the search result objects are // set up for the current sub-board, then the selected message offset // should be the search result array index. Otherwise (if not // searching), the message offset should be the actual message offset // in the message base. if (this.SearchingAndResultObjsDefinedForCurSub()) retObj.selectedMsgOffset = this.lightbarListSelectedMsgIdx; else retObj.selectedMsgOffset = msgHeader.offset; if (pReturnOnMsgSelect) return retObj; else { this.readAMessage = true; console.clear("\1n"); var readRetObj = null; if (this.SearchingAndResultObjsDefinedForCurSub()) readRetObj = this.ReadMessage(this.lightbarListSelectedMsgIdx); else readRetObj = this.ReadMessage(msgHeader.offset); repliedToMessage = readRetObj.userReplied; } } else this.deniedReadingMessage = true; // Ask the user if they want to continue reading messages if (this.promptToContinueListingMessages) { continueOn = console.yesno(this.colors["afterReadMsg_ListMorePromptColor"] + "Continue listing messages"); } // If the user chose to continue reading messages, then refresh // the screen. Even if the user chooses not to read the message, // the screen needs to be re-drawn so it appears properly. if (continueOn) { console.clear("\1n"); this.WriteMsgListScreenTopHeader(); DisplayHelpLine(this.msgListLightbarModeHelpLine); console.gotoxy(1, this.lightbarMsgListStartScreenRow); // If we're dispaying in reverse order and the user replied // to the message, then we'll have to re-arrange the screen // a bit to make way for the new message that will appear // in the list. if (this.reverseListOrder && repliedToMessage) { // Make way for the new message, which will appear at the // top. ++this.lightbarListTopMsgIdx; // If the cursor is below the bottommost line displaying // messages, then advance the cursor down one position. // Otherwise, increment this.lightbarListSelectedMsgIdx (since a new message // will appear at the top, the previous selected message // will be pushed to the next page). if (this.lightbarListCurPos.y < console.screen_rows - 1) { ++originalCurpos.y; ++this.lightbarListCurPos.y; } else ++this.lightbarListSelectedMsgIdx; } lastPage = this.ListScreenfulOfMessages(this.lightbarListTopMsgIdx, this.lightbarMsgListNumLines); console.gotoxy(originalCurpos); // Put the cursor back where it should be } } // PageDown: Next page else if (userInput == KEY_PAGE_DOWN) { // Next page if (!lastPage) { if (this.reverseListOrder) this.lightbarListTopMsgIdx -= this.lightbarMsgListNumLines; else this.lightbarListTopMsgIdx += this.lightbarMsgListNumLines; this.lightbarListSelectedMsgIdx = this.lightbarListTopMsgIdx; console.gotoxy(1, this.lightbarMsgListStartScreenRow); this.lightbarListCurPos.x = 1; this.lightbarListCurPos.y = this.lightbarMsgListStartScreenRow; lastPage = this.ListScreenfulOfMessages(this.lightbarListTopMsgIdx, this.lightbarMsgListNumLines); // If we were on the last page, then clear the screen from // the current line to the end of the screen. if (lastPage) { this.lightbarListCurPos = console.getxy(); clearToEOS(this.lightbarListCurPos.y); // Make sure the help line is still there DisplayHelpLine(this.msgListLightbarModeHelpLine); } // Move the cursor back to the first message info line console.gotoxy(1, this.lightbarMsgListStartScreenRow); this.lightbarListCurPos.x = 1; this.lightbarListCurPos.y = this.lightbarMsgListStartScreenRow; } else { // The user is on the last page - Go to the last message on the page. if (this.lightbarListSelectedMsgIdx != bottomMsgIndex) { // Print the current message information with regular colors this.PrintMessageInfo(msgHeader, false, this.lightbarListSelectedMsgIdx+1); // Update the selected message # this.lightbarListSelectedMsgIdx = bottomMsgIndex; this.lightbarListCurPos.x = 1; if (this.reverseListOrder) this.lightbarListCurPos.y = this.lightbarMsgListStartScreenRow + this.lightbarListTopMsgIdx - bottomMsgIndex; else this.lightbarListCurPos.y = this.lightbarMsgListStartScreenRow + bottomMsgIndex - this.lightbarListTopMsgIdx; console.gotoxy(this.lightbarListCurPos); } } } // PageUp: Previous page else if (userInput == KEY_PAGE_UP) { var canGoToPrevious = false; if (this.reverseListOrder) canGoToPrevious = (this.lightbarListTopMsgIdx < this.NumMessages() - 1); else canGoToPrevious = (this.lightbarListTopMsgIdx > 0); if (canGoToPrevious) { if (this.reverseListOrder) this.lightbarListTopMsgIdx += this.lightbarMsgListNumLines; else this.lightbarListTopMsgIdx -= this.lightbarMsgListNumLines; this.lightbarListSelectedMsgIdx = this.lightbarListTopMsgIdx; console.gotoxy(1, this.lightbarMsgListStartScreenRow); lastPage = this.ListScreenfulOfMessages(this.lightbarListTopMsgIdx, this.lightbarMsgListNumLines); console.gotoxy(1, this.lightbarMsgListStartScreenRow); this.lightbarListCurPos.x = 1; this.lightbarListCurPos.y = this.lightbarMsgListStartScreenRow; } else { // The user is on the first page - Go to the first message on the page. if (this.lightbarListSelectedMsgIdx != 0) { // Print the current message information with regular colors this.PrintMessageInfo(msgHeader, false, this.lightbarListSelectedMsgIdx+1); // Go to the first message of the current page if (this.reverseListOrder) this.lightbarListSelectedMsgIdx += (this.lightbarListCurPos.y - this.lightbarMsgListStartScreenRow); else this.lightbarListSelectedMsgIdx -= (this.lightbarListCurPos.y - this.lightbarMsgListStartScreenRow); // Move the cursor to the first message line console.gotoxy(1, this.lightbarMsgListStartScreenRow); this.lightbarListCurPos.x = 1; this.lightbarListCurPos.y = this.lightbarMsgListStartScreenRow; } } } // F: First page else if (userInput == "F") { var canGoToFirst = false; if (this.reverseListOrder) canGoToFirst = (this.lightbarListTopMsgIdx < this.NumMessages() - 1); else canGoToFirst = (this.lightbarListTopMsgIdx > 0); if (canGoToFirst) { if (this.reverseListOrder) this.lightbarListTopMsgIdx = this.NumMessages() - 1; else this.lightbarListTopMsgIdx = 0; this.lightbarListSelectedMsgIdx = this.lightbarListTopMsgIdx; console.gotoxy(1, this.lightbarMsgListStartScreenRow); lastPage = this.ListScreenfulOfMessages(this.lightbarListTopMsgIdx, this.lightbarMsgListNumLines); console.gotoxy(1, this.lightbarMsgListStartScreenRow); this.lightbarListCurPos.x = 1; this.lightbarListCurPos.y = this.lightbarMsgListStartScreenRow; } } // L: Last page else if (userInput == "L") { if (!lastPage) { // Set the top message index. If this.lightbarListTopMsgIdx is beyond the last // message in the sub-board, then move back a full page of messages. if (this.reverseListOrder) { this.lightbarListTopMsgIdx = (this.NumMessages() % this.lightbarMsgListNumLines) - 1; // If this.lightbarListTopMsgIdx is now invalid (below 0), then adjust it // to properly display the last page of messages. if (this.lightbarListTopMsgIdx < 0) this.lightbarListTopMsgIdx = this.lightbarMsgListNumLines - 1; } else { var totalNumMessages = this.NumMessages(); this.lightbarListTopMsgIdx = totalNumMessages - (totalNumMessages % this.lightbarMsgListNumLines); if (this.lightbarListTopMsgIdx >= totalNumMessages) this.lightbarListTopMsgIdx = totalNumMessages - this.lightbarMsgListNumLines; } this.lightbarListSelectedMsgIdx = this.lightbarListTopMsgIdx; console.gotoxy(1, this.lightbarMsgListStartScreenRow); lastPage = this.ListScreenfulOfMessages(this.lightbarListTopMsgIdx, this.lightbarMsgListNumLines); // If we were on the last page, then clear the screen from // the current line to the end of the screen. if (lastPage) { this.lightbarListCurPos = console.getxy(); clearToEOS(this.lightbarListCurPos.y); // Make sure the help line is still there DisplayHelpLine(this.msgListLightbarModeHelpLine); } // Move the cursor back to the first message info line console.gotoxy(1, this.lightbarMsgListStartScreenRow); this.lightbarListCurPos.x = 1; this.lightbarListCurPos.y = this.lightbarMsgListStartScreenRow; } } // Numeric digit: The start of a number of a message to read else if (userInput.match(/[0-9]/)) { var originalCurpos = console.getxy(); // Put the user's input back in the input buffer to // be used for getting the rest of the message number. console.ungetstr(userInput); // Move the cursor to the bottom of the screen and // prompt the user for the message number. console.gotoxy(1, console.screen_rows); userInput = this.PromptForMsgNum({ x: 1, y: console.screen_rows }, this.text.readMsgNumPromptText, true, ERROR_PAUSE_WAIT_MS, false); if (userInput > 0) { // Confirm with the user whether to read the message var readMsg = true; if (this.promptToReadMessage) { var sReadMsgConfirmText = this.colors["readMsgConfirmColor"] + "Read message " + this.colors["readMsgConfirmNumberColor"] + userInput + this.colors["readMsgConfirmColor"] + ": Are you sure"; readMsg = console.yesno(sReadMsgConfirmText); } if (readMsg) { // Update the message list screen variables this.CalcMsgListScreenIdxVarsFromMsgNum(+userInput); // Let the user read the message retObj.selectedMsgOffset = userInput - 1; if (pReturnOnMsgSelect) return retObj; else { this.readAMessage = true; if (this.SearchingAndResultObjsDefinedForCurSub()) this.ReadMessage(this.lightbarListSelectedMsgIdx); else this.ReadMessage(userInput-1); } } else this.deniedReadingMessage = true; // Prompt the user whether or not to continue listing // messages. if (this.promptToContinueListingMessages) { continueOn = console.yesno(this.colors["afterReadMsg_ListMorePromptColor"] + "Continue listing messages"); } } // If the user chose to continue listing messages, then re-draw // the screen. if (continueOn) { console.clear("\1n"); this.WriteMsgListScreenTopHeader(); DisplayHelpLine(this.msgListLightbarModeHelpLine); console.gotoxy(1, this.lightbarMsgListStartScreenRow); lastPage = this.ListScreenfulOfMessages(this.lightbarListTopMsgIdx, this.lightbarMsgListNumLines); console.gotoxy(originalCurpos); // Put the cursor back where it should be } } // DEL key: Delete a message else if (userInput == KEY_DEL) { if (this.CanDelete() || this.CanDeleteLastMsg()) { var originalCurpos = console.getxy(); console.gotoxy(1, console.screen_rows); console.print("\1n"); console.clearline(); // The PromptAndDeleteMessage() method will prompt the user for confirmation // to delete the message and then delete it if confirmed. this.PromptAndDeleteMessage(this.lightbarListSelectedMsgIdx, { x: 1, y: console.screen_rows}); // In case all messages were deleted, if that's the case, show // an appropriate message and don't continue listing messages. //if (this.NumMessages(true) == 0) if (!this.NonDeletedMessagesExist()) { continueOn = false; // Note: The following doesn't seem to be necessary, since // the ReadOrListSubBoard() method will show a message saying // there are no messages to read and then will quit out. /* this.msgbase.close(); this.msgbase = null; console.clear("\1n"); console.center("\1n\1h\1yThere are no messages to display."); console.crlf(); console.pause(); */ } else { // There are still some messages to show, so refresh the screen. // Refresh the screen console.clear("\1n"); this.WriteMsgListScreenTopHeader(); DisplayHelpLine(this.msgListLightbarModeHelpLine); console.gotoxy(1, this.lightbarMsgListStartScreenRow); lastPage = this.ListScreenfulOfMessages(this.lightbarListTopMsgIdx, this.lightbarMsgListNumLines); console.gotoxy(originalCurpos); // Put the cursor back where it should be } } } // E: Edit a message else if (userInput == "E") { if (this.CanEdit()) { var originalCurpos = console.getxy(); // Ask the user if they really want to edit the message console.gotoxy(1, console.screen_rows); console.print("\1n"); console.clearline(); // Let the user edit the message //var returnObj = this.EditExistingMsg(msgHeader.offset); var returnObj = this.EditExistingMsg(this.lightbarListSelectedMsgIdx); // Refresh the screen console.clear("\1n"); this.WriteMsgListScreenTopHeader(); DisplayHelpLine(this.msgListLightbarModeHelpLine); console.gotoxy(1, this.lightbarMsgListStartScreenRow); lastPage = this.ListScreenfulOfMessages(this.lightbarListTopMsgIdx, this.lightbarMsgListNumLines); console.gotoxy(originalCurpos); // Put the cursor back where it should be } } // G: Go to a specific message by # (highlight or place that message on the top) else if (userInput == "G") { var originalCurpos = console.getxy(); // Move the cursor to the bottom of the screen and // prompt the user for a message number. console.gotoxy(1, console.screen_rows); userInput = this.PromptForMsgNum({ x: 1, y: console.screen_rows }, "\n" + this.text.goToMsgNumPromptText, true, ERROR_PAUSE_WAIT_MS, false); if (userInput > 0) { // Make sure the message number is for a valid message (i.e., it // could be an invalid message number if there is a search, where // not all message numbers are consecutive). if (this.GetMsgHdrByMsgNum(userInput) != null) { // If the message is on the current page, then just go to and // highlight it. Otherwise, set the user's selected message on the // top of the page. We also have to make sure that this.lightbarListCurPos.y and // originalCurpos.y are set correctly. Also, account for search // results if there are any (we'll need to have the correct array // index for the search results). var chosenMsgIndex = userInput - 1; if ((chosenMsgIndex <= bottomMsgIndex) && (chosenMsgIndex >= this.lightbarListTopMsgIdx)) { this.lightbarListSelectedMsgIdx = chosenMsgIndex; originalCurpos.y = this.lightbarListCurPos.y = this.lightbarListSelectedMsgIdx - this.lightbarListTopMsgIdx + this.lightbarMsgListStartScreenRow; } else { this.lightbarListTopMsgIdx = this.lightbarListSelectedMsgIdx = chosenMsgIndex; originalCurpos.y = this.lightbarListCurPos.y = this.lightbarMsgListStartScreenRow; } } else { // The user entered an invalid message number console.print("\1n" + this.text.invalidMsgNumText.replace("%d", userInput) + "\1n"); console.inkey(K_NONE, ERROR_PAUSE_WAIT_MS); } } // Clear & re-draw the screen, to fix any possible alignment problems // caused by newline output after the user inputs their choice. console.clear("\1n"); this.WriteMsgListScreenTopHeader(); DisplayHelpLine(this.msgListLightbarModeHelpLine); console.gotoxy(1, this.lightbarMsgListStartScreenRow); lastPage = this.ListScreenfulOfMessages(this.lightbarListTopMsgIdx, this.lightbarMsgListNumLines); console.gotoxy(originalCurpos); // Put the cursor back where it should be } // C: Change to another message area (sub-board) else if (userInput == "C") { if (allowChgSubBoard && (this.subBoardCode != "mail")) { // Store the current sub-board code so we can see if it changed var oldSubCode = bbs.cursub_code; // Let the user choose another message area. If they chose // a different message area, then set up the message base // object accordingly. this.SelectMsgArea(); if (bbs.cursub_code != oldSubCode) { var chgSubRetval = this.ChangeSubBoard(bbs.cursub_code); continueOn = chgSubRetval.succeeded; } // Update the lightbar list variables and refresh the screen if (continueOn) { this.SetUpLightbarMsgListVars(); console.clear("\1n"); this.WriteMsgListScreenTopHeader(); DisplayHelpLine(this.msgListLightbarModeHelpLine); // List a screenful of message headers console.gotoxy(1, this.lightbarMsgListStartScreenRow); var lastPage = this.ListScreenfulOfMessages(this.lightbarListTopMsgIdx, this.lightbarMsgListNumLines); // Move the cursor to where it needs to be console.gotoxy(this.lightbarListCurPos); } } } // Spacebar: Select a message for batch operations (such as batch // delete, etc.) else if (userInput == " ") this.ToggleSelectedMessage(this.subBoardCode, this.lightbarListSelectedMsgIdx); // Ctrl-A: Select/de-select all messages else if (userInput == CTRL_A) { var originalCurpos = console.getxy(); console.gotoxy(1, console.screen_rows); console.print("\1n"); console.clearline(); console.gotoxy(1, console.screen_rows); // Prompt the user to select All, None (un-select all), or Cancel console.print("\1n\1gSelect \1c(\1hA\1n\1c)\1gll, \1c(\1hN\1n\1c)\1gone, or \1c(\1hC\1n\1c)\1gancel: \1h\1g"); var userChoice = getAllowedKeyWithMode("ANC", K_UPPER | K_NOCRLF); if ((userChoice == "A") || (userChoice == "N")) { // Toggle all the messages var messageSelectToggle = (userChoice == "A"); var totalNumMessages = this.NumMessages(); var messageIndex = 0; for (messageIndex = 0; messageIndex < totalNumMessages; ++messageIndex) this.ToggleSelectedMessage(this.subBoardCode, messageIndex, messageSelectToggle); // Refresh the selected message checkmarks on the screen - Add the // checkmarks for messages that are selected, and write a blank space // (no checkmark) for messages that are not selected. var currentRow = this.lightbarMsgListStartScreenRow; var messageIndexEnd = this.lightbarListTopMsgIdx + this.lightbarMsgListNumLines; for (messageIndex = this.lightbarListTopMsgIdx; messageIndex < messageIndexEnd; ++messageIndex) { // Skip the current selected message because that one's checkmark // will be refreshed. Also skip this one if the message has been // marked as deleted already. if (!this.MessageIsDeleted(messageIndex) && (messageIndex != this.lightbarListSelectedMsgIdx)) { console.gotoxy(this.MSGNUM_LEN+1, currentRow); console.print("\1n"); if (this.MessageIsSelected(this.subBoardCode, messageIndex)) console.print(this.colors.selectedMsgMarkColor + CHECK_CHAR + "\1n"); else console.print(" \1n"); } ++currentRow; } } // Refresh the help line and move the cursor back to its original position console.gotoxy(1, console.screen_rows); DisplayHelpLine(this.msgListLightbarModeHelpLine); console.gotoxy(originalCurpos); } // Ctrl-D: Batch delete (for selected messages) else if (userInput == CTRL_D) { var originalCurpos = console.getxy(); if (this.NumSelectedMessages() > 0) { console.gotoxy(1, console.screen_rows); console.print("\1n"); console.clearline(); // The PromptAndDeleteSelectedMessages() method will prompt the user for confirmation // to delete the message and then delete it if confirmed. this.PromptAndDeleteSelectedMessages({ x: 1, y: console.screen_rows}); // In case all messages were deleted, if that's the case, show // an appropriate message and don't continue listing messages. //if (this.NumMessages(true) == 0) if (!this.NonDeletedMessagesExist()) { continueOn = false; // Note: The following doesn't seem to be necessary, since // the ReadOrListSubBoard() method will show a message saying // there are no messages to read and then will quit out. /* this.msgbase.close(); this.msgbase = null; console.clear("\1n"); console.center("\1n\1h\1yThere are no messages to display."); console.crlf(); console.pause(); */ } else { // There are still messages to list, so refresh the screen. console.clear("\1n"); this.WriteMsgListScreenTopHeader(); DisplayHelpLine(this.msgListLightbarModeHelpLine); console.gotoxy(1, this.lightbarMsgListStartScreenRow); lastPage = this.ListScreenfulOfMessages(this.lightbarListTopMsgIdx, this.lightbarMsgListNumLines); console.gotoxy(originalCurpos); // Put the cursor back where it should be } } else { // There are no selected messages writeWithPause(1, console.screen_rows, "\1n\1h\1yThere are no selected messages.", ERROR_PAUSE_WAIT_MS, "\1n", true); // Refresh the help line and move the cursor back to its original position DisplayHelpLine(this.msgListLightbarModeHelpLine); console.gotoxy(originalCurpos); } } } return retObj; } // For the DigDistMsgListerClass: Prints a line of information about // a message. // // Parameters: // pMsgHeader: The message header object, returned by MsgBase.get_msg_header(). // pHighlight: Optional boolean - Whether or not to highlight the line (true) or // use the standard colors (false). // pMsgNum: Optional - A number to use for the message instead of the number/offset // in the message header function DigDistMsgReader_PrintMessageInfo(pMsgHeader, pHighlight, pMsgNum) { // pMsgHeader must be a valid object. if (typeof(pMsgHeader) == "undefined") return; if (pMsgHeader == null) return; var highlight = false; if (typeof(pHighlight) == "boolean") highlight = pHighlight; // Get the message's import date & time as strings. If // this.msgList_displayMessageDateImported is true, use the message import date. // Otherwise, use the message written date. var sDate; var sTime; if (this.msgList_displayMessageDateImported) { sDate = strftime("%Y-%m-%d", pMsgHeader.when_imported_time); sTime = strftime("%H:%M:%S", pMsgHeader.when_imported_time); } else { sDate = strftime("%Y-%m-%d", pMsgHeader.when_written_time); sTime = strftime("%H:%M:%S", pMsgHeader.when_written_time); } var msgNum = (typeof(pMsgNum) == "number" ? pMsgNum : pMsgHeader.offset+1); // Determine if the message has been deleted. var msgDeleted = ((pMsgHeader.attr & MSG_DELETE) == MSG_DELETE); // msgIndicatorChar will contain (possibly) a character to display after // the message number to indicate whether it has been deleted, selected, // etc. If not, then it will just be a space. var msgIndicatorChar = " "; // Write the message header information. // Note: The message header has the following fields: // 'number': The message number // 'offset': The message offset // 'to': Who the message is directed to (string) // 'from' Who wrote the message (string) // 'subject': The message subject (string) // 'date': The date - Full text (string) // To access one of these, use brackets; i.e., msgHeader['to'] if (highlight) { if (msgDeleted) msgIndicatorChar = "\1n\1r\1h\1i" + this.colors.msgListHighlightBkgColor + "*\1n"; else if (this.MessageIsSelected(this.subBoardCode, msgNum-1)) msgIndicatorChar = "\1n" + this.colors.selectedMsgMarkColor + this.colors.msgListHighlightBkgColor + CHECK_CHAR + "\1n"; printf(this.sMsgInfoFormatHighlightStr, msgNum, msgIndicatorChar, pMsgHeader.from.substr(0, this.FROM_LEN), pMsgHeader.to.substr(0, this.TO_LEN), pMsgHeader.subject.substr(0, this.SUBJ_LEN), sDate, sTime); } else { if (msgDeleted) msgIndicatorChar = "\1n\1r\1h\1i*\1n"; else if (this.MessageIsSelected(this.subBoardCode, msgNum-1)) msgIndicatorChar = "\1n" + this.colors.selectedMsgMarkColor + CHECK_CHAR + "\1n"; // Determine whether to use the normal, "to-user", or "from-user" format string. // The differences are the colors. Then, output the message information line. var toNameUpper = pMsgHeader.to.toUpperCase(); var msgToUser = ((toNameUpper == user.alias.toUpperCase()) || (toNameUpper == user.name.toUpperCase()) || (toNameUpper == user.handle.toUpperCase())); var fromNameUpper = pMsgHeader.from.toUpperCase(); var msgIsFromUser = ((fromNameUpper == user.alias.toUpperCase()) || (fromNameUpper == user.name.toUpperCase()) || (fromNameUpper == user.handle.toUpperCase())); printf((msgToUser ? this.sMsgInfoToUserFormatStr : (msgIsFromUser ? this.sMsgInfoFromUserFormatStr : this.sMsgInfoFormatStr)), msgNum, msgIndicatorChar, pMsgHeader.from.substr(0, this.FROM_LEN), pMsgHeader.to.substr(0, this.TO_LEN), pMsgHeader.subject.substr(0, this.SUBJ_LEN), sDate, sTime); } console.cleartoeol("\1n"); // To clear away any extra text that may have been entered by the user } // For the traditional interface of DigDistMsgListerClass: Prompts the user to // continue or read a message (by number). // // Parameters: // pStart: Whether or not we're on the first page (true or false) // pEnd: Whether or not we're at the last page (true or false) // pReturnOnMsgSelect: Optional - A boolean to specify whether or not // to return when a message is selected to read. // pAllowChgSubBoard: Optional - A boolean to specify whether or not to allow // changing to another sub-board. Defaults to true. // // Return value: An object with the following properties: // continueOn: Boolean, whether or not the user wants to continue // listing the messages // userInput: The user's input // selectedMsgOffset: The offset of the message selected to read, // if one was selected. If a message was not // selected, this will be -1. function DigDistMsgReader_PromptContinueOrReadMsg(pStart, pEnd, pReturnOnMsgSelect, pAllowChgSubBoard) { // Create the return object and set some initial default values var retObj = new Object(); retObj.continueOn = true; retObj.userInput = ""; retObj.selectedMsgOffset = -1; var allowChgSubBoard = (typeof(pAllowChgSubBoard) == "boolean" ? pAllowChgSubBoard : true); var continueOn = true; // Prompt the user whether or not to continue or to read a message // (by message number). Make use of the different prompt texts, // depending whether we're at the beginning, in the middle, or at // the end of the message list. var userInput = ""; var allowedKeys = "?GS"; // ? = help, G = Go to message #, S = Select message(s), Ctrl-D: Batch delete if (allowChgSubBoard) allowedKeys += "C"; // Change to another message area if (this.CanDelete() || this.CanDeleteLastMsg()) allowedKeys += "D"; // Delete if (this.CanEdit()) allowedKeys += "E"; // Edit if (pStart && pEnd) { // This is the only page. console.print(this.msgListOnlyOnePageContinuePrompt); // Get input from the user. Allow only Q (quit). allowedKeys += "Q"; } else if (pStart) { // We're on the first page. console.print(this.sStartContinuePrompt); // Get input from the user. Allow only L (last), N (next), or Q (quit). allowedKeys += "LNQ"; } else if (pEnd) { // We're on the last page. console.print(this.sEndContinuePrompt); // Get input from the user. Allow only F (first), P (previous), or Q (quit). allowedKeys += "FPQ"; } else { // We're neither on the first nor last page. Allow F (first), L (last), // N (next), P (previous), or Q (quit). console.print(this.sContinuePrompt); allowedKeys += "FLNPQ"; } // Get the user's input. Allow CTRL-D (batch delete) without echoing it. // If the user didn't press CTRL-L, allow the keys in allowedKeys or a number from 1 // to the highest message number. userInput = console.getkey(K_NOECHO); if (userInput != CTRL_D) { console.ungetstr(userInput); userInput = console.getkeys(allowedKeys, this.HighestMessageNum()).toString(); } if (userInput == "Q") continueOn = false; // If the user has typed all numbers, then read that message. if ((userInput != "") && /^[0-9]+$/.test(userInput)) { // If the user entered a valid message number, then let the user read the message. // The message number might be invalid if there are search results that // have non-continuous message numbers. if (this.IsValidMessageNum(userInput)) { // Confirm with the user whether to read the message var readMsg = true; if (this.promptToReadMessage) { var sReadMsgConfirmText = this.colors["readMsgConfirmColor"] + "Read message " + this.colors["readMsgConfirmNumberColor"] + userInput + this.colors["readMsgConfirmColor"] + ": Are you sure"; readMsg = console.yesno(sReadMsgConfirmText); } if (readMsg) { // Update the message list screen variables this.CalcMsgListScreenIdxVarsFromMsgNum(+userInput); // Let the user read the message if (pReturnOnMsgSelect) { // Fill a return object with the required values, and return it. retObj.continueOn = continueOn; retObj.userInput = userInput; retObj.selectedMsgOffset = userInput-1; return retObj; } else { this.readAMessage = true; this.ReadMessage(userInput-1); } } else this.deniedReadingMessage = true; // Prompt the user whether or not to continue listing // messages. if (this.promptToContinueListingMessages) { continueOn = console.yesno(this.colors["afterReadMsg_ListMorePromptColor"] + "Continue listing messages"); } } else { // The user entered an invalid message number. console.print("\1n\1h\1w" + userInput + " \1y is not a valid message number.\1n"); console.crlf(); console.pause(); continueOn = true; } } // Make sure color highlighting is turned off console.print("\1n"); // Fill the return object with the required values, and return it. retObj.continueOn = continueOn; retObj.userInput = userInput; return retObj; } // For the DigDistMsgReader Class: Given a message number of a message in the // current message area, shows the message to the user and allows the user to // respond. // // Parameters: // pOffset: The offset of the message to be read // // Return value: And object with the following properties: // offsetValid: Boolean - Whether or not the passed-in offset was valid // userReplied: Boolean - Whether or not the user replied to the message. // msgbaseReOpened: Boolean - Whether or not the messagebase is open after // the user replied to the message. Will be true if // the user didn't reply to the message. function DigDistMsgReader_ReadMessage(pOffset) { var retObj = new Object(); retObj.offsetValid = true; retObj.userReplied = false; retObj.msgbaseReOpened = true; // Get the message header var msgHeader = this.GetMsgHdrByMsgNum(pOffset+1); if (msgHeader == null) { console.print("\1n" + this.text.invalidMsgNumText.replace("%d", +(pOffset+1)) + "\1n"); console.inkey(K_NONE, ERROR_PAUSE_WAIT_MS); retObj.offsetValid = false; return retObj; } // Show the message header. this.DisplaySyncMsgHeader(msgHeader); // Show the message body. Make sure the text is word-wrapped // so that it looks good. var msgText = this.msgbase.get_msg_body(true, msgHeader.offset); var msgTextWrapped = word_wrap(msgText, console.screen_columns-1); console.print("\1n" + this.colors["msgBodyColor"]); console.putmsg(msgTextWrapped, P_NOATCODES); // Hack: If the "from" name in the header is blank (as it might be sometimes), then // set it to "All". This prevents Synchronet from crashing, and it will also default // the "to" name in the user's reply to "All". if (msgHeader["from"] == "") msgHeader["from"] = "All"; // Mark the message as read, if it was written to the current // user. var msgToUpper = msgHeader["to"].toUpperCase(); if ((msgToUpper == user.alias.toUpperCase()) || (msgToUpper == user.name.toUpperCase())) { msgHeader.attr = (msgHeader.attr | MSG_READ); var wroteHeader = this.msgbase.put_msg_header(true, msgHeader.offset, msgHeader); } // If not reading personal email, then update the scan & last read message pointers. if (this.subBoardCode != "mail") // && !this.SearchTypePopulatesSearchResults() { if (msgHeader.number > msg_area.sub[this.subBoardCode].scan_ptr) msg_area.sub[this.subBoardCode].scan_ptr = msgHeader.number; msg_area.sub[this.subBoardCode].last_read = msgHeader.number; } // Allow the user to reply to the message, either publicly or privately. console.print("\1n\1cEnd of message. \1hR\1b)\1n\1ceply\1h\1b, " + "\1cP\1b)\1n\1crivate reply\1h\1b, \1cENTER\1b/\1cN\1b)\1n\1co reply\1h\1g: \1n\1c"); var userKey = console.getkeys("RPN").toString(); var privateReply = (userKey == "P"); if ((userKey == "R") || privateReply) { var replyRetObj = this.ReplyToMsg(msgHeader, msgText, privateReply, pOffset); retObj.userReplied = replyRetObj.postSucceeded; retObj.msgbaseReOpened = replyRetObj.msgbaseReOpened; } return retObj; } // For the DigDistMsgReader Class: Given a message number of a message in the // current message area, shows the message to the user and allows the user to // respond. This is an enhanced version that allows scrolling up & down the // message with the up & down arrow keys, and the left & right arrow keys will // return from the function to allow calling code to navigate back & forth // through the message sub-board. // // Parameters: // pOffset: The offset of the message to be read // pAllowChgArea: Optional boolean - Whether or not to allow changing the // message area // // Return value: And object with the following properties: // offsetValid: Boolean - Whether or not the passed-in offset was valid // msgDeleted: Boolean - Whether or not the message is marked as deleted // (not deleted by the user in the reader) // userReplied: Boolean - Whether or not the user replied to the message. // lastKeypress: The last keypress from the user - For navigation purposes // newMsgOffset: The offset of another message to read, if the user // input another message number. If the user did not // input another message number, this will be -1. // nextAction: The next action for the caller to take. This will be // one of the values specified by the ACTION_* constants. // This defaults to ACTION_NONE on error. // refreshEnhancedRdrHelpLine: Boolean - Whether or not to refresh the // enhanced reader help line on the screen // (for instance, if switched to the traditional // non-scrolling interface to read the message) function DigDistMsgReader_ReadMessageEnhanced(pOffset, pAllowChgArea) { var retObj = new Object(); retObj.offsetValid = true; retObj.msgDeleted = false; retObj.userReplied = false; retObj.lastKeypress = ""; retObj.newMsgOffset = -1; retObj.nextAction = ACTION_NONE; retObj.refreshEnhancedRdrHelpLine = false; // Get the message header var msgHeader = this.GetMsgHdrByIdx(pOffset); if (msgHeader == null) { console.print("\1n" + this.text.invalidMsgNumText.replace("%d", +(pOffset+1)) + "\1n"); console.crlf(); console.inkey(K_NONE, ERROR_PAUSE_WAIT_MS); retObj.offsetValid = false; return retObj; } // See if the message is marked as deleted. If so, don't let the // user read it, just silently return. retObj.msgDeleted = ((msgHeader.attr & MSG_DELETE) == MSG_DELETE); if (retObj.msgDeleted) return retObj; // Update the message list index variables so that the message list is in // the right spot for the message currently being read this.CalcMsgListScreenIdxVarsFromMsgNum(pOffset+1); // Check the pAllowChgArea parameter. If it's a boolean, then use it. If // not, then check to see if we're reading personal mail - If not, then allow // the user to change to a different message area. var allowChgMsgArea = true; if (typeof(pAllowChgArea) == "boolean") allowChgMsgArea = pAllowChgArea; else allowChgMsgArea = (this.subBoardCode != "mail"); // Hack: If the "from" name in the header is empty (as it might be sometimes), then // set it to "All". This prevents Synchronet from crashing, and it will also default // the "to" name in the user's reply to "All". if (msgHeader.from.length == 0) msgHeader.from = "All"; // Some key bindings var enhReaderKeys = new Object(); enhReaderKeys.prevMsgByTitle = "<"; enhReaderKeys.nextMsgByTitle = ">"; enhReaderKeys.prevMsgByAuthor = "{"; enhReaderKeys.nextMsgByAuthor = "}"; enhReaderKeys.prevMsgByToUser = "["; enhReaderKeys.nextMsgByToUser = "]"; enhReaderKeys.prevMsgByThreadID = "("; enhReaderKeys.nextMsgByThreadID = ")"; enhReaderKeys.prevSubBoard = "-"; enhReaderKeys.nextSubBoard = "+"; enhReaderKeys.downloadAttachments = CTRL_A; enhReaderKeys.saveToBBSMachine = CTRL_S; enhReaderKeys.deleteMessage = KEY_DEL; enhReaderKeys.selectMessage = " "; enhReaderKeys.batchDelete = CTRL_D; // This function converts a thread navigation key character to its // corresponding thread type value function keypressToThreadType(pKeypress) { var threadType = THREAD_BY_ID; switch (pKeypress) { case enhReaderKeys.prevMsgByTitle: case enhReaderKeys.nextMsgByTitle: threadType = THREAD_BY_TITLE; break; case enhReaderKeys.prevMsgByAuthor: case enhReaderKeys.nextMsgByAuthor: threadType = THREAD_BY_AUTHOR; break; case enhReaderKeys.prevMsgByToUser: case enhReaderKeys.nextMsgByToUser: threadType = THREAD_BY_TO_USER; break; case enhReaderKeys.prevMsgByThreadID: case enhReaderKeys.nextMsgByThreadID: default: threadType = THREAD_BY_ID; break; } return threadType; } // Get the message text and see if it has any ANSI codes. If it has ANSI codes, // then don't use the scrolling interface so that the ANSI gets displayed properly. var messageText = this.msgbase.get_msg_body(true, msgHeader.offset); // If the message has ANSI content, then use the scrolling interface only // if frame.js is available on the BBS machine and the option to use the // scrolling interface for ANSI messages is enabled. var msgHasANSICodes = textHasANSICodes(messageText); var useScrollingInterface = this.scrollingReaderInterface && console.term_supports(USER_ANSI); if (useScrollingInterface && msgHasANSICodes) useScrollingInterface = gFrameJSAvailable && this.useScrollingInterfaceForANSIMessages; // If we switch to the non-scrolling interface here, then the calling method should // refresh the enhanced reader help line on the screen. retObj.refreshEnhancedRdrHelpLine = (this.scrollingReaderInterface && !useScrollingInterface); // Use the scrollable reader interface if the setting is enabled & the user's // terminal supports ANSI. Otherwise, use a more traditional user interface. if (useScrollingInterface) { // Show the message header this.DisplayEnhancedMsgHdr(msgHeader, pOffset+1, 1); // Get the message text, interpret any @-codes in it, replace tabs with spaces // to prevent weirdness when displaying the message lines, and word-wrap the // text so that it looks good on the screen, var msgInfo = this.GetMsgInfoForEnhancedReader(msgHeader, true, true, true, messageText, msgHasANSICodes); var topMsgLineIdxForLastPage = msgInfo.topMsgLineIdxForLastPage; var msgFractionShown = msgInfo.msgFractionShown; var numSolidScrollBlocks = msgInfo.numSolidScrollBlocks; var numNonSolidScrollBlocks = msgInfo.numNonSolidScrollBlocks; var solidBlockStartRow = msgInfo.solidBlockStartRow; var solidBlockLastStartRow = solidBlockStartRow; var topMsgLineIdx = 0; var fractionToLastPage = 0; if (topMsgLineIdxForLastPage != 0) fractionToLastPage = topMsgLineIdx / topMsgLineIdxForLastPage; // Draw an initial scrollbar on the rightmost column of the message area // showing the fraction of the message shown and what part of the message // is currently being shown. The scrollbar will be updated minimally in // the input loop to minimize screen redraws. this.DisplayEnhancedReaderWholeScrollbar(solidBlockStartRow, numSolidScrollBlocks); // Input loop (for scrolling the message up & down) var msgLineFormatStr = "%-" + this.msgAreaWidth + "s"; var writeMessage = true; // msgAreaHeight, msgReaderObj, and scrollbarUpdateFunction are for use // with scrollTextLines(). var msgAreaHeight = this.msgAreaBottom - this.msgAreaTop + 1; var msgReaderObj = this; function msgScrollbarUpdateFn(pFractionToLastPage) { // Update the scrollbar position for the message, depending on the // value of pFractionToLastMessage. fractionToLastPage = pFractionToLastPage; solidBlockStartRow = msgReaderObj.msgAreaTop + Math.floor(numNonSolidScrollBlocks * pFractionToLastPage); if (solidBlockStartRow != solidBlockLastStartRow) msgReaderObj.UpdateEnhancedReaderScollbar(solidBlockStartRow, solidBlockLastStartRow, numSolidScrollBlocks); solidBlockLastStartRow = solidBlockStartRow; console.gotoxy(1, console.screen_rows); } // User input loop var continueOn = true; while (continueOn) { // Display the message lines (depending on the value of writeMessage) // and handle scroll keys via scrollTextLines(). Handle other keypresses // here. var scrollRetObj = null; if (msgInfo.displayFrame != null) { msgInfo.displayFrame.draw(); scrollRetObj = scrollFrame(msgInfo.displayFrame, msgInfo.displayFrameScrollbar, topMsgLineIdx, this.colors["msgBodyColor"], writeMessage, 1, console.screen_rows, msgScrollbarUpdateFn); } else { scrollRetObj = scrollTextLines(msgInfo.messageLines, topMsgLineIdx, this.colors["msgBodyColor"], writeMessage, this.msgAreaLeft, this.msgAreaTop, this.msgAreaWidth, msgAreaHeight, 1, console.screen_rows, msgScrollbarUpdateFn); } topMsgLineIdx = scrollRetObj.topLineIdx; retObj.lastKeypress = scrollRetObj.lastKeypress; switch (retObj.lastKeypress) { case enhReaderKeys.deleteMessage: // Delete message var originalCurpos = console.getxy(); // The 2nd to last row of the screen is where the user will // be prompted for confirmation to delete the message. // Ideally, I'd like to put the cursor on the last row of // the screen for this, but console.noyes() lets the enter // key shift everything on screen up one row, and there's // no way to avoid that. So, to optimize screen refreshing, // the cursor is placed on the 2nd to the last row on the // screen to prompt for confirmation. var promptPos = this.EnhReaderPrepLast2LinesForPrompt(); // Prompt the user for confirmation to delete the message. // Note: this.PromptAndDeleteMessage() will check to see if the user // is a sysop or the message was posted by the user. // If the message was deleted, then exit this read method // and return KEY_RIGHT as the last keypress so that the // calling method will go to the next message/sub-board. // Otherwise (if the message was not deleted), refresh the // last 2 lines of the message on the screen. var msgWasDeleted = this.PromptAndDeleteMessage(pOffset, promptPos, true, this.msgAreaWidth, true, msgInfo.attachments); if (msgWasDeleted) { var msgSearchObj = this.LookForNextOrPriorNonDeletedMsg(pOffset); continueOn = msgSearchObj.continueInputLoop; retObj.newMsgOffset = msgSearchObj.newMsgOffset; retObj.nextAction = msgSearchObj.nextAction; if (msgSearchObj.promptGoToNextArea) { if (this.EnhReaderPromptYesNo(this.text.goToNextMsgAreaPromptText, msgInfo.messageLines, topMsgLineIdx, msgLineFormatStr, solidBlockStartRow, numSolidScrollBlocks)) { // Let this method exit and let the caller go to the next sub-board continueOn = false; retObj.nextAction = ACTION_GO_NEXT_MSG; } else writeMessage = false; // No need to refresh the message } } else { this.DisplayEnhReaderError("", msgInfo.messageLines, topMsgLineIdx, msgLineFormatStr); // Move the cursor back to its original position console.gotoxy(originalCurpos); writeMessage = false; } break; case enhReaderKeys.selectMessage: // Select message (for batch delete, etc.) var originalCurpos = console.getxy(); var promptPos = this.EnhReaderPrepLast2LinesForPrompt(); if (this.EnhReaderPromptYesNo("Select this message", msgInfo.messageLines, topMsgLineIdx, msgLineFormatStr, solidBlockStartRow, numSolidScrollBlocks, true)) this.ToggleSelectedMessage(this.subBoardCode, pOffset, true); else this.ToggleSelectedMessage(this.subBoardCode, pOffset, false); writeMessage = false; // No need to refresh the message break; case enhReaderKeys.batchDelete: // TODO: Write this? Not sure yet if it makes much sense to // have batch delete in the reader interface. // Prompt the user for confirmation, and use // this.DeleteSelectedMessages() to mark the selected messages // as deleted. // Returns an object with the following properties: // deletedAll: Boolean - Whether or not all messages were successfully marked // for deletion // failureList: An object containing indexes of messages that failed to get // marked for deletion, indexed by internal sub-board code, then // containing messages indexes as properties. Reasons for failing // to mark messages deleted can include the user not having permission // to delete in a sub-board, failure to open the sub-board, etc. writeMessage = false; // No need to refresh the message break; case "E": // Edit the messaage if (this.CanEdit()) { // Move the cursor to the last line in the message area so // the edit confirmation prompt will appear there. Not using // the last line on the screen because the yes/no prompt will // output a carriage return and move everything on the screen // up one line, which is not ideal in case the user says No. var promptPos = this.EnhReaderPrepLast2LinesForPrompt(); // Let the user edit the message if they want to var editReturnObj = this.EditExistingMsg(pOffset); // If the user didn't confirm, then we only have to refresh the bottom // help line. Otherwise, we need to refresh everything on the screen. if (!editReturnObj.userConfirmed) { // For some reason, the yes/no prompt erases the last character // of the scrollbar - So, figure out which block was there and // refresh it. //var scrollBarBlock = "\1n\1h\1k" + BLOCK1; // Dim block // Dim block var scrollBarBlock = this.colors.scrollbarBGColor + this.text.scrollbarBGChar; if (solidBlockStartRow + numSolidScrollBlocks - 1 == this.msgAreaBottom) { //scrollBarBlock = "\1w" + BLOCK2; // Bright block // Bright block scrollBarBlock = this.colors.scrollbarScrollBlockColor + this.text.scrollbarScrollBlockChar; } console.gotoxy(this.msgAreaRight+1, this.msgAreaBottom); console.print(scrollBarBlock); // Refresh the last 2 message lines on the screen, then display // the key help line this.DisplayEnhReaderError("", msgInfo.messageLines, topMsgLineIdx, msgLineFormatStr); this.DisplayEnhancedMsgReadHelpLine(console.screen_rows, allowChgMsgArea); writeMessage = false; } else { // If the message was edited, then refresh the text lines // array and update the other message-related variables. if (editReturnObj.msgEdited && (editReturnObj.newMsgIdx > -1)) { // When the message is edited, the old message will be // deleted and the edited message will be posted as a new // message. So we should return to the caller and have it // go directly to that new message. this.DisplayEnhancedMsgReadHelpLine(console.screen_rows, allowChgMsgArea); continueOn = false; retObj.newMsgOffset = editReturnObj.newMsgIdx; } else { // The message was not edited. Refresh everything on the screen. // If the enhanced message header width is less than the console // width, then clear the screen to remove anything that might be // left on the screen by the message editor. if (this.enhMsgHeaderWidth < console.screen_columns) console.clear("\1n"); // Display the message header and key help line this.DisplayEnhancedMsgHdr(msgHeader, pOffset+1, 1); this.DisplayEnhancedMsgReadHelpLine(console.screen_rows, allowChgMsgArea); // Display the scrollbar again, and ensure it's in the correct position solidBlockStartRow = this.msgAreaTop + Math.floor(numNonSolidScrollBlocks * fractionToLastPage); this.DisplayEnhancedReaderWholeScrollbar(solidBlockStartRow, numSolidScrollBlocks); writeMessage = true; // We want to refresh the message on the screen } } } else writeMessage = false; // Don't write the current message again break; case "?": // Show the help screen this.DisplayEnhancedReaderHelp(allowChgMsgArea, msgInfo.attachments.length > 0); // If the enhanced message header width is less than the console // width, then clear the screen to remove anything left on the // screen from the help screen. if (this.enhMsgHeaderWidth < console.screen_columns) console.clear("\1n"); // Display the message header and key help line this.DisplayEnhancedMsgHdr(msgHeader, pOffset+1, 1); this.DisplayEnhancedMsgReadHelpLine(console.screen_rows, allowChgMsgArea); // Display the scrollbar again, and ensure it's in the correct position solidBlockStartRow = this.msgAreaTop + Math.floor(numNonSolidScrollBlocks * fractionToLastPage); this.DisplayEnhancedReaderWholeScrollbar(solidBlockStartRow, numSolidScrollBlocks); writeMessage = true; // We want to refresh the message on the screen break; case "R": // Reply to the message case "I": // Private message reply // If the user pressed P (private reply) while reading private // mail, then do nothing (allow only the "R" key to reply). var privateReply = (retObj.lastKeypress == "I"); if (privateReply && this.readingPersonalEmail) writeMessage = false; // Don't re-write the current message again else { // Let the user reply to the message. var replyRetObj = this.ReplyToMsg(msgHeader, msgInfo.msgText, privateReply, pOffset); retObj.userReplied = replyRetObj.postSucceeded; //retObj.msgDeleted = replyRetObj.msgWasDeleted; var msgWasDeleted = replyRetObj.msgWasDeleted; //if (retObj.msgDeleted) if (msgWasDeleted) { var msgSearchObj = this.LookForNextOrPriorNonDeletedMsg(pOffset); continueOn = msgSearchObj.continueInputLoop; retObj.newMsgOffset = msgSearchObj.newMsgOffset; retObj.nextAction = msgSearchObj.nextAction; if (msgSearchObj.promptGoToNextArea) { if (this.EnhReaderPromptYesNo(this.text.goToNextMsgAreaPromptText, msgInfo.messageLines, topMsgLineIdx, msgLineFormatStr, solidBlockStartRow, numSolidScrollBlocks)) { // Let this method exit and let the caller go to the next sub-board continueOn = false; retObj.nextAction = ACTION_GO_NEXT_MSG; } else writeMessage = true; // We want to refresh the message on the screen } } else { // If the messagebase object was successfully re-opened after // posting the message, then refresh the screen. Otherwise, // we'll want to quit. if (replyRetObj.msgbaseReOpened) { // If the enhanced message header width is less than the console // width, then clear the screen to remove anything left on the // screen by the message editor. if (this.enhMsgHeaderWidth < console.screen_columns) console.clear("\1n"); // Display the message header and key help line again this.DisplayEnhancedMsgHdr(msgHeader, pOffset+1, 1); this.DisplayEnhancedMsgReadHelpLine(console.screen_rows, allowChgMsgArea); // Display the scrollbar again to refresh it on the screen solidBlockStartRow = this.msgAreaTop + Math.floor(numNonSolidScrollBlocks * fractionToLastPage); this.DisplayEnhancedReaderWholeScrollbar(solidBlockStartRow, numSolidScrollBlocks); writeMessage = true; // We want to refresh the message on the screen } else { retObj.nextAction = ACTION_QUIT; continueOn = false; // Display an error console.print("\1n"); console.crlf(); console.print("\1h\1yMessagebase error after replying. Aborting.\1n"); mswait(ERROR_PAUSE_WAIT_MS); } } } break; case "P": // Post a message if (!this.readingPersonalEmail) { // Let the user post a message. if (bbs.post_msg(this.subBoardCode)) { if (searchTypePopulatesSearchResults(this.searchType)) { // TODO: If the user is doing a search, it might be // useful to search their new message and add it to // the search results if it's a match.. but maybe // not? } console.pause(); } // Refresh things on the screen // Display the message header and key help line again this.DisplayEnhancedMsgHdr(msgHeader, pOffset+1, 1); this.DisplayEnhancedMsgReadHelpLine(console.screen_rows, allowChgMsgArea); // Display the scrollbar again to refresh it on the screen solidBlockStartRow = this.msgAreaTop + Math.floor(numNonSolidScrollBlocks * fractionToLastPage); this.DisplayEnhancedReaderWholeScrollbar(solidBlockStartRow, numSolidScrollBlocks); writeMessage = true; // We want to refresh the message on the screen } else writeMessage = false; // Don't re-write the current message again break; // Numeric digit: The start of a number of a message to read case "0": case "1": case "2": case "3": case "4": case "5": case "6": case "7": case "8": case "9": var originalCurpos = console.getxy(); // Put the user's input back in the input buffer to // be used for getting the rest of the message number. console.ungetstr(retObj.lastKeypress); // Move the cursor to the 2nd to last row of the screen and // prompt the user for the message number. Ideally, I'd like // to put the cursor on the last row of the screen for this, but // console.getnum() lets the enter key shift everything on screen // up one row, and there's no way to avoid that. So, to optimize // screen refreshing, the cursor is placed on the 2nd to the last // row on the screen to prompt for the message number. var promptPos = this.EnhReaderPrepLast2LinesForPrompt(); // Prompt for the message number var msgNumInput = this.PromptForMsgNum(promptPos, this.text.readMsgNumPromptText, false, ERROR_PAUSE_WAIT_MS, false); // Only allow reading the message if the message number is valid // and it's not the same message number that was passed in. if ((msgNumInput > 0) && (msgNumInput-1 != pOffset)) { // If the message is marked as deleted, then output an error if (this.MessageIsDeleted(msgNumInput-1)) { writeWithPause(this.msgAreaLeft, console.screen_rows-1, "\1n" + this.text.msgHasBeenDeletedText.replace("%d", msgNumInput) + "\1n", ERROR_PAUSE_WAIT_MS, "\1n", true); } else { // Confirm with the user whether to read the message var readMsg = true; if (this.promptToReadMessage) { var sReadMsgConfirmText = this.colors["readMsgConfirmColor"] + "Read message " + this.colors["readMsgConfirmNumberColor"] + msgNumInput + this.colors["readMsgConfirmColor"] + ": Are you sure"; console.gotoxy(promptPos); console.print("\1n"); readMsg = console.yesno(sReadMsgConfirmText); } if (readMsg) { continueOn = false; retObj.newMsgOffset = msgNumInput - 1; retObj.nextAction = ACTION_GO_SPECIFIC_MSG; } else writeMessage = false; // Don't re-write the current message again } } else // Message number invalid or the same as what was passed in writeMessage = false; // Don't re-write the current message again // If the user chose to continue reading messages, then refresh // the last 2 message lines in the last part of the message area // and then put the cursor back to its original position. if (continueOn) { this.DisplayEnhReaderError("", msgInfo.messageLines, topMsgLineIdx, msgLineFormatStr); // Move the cursor back to its original position console.gotoxy(originalCurpos); } break; case enhReaderKeys.prevMsgByTitle: // Previous message by title case enhReaderKeys.prevMsgByAuthor: // Previous message by author case enhReaderKeys.prevMsgByToUser: // Previous message by 'to user' case enhReaderKeys.prevMsgByThreadID: // Previous message by thread ID // Only allow this if we aren't doing a message search. if (!this.SearchingAndResultObjsDefinedForCurSub()) { var threadPrevMsgOffset = this.FindThreadPrevOffset(msgHeader, keypressToThreadType(retObj.lastKeypress), true); if (threadPrevMsgOffset > -1) { retObj.newMsgOffset = threadPrevMsgOffset; retObj.nextAction = ACTION_GO_SPECIFIC_MSG; continueOn = false; } else { // Refresh the help line at the bottom of the screen //this.DisplayEnhancedMsgReadHelpLine(console.screen_rows, allowChgMsgArea); writeMessage = false; // Don't re-write the current message again } // Make sure the help line on the bottom of the screen is // drawn. this.DisplayEnhancedMsgReadHelpLine(console.screen_rows, allowChgMsgArea); } else writeMessage = false; // Don't re-write the current message again break; case enhReaderKeys.nextMsgByTitle: // Next message by title (subject) case enhReaderKeys.nextMsgByAuthor: // Next message by author case enhReaderKeys.nextMsgByToUser: // Next message by 'to user' case enhReaderKeys.nextMsgByThreadID: // Next message by thread ID // Only allow this if we aren't doing a message search. if (!this.SearchingAndResultObjsDefinedForCurSub()) { var threadPrevMsgOffset = this.FindThreadNextOffset(msgHeader, keypressToThreadType(retObj.lastKeypress), true); if (threadPrevMsgOffset > -1) { retObj.newMsgOffset = threadPrevMsgOffset; retObj.nextAction = ACTION_GO_SPECIFIC_MSG; continueOn = false; } else writeMessage = false; // Don't re-write the current message again // Make sure the help line on the bottom of the screen is // drawn. this.DisplayEnhancedMsgReadHelpLine(console.screen_rows, allowChgMsgArea); } else writeMessage = false; // Don't re-write the current message again break; case KEY_LEFT: // Previous message // Look for a prior message that isn't marked for deletion. Even // if we don't find one, we'll still want to return from this // function (with message index -1) so that this script can go // onto the previous message sub-board/group. retObj.newMsgOffset = this.FindNextNonDeletedMsgIdx(pOffset, false); // As a screen redraw optimization: Only return if there is a valid new // message offset or the user is allowed to change to a different sub-board. // Otherwise, don't return, and don't refresh the message on the screen. var goToPrevMessage = false; if ((retObj.newMsgOffset > -1) || allowChgMsgArea) { if (retObj.newMsgOffset == -1 && !curMsgSubBoardIsLast()) { goToPrevMessage = this.EnhReaderPromptYesNo(this.text.goToPrevMsgAreaPromptText, msgInfo.messageLines, topMsgLineIdx, msgLineFormatStr, solidBlockStartRow, numSolidScrollBlocks); } else { // We're not at the beginning of the sub-board, so it's okay to exit this // method and go to the previous message. goToPrevMessage = true; } } if (goToPrevMessage) { continueOn = false; retObj.nextAction = ACTION_GO_PREVIOUS_MSG; } else writeMessage = false; // No need to refresh the message break; case KEY_RIGHT: // Next message case KEY_ENTER: // Look for a later message that isn't marked for deletion. Even // if we don't find one, we'll still want to return from this // function (with message index -1) so that this script can go // onto the next message sub-board/group. retObj.newMsgOffset = this.FindNextNonDeletedMsgIdx(pOffset, true); // Note: Unlike the left arrow key, we want to exit this method when // navigating to the next message, regardless of whether or not the // user is allowed to change to a different sub-board, so that processes // that require continuation (such as new message scan) can continue. // Still, if there are no more readable messages in the current sub-board // (and thus the user would go onto the next message area), prompt the // user whether they want to continue onto the next message area. if (retObj.newMsgOffset == -1 && !curMsgSubBoardIsLast()) { // For personal mail, don't do anything, and don't refresh the // message. In a sub-board, ask the user if they want to go // to the next one. if (this.readingPersonalEmail) writeMessage = false; else { if (this.EnhReaderPromptYesNo(this.text.goToNextMsgAreaPromptText, msgInfo.messageLines, topMsgLineIdx, msgLineFormatStr, solidBlockStartRow, numSolidScrollBlocks)) { // Let this method exit and let the caller go to the next sub-board continueOn = false; retObj.nextAction = ACTION_GO_NEXT_MSG; } else writeMessage = false; // No need to refresh the message } } else { // We're not at the end of the sub-board, so it's okay to exit this // method and go to the next message. continueOn = false; retObj.nextAction = ACTION_GO_NEXT_MSG; } break; // First & last message: Quit out of this input loop and let the // calling function, this.ReadMessages(), handle the action. case "F": // First message // Only leave this function if we aren't already on the first message. if (pOffset > 0) { continueOn = false; retObj.nextAction = ACTION_GO_FIRST_MSG; } else writeMessage = false; // Don't re-write the current message again break; case "L": // Last message // Only leave this function if we aren't already on the last message. if (pOffset < this.NumMessages() - 1) { continueOn = false; retObj.nextAction = ACTION_GO_LAST_MSG; } else writeMessage = false; // Don't re-write the current message again break; case enhReaderKeys.prevSubBoard: // Go to the previous message area if (allowChgMsgArea) { continueOn = false; retObj.nextAction = ACTION_GO_PREV_MSG_AREA; } else writeMessage = false; // Don't re-write the current message again break; case enhReaderKeys.nextSubBoard: // Go to the next message area if (allowChgMsgArea || this.doingMultiSubBoardScan) { continueOn = false; retObj.nextAction = ACTION_GO_NEXT_MSG_AREA; } else writeMessage = false; // Don't re-write the current message again break; // H and K: Display the extended message header info/kludge lines // (for the sysop) case "H": case "K": if (gIsSysop) { // Save the original cursor position var originalCurPos = console.getxy(); // Get an array of the extended header info/kludge lines and then // allow the user to scroll through them. var extdHdrInfoLines = this.GetExtdMsgHdrInfo(msgHeader, (retObj.lastKeypress == "K")); if (extdHdrInfoLines.length > 0) { // Calculate information for the scrollbar for the kludge lines var infoFractionShown = this.msgAreaHeight / extdHdrInfoLines.length; if (infoFractionShown > 1) infoFractionShown = 1.0; var numInfoSolidScrollBlocks = Math.floor(this.msgAreaHeight * infoFractionShown); if (numInfoSolidScrollBlocks == 0) numInfoSolidScrollBlocks = 1; var numNonSolidInfoScrollBlocks = this.msgAreaHeight - numInfoSolidScrollBlocks; var lastInfoSolidBlockStartRow = this.msgAreaTop; // Define a scrollbar update function for the header info/kludge lines function msgInfoScrollbarUpdateFn(pFractionToLastPage) { var infoSolidBlockStartRow = msgReaderObj.msgAreaTop + Math.floor(numNonSolidInfoScrollBlocks * pFractionToLastPage); if (infoSolidBlockStartRow != lastInfoSolidBlockStartRow) msgReaderObj.UpdateEnhancedReaderScollbar(infoSolidBlockStartRow, lastInfoSolidBlockStartRow, numInfoSolidScrollBlocks); lastInfoSolidBlockStartRow = infoSolidBlockStartRow; console.gotoxy(1, console.screen_rows); } // Display the kludge lines and let the user scroll through them this.DisplayEnhancedReaderWholeScrollbar(this.msgAreaTop, numInfoSolidScrollBlocks); scrollTextLines(extdHdrInfoLines, 0, this.colors["msgBodyColor"], true, this.msgAreaLeft, this.msgAreaTop, this.msgAreaWidth, msgAreaHeight, 1, console.screen_rows, msgInfoScrollbarUpdateFn); // Display the scrollbar for the message to refresh it on the screen solidBlockStartRow = this.msgAreaTop + Math.floor(numNonSolidScrollBlocks * fractionToLastPage); this.DisplayEnhancedReaderWholeScrollbar(solidBlockStartRow, numSolidScrollBlocks); writeMessage = true; // We want to refresh the message on the screen } else { // There are no kludge lines for this message this.DisplayEnhReaderError(this.text.noKludgeLinesForThisMsgText, msgInfo.messageLines, topMsgLineIdx, msgLineFormatStr); console.gotoxy(originalCurPos); writeMessage = false; } } else // The user is not a sysop writeMessage = false; break; // Message list, change message area: Quit out of this input loop // and let the calling function, this.ReadMessages(), handle the // action. case "M": // Message list retObj.nextAction = ACTION_DISPLAY_MSG_LIST; continueOn = false; break; case "C": // Change message area, if allowed if (allowChgMsgArea) { retObj.nextAction = ACTION_CHG_MSG_AREA; continueOn = false; } else writeMessage = false; // No need to refresh the message break; case enhReaderKeys.downloadAttachments: // Download attachments if (msgInfo.attachments.length > 0) { console.print("\1n"); console.gotoxy(1, console.screen_rows); console.crlf(); console.print("\1c- Download Attached Files -\1n"); // Note: sendAttachedFiles() will output a CRLF at the beginning. sendAttachedFiles(msgInfo.attachments); // Refresh things on the screen console.clear("\1n"); // Display the message header and key help line again this.DisplayEnhancedMsgHdr(msgHeader, pOffset+1, 1); this.DisplayEnhancedMsgReadHelpLine(console.screen_rows, allowChgMsgArea); // Display the scrollbar again to refresh it on the screen solidBlockStartRow = this.msgAreaTop + Math.floor(numNonSolidScrollBlocks * fractionToLastPage); this.DisplayEnhancedReaderWholeScrollbar(solidBlockStartRow, numSolidScrollBlocks); writeMessage = true; // We want to refresh the message on the screen } else writeMessage = false; break; case enhReaderKeys.saveToBBSMachine: // Save the message to the BBS machine - Only allow this // if the user is a sysop. if (gIsSysop) { // Prompt the user for a filename to save the message to the // BBS machine var promptPos = this.EnhReaderPrepLast2LinesForPrompt(); console.print("\1n\1cFilename:\1h"); var inputLen = console.screen_columns - 10; // 10 = "Filename:" length + 1 var filename = console.getstr(inputLen, K_NOCRLF); console.print("\1n"); if (filename.length > 0) { //var saveMsgRetObj = this.SaveMsgToFile(msgHeader, filename, true, msgInfo.messageLines); var saveMsgRetObj = this.SaveMsgToFile(msgHeader, filename, true); console.gotoxy(promptPos); console.cleartoeol("\1n"); console.gotoxy(promptPos); if (saveMsgRetObj.succeeded) console.print("\1n\1cThe message has been saved.\1n"); else console.print("\1n\1y\1hFailed: " + saveMsgRetObj.errorMsg + "\1n"); mswait(ERROR_PAUSE_WAIT_MS); } else { console.gotoxy(promptPos); console.print("\1n\1y\1hMessage not exported\1n"); mswait(ERROR_PAUSE_WAIT_MS); } // Refresh the last 2 lines of the message on the screen to overwrite // the file save prompt this.DisplayEnhReaderError("", msgInfo.messageLines, topMsgLineIdx, msgLineFormatStr); writeMessage = false; // Don't write the whole message again } else writeMessage = false; break; case "Q": // Quit retObj.nextAction = ACTION_QUIT; continueOn = false; break; default: writeMessage = false; break; } } } else { // Use the non-scrolling interface. // Separate the message text from any attachments in the message. var msgAndAttachmentInfo = determineMsgAttachments(msgHeader, messageText, true); // Only interpret @-codes if the user is reading personal email. There // are many @-codes that do some action such as move the cursor, execute a // script, etc., and I don't want users on message networks to do anything // malicious to users on other BBSes. if (this.readingPersonalEmail) msgAndAttachmentInfo.msgText = replaceAtCodesInStr(msgAndAttachmentInfo.msgText); // Or this.ParseMsgAtCodes(msgAndAttachmentInfo.msgText, msgHeader) to replace only some @ codes var msgTextWrapped = word_wrap(msgAndAttachmentInfo.msgText, console.screen_columns-1); // Generate the key help text var keyHelpText = "\1n\1c\1h#\1n\1b, \1c\1hLeft\1n\1b, \1c\1hRight\1n\1b, "; if (this.CanDelete() || this.CanDeleteLastMsg()) keyHelpText += "\1c\1hDEL\1b, "; if (this.CanEdit()) keyHelpText += "\1c\1hE\1y)\1n\1cdit\1b, "; keyHelpText += "\1c\1hF\1y)\1n\1cirst\1b, \1c\1hL\1y)\1n\1cast\1b, \1c\1hR\1y)\1n\1ceply\1b, "; // If the user is allowed to change to a different message area, then // include that option. if (allowChgMsgArea) { // If there's room for the private reply option, then include that // before the change area option. if (console.screen_columns >= 89) keyHelpText += "\1c\1hP\1y)\1n\1crivate reply\1b, "; keyHelpText += "\1c\1hC\1y)\1n\1chg area\1b, "; } else { // The user isn't allowed to change to a different message area. // Go ahead and include the private reply option. keyHelpText += "\1c\1hP\1y)\1n\1crivate reply\1b, "; } keyHelpText += "\1c\1hQ\1y)\1n\1cuit\1b, \1c\1h?\1g: \1c"; // User input loop var writeMessage = true; var writePromptText = true; var continueOn = true; while (continueOn) { if (writeMessage) { if (console.term_supports(USER_ANSI)) console.clear("\1n"); // Write the message header & message body to the screen this.DisplayEnhancedMsgHdr(msgHeader, pOffset+1, 1); console.print("\1n" + this.colors["msgBodyColor"]); console.putmsg(msgTextWrapped, P_NOATCODES); } // Write the prompt text if (writePromptText) console.print(keyHelpText); // Default the writing of the message & input prompt to true for the // next iteration. writeMessage = true; writePromptText = true; // Input a key from the user and take action based on the keypress. retObj.lastKeypress = getKeyWithESCChars(K_UPPER/*|K_NOCRLF|K_NOECHO|K_NOSPIN*/); switch (retObj.lastKeypress) { case enhReaderKeys.deleteMessage: // Delete message console.crlf(); // Prompt the user for confirmation to delete the message. // Note: this.PromptAndDeleteMessage() will check to see if the user // is a sysop or the message was posted by the user. // If the message was deleted, then exit this read method // and return KEY_RIGHT as the last keypress so that the // calling method will go to the next message/sub-board. // Otherwise (if the message was not deleted), refresh the // last 2 lines of the message on the screen. // TODO: For the DeleteMessage() call, pass the array of file // attachments for it to delete (i.e., msgInfo.attachments) var msgWasDeleted = this.PromptAndDeleteMessage(pOffset); if (msgWasDeleted) { var msgSearchObj = this.LookForNextOrPriorNonDeletedMsg(pOffset); continueOn = msgSearchObj.continueInputLoop; retObj.newMsgOffset = msgSearchObj.newMsgOffset; retObj.nextAction = msgSearchObj.nextAction; if (msgSearchObj.promptGoToNextArea) { if (console.yesno(this.text.goToNextMsgAreaPromptText)) { // Let this method exit and let the caller go to the next sub-board continueOn = false; retObj.nextAction = ACTION_GO_NEXT_MSG; } else writeMessage = false; // No need to refresh the message } } break; case enhReaderKeys.selectMessage: // Select message (for batch delete, etc.) console.crlf(); var selectMessage = !console.noyes("Select this message"); this.ToggleSelectedMessage(this.subBoardCode, pOffset, selectMessage); break; case enhReaderKeys.batchDelete: // TODO: Write this? Not sure yet if it makes much sense to // have batch delete in the reader interface. // Prompt the user for confirmation, and use // this.DeleteSelectedMessages() to mark the selected messages // as deleted. // Returns an object with the following properties: // deletedAll: Boolean - Whether or not all messages were successfully marked // for deletion // failureList: An object containing indexes of messages that failed to get // marked for deletion, indexed by internal sub-board code, then // containing messages indexes as properties. Reasons for failing // to mark messages deleted can include the user not having permission // to delete in a sub-board, failure to open the sub-board, etc. writeMessage = false; // No need to refresh the message break; case "E": // Edit the message if (this.CanEdit()) { console.crlf(); // Let the user edit the message if they want to var editReturnObj = this.EditExistingMsg(pOffset); // If the user confirmed editing the message, then see if the // message was edited and refresh the screen accordingly. if (editReturnObj.userConfirmed) { // If the message was edited, then refresh the text lines // array and update the other message-related variables. if (editReturnObj.msgEdited && (editReturnObj.newMsgIdx > -1)) { // When the message is edited, the old message will be // deleted and the edited message will be posted as a new // message. So we should return to the caller and have it // go directly to that new message. continueOn = false; retObj.newMsgOffset = editReturnObj.newMsgIdx; } } } else { writeMessage = false; writePromptText = false; } break; case "?": // Show help if (!console.term_supports(USER_ANSI)) { console.crlf(); console.crlf(); } this.DisplayEnhancedReaderHelp(allowChgMsgArea, msgAndAttachmentInfo.attachments.length > 0); if (!console.term_supports(USER_ANSI)) { console.crlf(); console.crlf(); } break; case "R": // Reply to the message case "I": // Private reply // If the user pressed P (private reply) while reading private // mail, then do nothing (allow only the "R" key to reply). // If not reading personal email, go ahead and let the user reply // with either the "P" or "R" keypress. var privateReply = (retObj.lastKeypress == "I"); if (privateReply && this.readingPersonalEmail) { writeMessage = false; // Don't re-write the current message again writePromptText = false; // Don't write the prompt text again } else { console.crlf(); var replyRetObj = this.ReplyToMsg(msgHeader, msgAndAttachmentInfo.msgText, privateReply, pOffset); retObj.userReplied = replyRetObj.postSucceeded; //retObj.msgDeleted = replyRetObj.msgWasDeleted; var msgWasDeleted = replyRetObj.msgWasDeleted; if (msgWasDeleted) { var msgSearchObj = this.LookForNextOrPriorNonDeletedMsg(pOffset); continueOn = msgSearchObj.continueInputLoop; retObj.newMsgOffset = msgSearchObj.newMsgOffset; retObj.nextAction = msgSearchObj.nextAction; if (msgSearchObj.promptGoToNextArea) { if (console.yesno(this.text.goToNextMsgAreaPromptText)) { // Let this method exit and let the caller go to the next sub-board continueOn = false; retObj.nextAction = ACTION_GO_NEXT_MSG; } else writeMessage = true; // We want to refresh the message on the screen } } else { // If the messagebase object was not successfully re-opened // after posting the message, then we'll want to quit. if (!replyRetObj.msgbaseReOpened) { retObj.nextAction = ACTION_QUIT; continueOn = false; // Display an error console.print("\1n"); console.crlf(); console.print("\1h\1yMessagebase error after replying. Aborting.\1n"); mswait(ERROR_PAUSE_WAIT_MS); } } } break; case "P": // Post a message if (!this.readingPersonalEmail) { // Let the user post a message. if (bbs.post_msg(this.subBoardCode)) { // TODO: If the user is doing a search, it might be // useful to search their new message and add it to // the search results if it's a match.. but maybe // not? } console.pause(); // We'll want to refresh the message & prompt text on the screen writeMessage = true; writePromptText = true; } else { // Don't write the current message or prompt text in the next iteration writeMessage = false; writePromptText = false; } break; // Numeric digit: The start of a number of a message to read case "0": case "1": case "2": case "3": case "4": case "5": case "6": case "7": case "8": case "9": console.crlf(); // Put the user's input back in the input buffer to // be used for getting the rest of the message number. console.ungetstr(retObj.lastKeypress); // Prompt for the message number var msgNumInput = this.PromptForMsgNum(null, this.text.readMsgNumPromptText, false, ERROR_PAUSE_WAIT_MS, false); // Only allow reading the message if the message number is valid // and it's not the same message number that was passed in. if ((msgNumInput > 0) && (msgNumInput-1 != pOffset)) { // If the message is marked as deleted, then output an error if (this.MessageIsDeleted(msgNumInput-1)) { console.crlf(); console.print("\1n" + this.text.msgHasBeenDeletedText.replace("%d", msgNumInput) + "\1n"); console.crlf(); console.pause(); } else { // Confirm with the user whether to read the message var readMsg = true; if (this.promptToReadMessage) { readMsg = console.yesno("\1n" + this.colors["readMsgConfirmColor"] + "Read message " + this.colors["readMsgConfirmNumberColor"] + msgNumInput + this.colors["readMsgConfirmColor"] + ": Are you sure"); } if (readMsg) { continueOn = false; retObj.newMsgOffset = msgNumInput - 1; retObj.nextAction = ACTION_GO_SPECIFIC_MSG; } } } break; case enhReaderKeys.prevMsgByTitle: // Previous message by title case enhReaderKeys.prevMsgByAuthor: // Previous message by author case enhReaderKeys.prevMsgByToUser: // Previous message by 'to user' case enhReaderKeys.prevMsgByThreadID: // Previous message by thread ID // Only allow this if we aren't doing a message search. if (!this.SearchingAndResultObjsDefinedForCurSub()) { console.crlf(); // For the "Searching..." text var threadPrevMsgOffset = this.FindThreadPrevOffset(msgHeader, keypressToThreadType(retObj.lastKeypress), false); if (threadPrevMsgOffset > -1) { retObj.newMsgOffset = threadPrevMsgOffset; retObj.nextAction = ACTION_GO_SPECIFIC_MSG; continueOn = false; } } else { writeMessage = false; writePromptText = false; } break; case enhReaderKeys.nextMsgByTitle: // Next message by title (subject) case enhReaderKeys.nextMsgByAuthor: // Next message by author case enhReaderKeys.nextMsgByToUser: // Next message by 'to user' case enhReaderKeys.nextMsgByThreadID: // Next message by thread ID // Only allow this if we aren't doing a message search. if (!this.SearchingAndResultObjsDefinedForCurSub()) { console.crlf(); // For the "Searching..." text var threadNextMsgOffset = this.FindThreadNextOffset(msgHeader, keypressToThreadType(retObj.lastKeypress), false); if (threadNextMsgOffset > -1) { retObj.newMsgOffset = threadNextMsgOffset; retObj.nextAction = ACTION_GO_SPECIFIC_MSG; continueOn = false; } } else { writeMessage = false; writePromptText = false; } break; case KEY_LEFT: // Previous message // TODO: Change the key for this? // Look for a prior message that isn't marked for deletion. Even // if we don't find one, we'll still want to return from this // function (with message index -1) so that this script can go // onto the previous message sub-board/group. retObj.newMsgOffset = this.FindNextNonDeletedMsgIdx(pOffset, false); var goToPrevMessage = false; if ((retObj.newMsgOffset > -1) || allowChgMsgArea) { if (retObj.newMsgOffset == -1 && !curMsgSubBoardIsLast()) { console.crlf(); goToPrevMessage = console.yesno(this.text.goToPrevMsgAreaPromptText); } else { // We're not at the beginning of the sub-board, so it's okay to exit this // method and go to the previous message. goToPrevMessage = true; } } if (goToPrevMessage) { continueOn = false; retObj.nextAction = ACTION_GO_PREVIOUS_MSG; } break; case KEY_RIGHT: // Next message case KEY_ENTER: // Look for a later message that isn't marked for deletion. Even // if we don't find one, we'll still want to return from this // function (with message index -1) so that this script can go // onto the next message sub-board/group. retObj.newMsgOffset = this.FindNextNonDeletedMsgIdx(pOffset, true); // Note: Unlike the left arrow key, we want to exit this method when // navigating to the next message, regardless of whether or not the // user is allowed to change to a different sub-board, so that processes // that require continuation (such as new message scan) can continue. // Still, if there are no more readable messages in the current sub-board // (and thus the user would go onto the next message area), prompt the // user whether they want to continue onto the next message area. if (retObj.newMsgOffset == -1 && !curMsgSubBoardIsLast()) { console.crlf(); if (console.yesno(this.text.goToNextMsgAreaPromptText)) { // Let this method exit and let the caller go to the next sub-board continueOn = false; retObj.nextAction = ACTION_GO_NEXT_MSG; } } else { // We're not at the end of the sub-board, so it's okay to exit this // method and go to the next message. continueOn = false; retObj.nextAction = ACTION_GO_NEXT_MSG; } break; case "F": // First message // Only leave this function if we aren't already on the first message. if (pOffset > 0) { continueOn = false; retObj.nextAction = ACTION_GO_FIRST_MSG; } else { writeMessage = false; writePromptText = false; } break; case "L": // Last message // Only leave this function if we aren't already on the last message. if (pOffset < this.NumMessages() - 1) { continueOn = false; retObj.nextAction = ACTION_GO_LAST_MSG; } else { writeMessage = false; writePromptText = false; } break; case "-": // Go to the previous message area if (allowChgMsgArea) { continueOn = false; retObj.nextAction = ACTION_GO_PREV_MSG_AREA; } else { writeMessage = false; writePromptText = false; } break; case "+": // Go to the next message area if (allowChgMsgArea || this.doingMultiSubBoardScan) { continueOn = false; retObj.nextAction = ACTION_GO_NEXT_MSG_AREA; } else { writeMessage = false; writePromptText = false; } break; // H and K: Display the extended message header info/kludge lines // (for the sysop) case "H": case "K": if (gIsSysop) { console.crlf(); // Get an array of the extended header info/kludge lines and then // display them. var extdHdrInfoLines = this.GetExtdMsgHdrInfo(msgHeader, (retObj.lastKeypress == "K")); if (extdHdrInfoLines.length > 0) { console.crlf(); for (var infoIter = 0; infoIter < extdHdrInfoLines.length; ++infoIter) { console.print(extdHdrInfoLines[infoIter]); console.crlf(); } console.pause(); } else { // There are no kludge lines for this message console.print(this.text.noKludgeLinesForThisMsgText); console.crlf(); console.pause(); } } else // The user is not a sysop { writeMessage = false; writePromptText = false; } break; // Message list, change message area: Quit out of this input loop // and let the calling function, this.ReadMessages(), handle the // action. case "M": // Message list retObj.nextAction = ACTION_DISPLAY_MSG_LIST; continueOn = false; break; case "C": // Change message area, if allowed if (allowChgMsgArea) { retObj.nextAction = ACTION_CHG_MSG_AREA; continueOn = false; } else { writeMessage = false; writePromptText = false; } break; case enhReaderKeys.downloadAttachments: // Download attachments if (msgAndAttachmentInfo.attachments.length > 0) { console.print("\1n"); console.crlf(); console.print("\1c- Download Attached Files -\1n"); // Note: sendAttachedFiles() will output a CRLF at the beginning. sendAttachedFiles(msgAndAttachmentInfo.attachments); // Ensure the message is refreshed on the screen writeMessage = true; writePromptText = true; } else { writeMessage = false; writePromptText = false; } break; case enhReaderKeys.saveToBBSMachine: // Save the message to the BBS machine - Only allow this // if the user is a sysop. if (gIsSysop) { console.crlf(); console.print("\1n\1cFilename:\1h"); var inputLen = console.screen_columns - 10; // 10 = "Filename:" length + 1 var filename = console.getstr(inputLen, K_NOCRLF); console.print("\1n"); console.crlf(); if (filename.length > 0) { var saveMsgRetObj = this.SaveMsgToFile(msgHeader, filename, true); if (saveMsgRetObj.succeeded) console.print("\1n\1cThe message has been saved.\1n"); else console.print("\1n\1y\1hFailed: " + saveMsgRetObj.errorMsg + "\1n"); mswait(ERROR_PAUSE_WAIT_MS); } else { console.print("\1n\1y\1hMessage not exported\1n"); mswait(ERROR_PAUSE_WAIT_MS); } writeMessage = true; } else writeMessage = false; break; case "Q": // Quit retObj.nextAction = ACTION_QUIT; continueOn = false; break; default: // No need to do anything writeMessage = false; writePromptText = false; break; } } } // Mark the message as read if it was written to the current user or if // the user is reading personal email. if (userNameHandleAliasMatch(msgHeader.to)) { msgHeader.attr |= MSG_READ; this.msgbase.put_msg_header(true, msgHeader.offset, msgHeader); if (this.SearchTypePopulatesSearchResults()) this.RefreshSearchResultMsgHdr(pOffset, MSG_READ); } // If not reading personal email, then update the scan & last read message pointers. if (this.subBoardCode != "mail") // && !this.SearchTypePopulatesSearchResults() { if (msgHeader.number > msg_area.sub[this.subBoardCode].scan_ptr) msg_area.sub[this.subBoardCode].scan_ptr = msgHeader.number; msg_area.sub[this.subBoardCode].last_read = msgHeader.number; } return retObj; } // DigDistMsgReader_ReadMessageEnhanced // For the DigDistMsgReader class: For the enhanced reader method - Prepares the // last 2 lines on the screen for propmting the user for something. // // Return value: An object containing x and y values representing the cursor // position, ready to prompt the user. function DigDistMsgReader_EnhReaderPrepLast2LinesForPrompt() { var promptPos = { x: this.msgAreaLeft, y: this.msgAreaBottom }; // Write a line of characters above where the prompt will be placed, // to help get the user's attention. console.gotoxy(promptPos.x, promptPos.y-1); console.print("\1n" + this.colors.enhReaderPromptSepLineColor); for (var lineCounter = 0; lineCounter < this.msgAreaWidth; ++lineCounter) console.print(HORIZONTAL_SINGLE); // Clear the inside of the message area, so as not to overwrite // the scrollbar character console.print("\1n"); console.gotoxy(promptPos); for (var lineCounter = 0; lineCounter < this.msgAreaWidth; ++lineCounter) console.print(" "); // Position the cursor at the prompt location console.gotoxy(promptPos); return promptPos; } // For the DigDistMsgReader class: For the enhanced reader method - Looks for a // later method that isn't marked for deletion. If none is found, looks for a // prior message that isn't marked for deletion. // // Parameters: // pOffset: The offset of the message to start at // // Return value: An object with the following properties: // newMsgOffset: The offset of the next readable message // nextAction: The next action (code) for the enhanced reader // continueInputLoop: Boolean - Whether or not to continue the input loop // promptGoToNextArea: Boolean - Whether or not to prompt the user to go // to the next message area function DigDistMsgReader_LookForNextOrPriorNonDeletedMsg(pOffset) { var retObj = new Object(); retObj.newMsgOffset = 0; retObj.nextAction = ACTION_NONE; retObj.continueInputLoop = true; retObj.promptGoToNextArea = false; // Look for a later message that isn't marked for deletion. // If none is found, then look for a prior message that isn't // marked for deletion. retObj.newMsgOffset = this.FindNextNonDeletedMsgIdx(pOffset, true); if (retObj.newMsgOffset > -1) { retObj.continueInputLoop = false; retObj.nextAction = ACTION_GO_NEXT_MSG; } else { // No later message found, so look for a prior message. retObj.newMsgOffset = this.FindNextNonDeletedMsgIdx(pOffset, false); if (retObj.newMsgOffset > -1) { retObj.continueInputLoop = false; retObj.nextAction = ACTION_GO_PREVIOUS_MSG; } else { // No prior message found. We'll want to return from the enhanced // reader function (with message index -1) so that this script can // go onto the next message sub-board/group. Also, set the next // action such that the calling method will go on to the next // message/sub-board. if (!curMsgSubBoardIsLast()) { if (this.readingPersonalEmail) { retObj.continueInputLoop = false; retObj.nextAction = ACTION_QUIT; } else retObj.promptGoToNextArea = true; } else { // We're not at the end of the sub-board or the current sub-board // is the last, so go ahead and exit. retObj.continueInputLoop = false; retObj.nextAction = ACTION_GO_NEXT_MSG; } } } return retObj; } // For the DigDistMsgReader class: Writes the help line for enhanced reader // mode. // // Parameters: // pScreenRow: Optional - The screen row to write the help line on. If not // specified, the last row on the screen will be used. // pDisplayChgAreaOpt: Optional boolean - Whether or not to show the "change area" option. // Defaults to true. function DigDistMsgReader_DisplayEnhancedMsgReadHelpLine(pScreenRow, pDisplayChgAreaOpt) { var displayChgAreaOpt = (typeof(pDisplayChgAreaOpt) == "boolean" ? pDisplayChgAreaOpt : true); // Move the cursor to the desired location on the screen and display the help line console.gotoxy(1, typeof(pScreenRow) == "number" ? pScreenRow : console.screen_rows); console.print(displayChgAreaOpt ? this.enhReadHelpLine : this.enhReadHelpLineWithoutChgArea); } // For the DigDistMsgReader class: Goes back to the prior readable sub-board // (accounting for search results, etc.). Changes the object's subBoardCode, // msgbase object, etc. // // Parameters: // pAllowChgMsgArea: Boolean - Whether or not the user is allowed to change // to another message area // Return value: An object with the following properties: // changedMsgArea: Boolean - Whether or not this method successfully // changed to a prior message area // msgIndex: The message index for the new sub-board. Will be -1 // if there is no new sub-board or otherwise invalid // scenario. // shouldStopReading: Whether or not the script should stop letting // the user read messages function DigDistMsgReader_GoToPrevSubBoardForEnhReader(pAllowChgMsgArea) { var retObj = new Object(); retObj.changedMsgArea = false; retObj.msgIndex = -1; retObj.shouldStopReading = false; // Only allow this if pAllowChgMsgArea is true and we're not reading personal // email. If we're reading personal email, then msg_area.sub is unavailable // for the "mail" internal code. if (pAllowChgMsgArea && (this.subBoardCode != "mail")) { // continueGoingToPrevSubBoard specifies whether or not to continue // going to the previous sub-boards in case there is search text // specified. var continueGoingToPrevSubBoard = true; while (continueGoingToPrevSubBoard) { // Allow going to the previous message sub-board/group. var msgGrpIdx = msg_area.sub[this.subBoardCode].grp_index; var subBoardIdx = msg_area.sub[this.subBoardCode].index; var readMsgRetObj = findNextOrPrevNonEmptySubBoard(msgGrpIdx, subBoardIdx, false); // If a different sub-board was found, then go to that sub-board. if (readMsgRetObj.foundSubBoard && readMsgRetObj.subChanged) { bbs.cursub = 0; bbs.curgrp = readMsgRetObj.grpIdx; bbs.cursub = readMsgRetObj.subIdx; // Open the new sub-board this.msgbase.close(); this.setSubBoardCode(readMsgRetObj.subCode); this.msgbase = new MsgBase(this.subBoardCode); if (this.msgbase.open()) { if (this.searchType == SEARCH_NONE || !this.SearchingAndResultObjsDefinedForCurSub()) { continueGoingToPrevSubBoard = false; // No search results, so don't keep going to the previous sub-board. // Go to the user's last read message. If the message index ends up // below 0, then go to the last message not marked as deleted. retObj.msgIndex = this.AbsMsgNumToIdx(msg_area.sub[this.subBoardCode].last_read); if (retObj.msgIndex >= 0) retObj.changedMsgArea = true; else { // Look for the last message not marked as deleted var nonDeletedMsgIdx = this.FindNextNonDeletedMsgIdx(this.NumMessages(), false); // If a non-deleted message was found, then set retObj.msgIndex to it. // Otherwise, tell the user there are no messages in this sub-board // and return. if (nonDeletedMsgIdx > -1) retObj.msgIndex = nonDeletedMsgIdx; else retObj.msgIndex = this.NumMessages() - 1; // Shouldn't get here var newLastRead = this.IdxToAbsMsgNum(retObj.msgIndex); if (newLastRead > -1) msg_area.sub[this.subBoardCode].last_read = newLastRead; } } // Set the hotkey help line again, as this sub-board might have // different settings for whether messages can be edited or deleted, // then refresh it on the screen. var oldHotkeyHelpLine = this.enhReadHelpLine; this.SetEnhancedReaderHelpLine(); // If a search is is specified that would populate the search // results, then populate this.msgSearchHdrs for the current // sub-board if there is search text specified. If there // are no search results, then ask the user if they want // to continue searching the message areas. if (this.SearchTypePopulatesSearchResults()) { if (this.PopulateHdrsIfSearch_DispErrorIfNoMsgs(false, true, false)) { retObj.changedMsgArea = true; continueGoingToPrevSubBoard = false; retObj.msgIndex = this.NumMessages() - 1; if (this.scrollingReaderInterface && console.term_supports(USER_ANSI)) this.DisplayEnhancedMsgReadHelpLine(console.screen_rows, pAllowChgMsgArea); } else // No search results in this sub-board { continueGoingToPrevSubBoard = !console.noyes("Continue searching"); if (!continueGoingToPrevSubBoard) { retObj.shouldStopReading = true; return retObj; } } } else { retObj.changedMsgArea = true; if ((oldHotkeyHelpLine != this.enhReadHelpLine) && this.scrollingReaderInterface && console.term_supports(USER_ANSI)) this.DisplayEnhancedMsgReadHelpLine(console.screen_rows, pAllowChgMsgArea); } } else // The message base failed to open { console.clear("\1n"); console.print("\1h\1y* \1wUnable to open message sub-board:"); console.crlf(); console.print(subBoardGrpAndName(this.subBoardCode)); console.crlf(); console.pause(); retObj.shouldStopReading = true; continueGoingToPrevSubBoard = false; return retObj; } } else { // Didn't find a prior sub-board with readable messages. // We could stop and exit the script here by doing the following, // but I'd rather let the user exit when they want to. continueGoingToPrevSubBoard = false; // Show a message telling the user that there are no prior // messages or sub-boards. Then, refresh the hotkey help line. writeWithPause(this.msgAreaLeft, console.screen_rows, "\1n\1h\1y* No prior messages or no message in prior message areas.", ERROR_PAUSE_WAIT_MS, "\1n", true); if (this.scrollingReaderInterface && console.term_supports(USER_ANSI)) this.DisplayEnhancedMsgReadHelpLine(console.screen_rows, pAllowChgMsgArea); } } } return retObj; } // For the DigDistMsgReader class: Goes to the next readable sub-board // (accounting for search results, etc.). Changes the object's subBoardCode, // msgbase object, etc. // // Parameters: // pAllowChgMsgArea: Boolean - Whether or not the user is allowed to change // to another message area // Return value: An object with the following properties: // changedMsgArea: Boolean - Whether or not this method successfully // changed to a prior message area // msgIndex: The message index for the new sub-board. Will be -1 // if there is no new sub-board or otherwise invalid // scenario. // shouldStopReading: Whether or not the script should stop letting // the user read messages function DigDistMsgReader_GoToNextSubBoardForEnhReader(pAllowChgMsgArea) { var retObj = new Object(); retObj.changedMsgArea = false; retObj.msgIndex = -1; retObj.shouldStopReading = false; // Only allow this if pAllowChgMsgArea is true and we're not reading personal // email. If we're reading personal email, then msg_area.sub is unavailable // for the "mail" internal code. if (pAllowChgMsgArea && (this.subBoardCode != "mail")) { // continueGoingToNextSubBoard specifies whether or not to continue // advancing to the next sub-boards in case there is search text // specified. var continueGoingToNextSubBoard = true; while (continueGoingToNextSubBoard) { // Allow going to the next message sub-board/group. var msgGrpIdx = msg_area.sub[this.subBoardCode].grp_index; var subBoardIdx = msg_area.sub[this.subBoardCode].index; var readMsgRetObj = findNextOrPrevNonEmptySubBoard(msgGrpIdx, subBoardIdx, true); // If a different sub-board was found, then go to that sub-board. if (readMsgRetObj.foundSubBoard && readMsgRetObj.subChanged) { retObj.msgIndex = 0; bbs.cursub = 0; bbs.curgrp = readMsgRetObj.grpIdx; bbs.cursub = readMsgRetObj.subIdx; // Open the new sub-board this.msgbase.close(); this.setSubBoardCode(readMsgRetObj.subCode); this.msgbase = new MsgBase(this.subBoardCode); if (this.msgbase.open()) { if ((this.searchType == SEARCH_NONE) || !this.SearchingAndResultObjsDefinedForCurSub()) { continueGoingToNextSubBoard = false; // No search results, so don't keep going to the next sub-board. // Go to the user's last read message. If the message index ends up // below 0, then go to the first message not marked as deleted. retObj.msgIndex = this.AbsMsgNumToIdx(msg_area.sub[this.subBoardCode].last_read); if (retObj.msgIndex >= 0) retObj.changedMsgArea = true; else { // Set the index of the message to display - Look for the // first message not marked as deleted var nonDeletedMsgIdx = this.FindNextNonDeletedMsgIdx(this.NumMessages()-1, true); // If a non-deleted message was found, then set retObj.msgIndex to it. // Otherwise, tell the user there are no messages in this sub-board // and return. if (nonDeletedMsgIdx > -1) { retObj.msgIndex = nonDeletedMsgIdx; retObj.changedMsgArea = true; var newLastRead = this.IdxToAbsMsgNum(nonDeletedMsgIdx); if (newLastRead > -1) msg_area.sub[this.subBoardCode].last_read = newLastRead; } } } // Set the hotkey help line again, as this sub-board might have // different settings for whether messages can be edited or deleted, // then refresh it on the screen. var oldHotkeyHelpLine = this.enhReadHelpLine; this.SetEnhancedReaderHelpLine(); // If a search is is specified that would populate the search // results, then populate this.msgSearchHdrs for the current // sub-board if there is search text specified. If there // are no search results, then ask the user if they want // to continue searching the message areas. if (this.SearchTypePopulatesSearchResults()) { if (this.PopulateHdrsIfSearch_DispErrorIfNoMsgs(false, true, false)) { retObj.changedMsgArea = true; continueGoingToNextSubBoard = false; retObj.msgIndex = 0; if (this.scrollingReaderInterface && console.term_supports(USER_ANSI)) this.DisplayEnhancedMsgReadHelpLine(console.screen_rows, pAllowChgMsgArea); } else // No search results in this sub-board { continueGoingToNextSubBoard = !console.noyes("Continue searching"); if (!continueGoingToNextSubBoard) { retObj.shouldStopReading = true; return retObj; } } } } else // The message base failed to open { console.clear("\1n"); console.print("\1h\1y* \1wUnable to open message sub-board:"); console.crlf(); console.print(subBoardGrpAndName(this.subBoardCode)); console.crlf(); console.pause(); retObj.shouldStopReading = true; continueGoingToNextSubBoard = false; return retObj; } } else { // Didn't find later sub-board with readable messages. // We could stop and exit the script here by doing the following, // but I'd rather let the user exit when they want to. //retObj.shouldStopReading = true; //return retObj; continueGoingToNextSubBoard = false; // Show a message telling the user that there are no more // messages or sub-boards. Then, refresh the hotkey help line. writeWithPause(this.msgAreaLeft, console.screen_rows, "\1n\1h\1y* No more messages or message areas.", ERROR_PAUSE_WAIT_MS, "\1n", true); if (this.scrollingReaderInterface && console.term_supports(USER_ANSI)) this.DisplayEnhancedMsgReadHelpLine(console.screen_rows, pAllowChgMsgArea); } } } return retObj; } // For the DigDistMsgReader Class: Prepares the variables that keep track of the // traditional-interface message list position, current messsage number, etc. function DigDistMsgReader_SetUpTraditionalMsgListVars() { // If a search is specified, then just start at the first message. // If no search is specified, then get the index of the user's last read // message. Then, figure out which page it's on and set the lightbar list // index & cursor position variables accordingly. var lastReadMsgIdx = 0; if (!this.SearchingAndResultObjsDefinedForCurSub()) { lastReadMsgIdx = this.GetLastReadMsgIdx(); if (lastReadMsgIdx == -1) lastReadMsgIdx = 0; } var pageNum = findPageNumOfItemNum(lastReadMsgIdx+1, this.tradMsgListNumLines, this.NumMessages(), this.reverseListOrder); this.CalcTraditionalMsgListTopIdx(pageNum); if (!this.reverseListOrder && (this.tradListTopMsgIdx > lastReadMsgIdx)) this.tradListTopMsgIdx -= this.tradMsgListNumLines; } // For the DigDistMsgReader Class: Prepares the variables that keep track of the // lightbar message list position, current messsage number, etc. function DigDistMsgReader_SetUpLightbarMsgListVars() { // If no search is specified or if reading personal email, then get the index // of the user's last read message. Then, figure out which page it's on and // set the lightbar list index & cursor position variables accordingly. var lastReadMsgIdx = 0; if (!this.SearchingAndResultObjsDefinedForCurSub() || this.readingPersonalEmail) { lastReadMsgIdx = this.GetLastReadMsgIdx(); if (lastReadMsgIdx == -1) lastReadMsgIdx = 0; } else { // A search was specified. If reading personal email, then set the // message index to the last read message. if (this.readingPersonalEmail) { lastReadMsgIdx = this.GetLastReadMsgIdx(); if (lastReadMsgIdx == -1) lastReadMsgIdx = 0; } } var pageNum = findPageNumOfItemNum(lastReadMsgIdx+1, this.lightbarMsgListNumLines, this.NumMessages(), this.reverseListOrder); this.CalcLightbarMsgListTopIdx(pageNum); var initialCursorRow = 0; if (this.reverseListOrder) initialCursorRow = this.lightbarMsgListStartScreenRow+(this.lightbarListTopMsgIdx-lastReadMsgIdx); else { if (this.lightbarListTopMsgIdx > lastReadMsgIdx) this.lightbarListTopMsgIdx -= this.lightbarMsgListNumLines; initialCursorRow = this.lightbarMsgListStartScreenRow+(lastReadMsgIdx-this.lightbarListTopMsgIdx); } this.lightbarListSelectedMsgIdx = lastReadMsgIdx; this.lightbarListCurPos = { x: 1, y: initialCursorRow }; } // For the DigDistMsgReader Class: Writes the message list column headers at the // top of the screen. function DigDistMsgReader_WriteMsgListScreenTopHeader() { console.home(); // If we will be displaying the message group and sub-board in the // header at the top of the screen (an additional 2 lines), then // update nMaxLines and nListStartLine to account for this. if (this.displayBoardInfoInHeader && canDoHighASCIIAndANSI()) // console.term_supports(USER_ANSI) { var curpos = console.getxy(); // Figure out the message group name var msgGroupName = ""; // For the message group name, we can also use this.msgbase.cfg.grp_name in // Synchronet 3.12 and higher. if (this.msgbase.cfg != null) msgGroupName = msg_area.grp_list[this.msgbase.cfg.grp_number].description; else msgGroupName = "Unspecified"; // Figure out the sub-board name var subBoardName = ""; if (this.msgbase.cfg != null) subBoardName = this.msgbase.cfg.description; else if ((this.msgbase.subnum == -1) || (this.msgbase.subnum == 65535)) subBoardName = "Electronic Mail"; else subBoardName = "Unspecified"; // Display the message group name console.print(this.colors["msgListHeaderMsgGroupTextColor"] + "Msg group: " + this.colors["msgListHeaderMsgGroupNameColor"] + msgGroupName); console.cleartoeol(); // Fill to the end of the line with the current colors // Display the sub-board name on the next line ++curpos.y; console.gotoxy(curpos); console.print(this.colors["msgListHeaderSubBoardTextColor"] + "Sub-board: " + this.colors["msgListHeaderMsgSubBoardName"] + subBoardName); console.cleartoeol(); // Fill to the end of the line with the current colors ++curpos.y; console.gotoxy(curpos); } // Write the message listing column headers printf(this.colors["msgListColHeader"] + this.sHdrFormatStr, "Msg#", "From", "To", "Subject", "Date", "Time"); // Set the normal text attribute console.print("\1n"); } // For the DigDistMsgReader Class: Lists a screenful of message header information. // // Parameters: // pTopIndex: The index (offset) of the top message // pMaxLines: The maximum number of lines to output to the screen // // Return value: Boolean, whether or not the last message output to the // screen is the last message in the sub-board. function DigDistMsgReader_ListScreenfulOfMessages(pTopIndex, pMaxLines) { var atLastPage = false; var curpos = console.getxy(); var msgIndex = 0; if (this.reverseListOrder) { var endIndex = pTopIndex - pMaxLines + 1; // The index of the last message to display for (msgIndex = pTopIndex; (msgIndex >= 0) && (msgIndex >= endIndex); --msgIndex) { // The following line which sets console.line_counter to 0 is a // kludge to disable Synchronet's automatic pausing after a // screenful of text, so that this script can have more control // over screen pausing. console.line_counter = 0; // Get the message header (it will be a MsgHeader object) and // display it. msgHeader = this.GetMsgHdrByIdx(msgIndex); if (msgHeader == null) continue; // Display the message info this.PrintMessageInfo(msgHeader, false, msgIndex+1); if (console.term_supports(USER_ANSI)) { ++curpos.y; console.gotoxy(curpos); } else console.crlf(); } atLastPage = (msgIndex < 0); } else { var endIndex = pTopIndex + pMaxLines; // One past the last message index to display for (msgIndex = pTopIndex; (msgIndex < this.NumMessages()) && (msgIndex < endIndex); ++msgIndex) { // The following line which sets console.line_counter to 0 is a // kludge to disable Synchronet's automatic pausing after a // screenful of text, so that this script can have more control // over screen pausing. console.line_counter = 0; // Get the message header (it will be a MsgHeader object) and // display it. msgHeader = this.GetMsgHdrByIdx(msgIndex); if (msgHeader == null) continue; // Display the message info this.PrintMessageInfo(msgHeader, false, msgIndex+1); if (console.term_supports(USER_ANSI)) { ++curpos.y; console.gotoxy(curpos); } else console.crlf(); } atLastPage = (msgIndex == this.NumMessages()); } return atLastPage; } // For the DigDistMsgReader Class: Displays the help screen for the message list. // // Parameters: // pChgSubBoardAllowed: Whether or not changing to another sub-board is allowed // pPauseAtEnd: Boolean, whether or not to pause at the end. function DigDistMsgReader_DisplayMsgListHelp(pChgSubBoardAllowed, pPauseAtEnd) { DisplayProgramInfo(); // Display help specific to which interface is being used. if (this.msgListUseLightbarListInterface) this.DisplayLightbarMsgListHelp(false, pChgSubBoardAllowed); else this.DisplayTraditionalMsgListHelp(false, pChgSubBoardAllowed); // If pPauseAtEnd is true, then output a newline and // prompt the user whether or not to continue. if (pPauseAtEnd) console.pause(); } // For the DigDistMsgReader Class: Displays help for the traditional-interface // message list // // Parameters: // pDisplayHeader: Whether or not to display a help header at the beginning // pChgSubBoardAllowed: Whether or not changing to another sub-board is allowed // pPauseAtEnd: Boolean, whether or not to pause at the end. function DigDistMsgReader_DisplayTraditionalMsgListHelp(pDisplayHeader, pChgSubBoardAllowed, pPauseAtEnd) { // If pDisplayHeader is true, then display the program information. if (pDisplayHeader) DisplayProgramInfo(); // Display information about the current sub-board and search results. console.print("\1n\1cCurrently reading \1g" + subBoardGrpAndName(this.subBoardCode)); console.crlf(); // If the user isn't reading personal messages (i.e., is reading a sub-board), // then output the total number of messages in the sub-board. We probably // shouldn't output the total number of messages in the "mail" area, because // that includes more than the current user's email. if (this.subBoardCode != "mail") { console.print("\1n\1cThere are a total of \1g" + this.msgbase.total_msgs + " \1cmessages in the current area."); console.crlf(); } // If there is currently a search (which also includes personal messages), // then output the number of search results/personal messages. if (this.SearchingAndResultObjsDefinedForCurSub()) { var numSearchResults = this.NumMessages(); var resultsWord = (numSearchResults > 1 ? "results" : "result"); console.print("\1n\1c"); if (this.readingPersonalEmail) console.print("You have \1g" + numSearchResults + " \1c" + (numSearchResults == 1 ? "message" : "messages") + "."); else { if (numSearchResults == 1) console.print("There is \1g1 \1csearch result."); else console.print("There are \1g" + numSearchResults + " \1csearch results."); } console.crlf(); } console.crlf(); console.print("\1n" + this.colors["tradInterfaceHelpScreenColor"]); displayTextWithLineBelow("Page navigation and message selection", false, this.colors["tradInterfaceHelpScreenColor"], "\1k\1h"); console.print(this.colors["tradInterfaceHelpScreenColor"]); console.print("The message lister will display a page of message header information. At\r\n"); console.print("the end of each page, a prompt is displayed, allowing you to navigate to\r\n"); console.print("the next page, previous page, first page, or the last page. If you would\r\n"); console.print("like to read a message, you may type the message number, followed by\r\n"); console.print("the enter key if the message number is short. To quit the listing, press\r\n"); console.print("the Q key.\r\n\r\n"); this.DisplayMessageListNotesHelp(); console.crlf(); console.crlf(); displayTextWithLineBelow("Summary of the keyboard commands:", false, this.colors["tradInterfaceHelpScreenColor"], "\1k\1h"); console.print(this.colors["tradInterfaceHelpScreenColor"]); console.print("\1n\1h\1cN" + this.colors["tradInterfaceHelpScreenColor"] + ": Go to the next page\r\n"); console.print("\1n\1h\1cP" + this.colors["tradInterfaceHelpScreenColor"] + ": Go to the previous page\r\n"); console.print("\1n\1h\1cF" + this.colors["tradInterfaceHelpScreenColor"] + ": Go to the first page\r\n"); console.print("\1n\1h\1cL" + this.colors["tradInterfaceHelpScreenColor"] + ": Go to the last page\r\n"); console.print("\1n\1h\1cG" + this.colors["tradInterfaceHelpScreenColor"] + ": Go to a specific message by number (the message will appear at the top\r\n" + " of the list)\r\n"); console.print("\1n\1h\1cNumber" + this.colors["tradInterfaceHelpScreenColor"] + ": Read the message corresponding with that number\r\n"); //console.print("The following commands are available only if you have permission to do so:\r\n"); if (this.CanDelete() || this.CanDeleteLastMsg()) console.print("\1n\1h\1cD" + this.colors["tradInterfaceHelpScreenColor"] + ": Mark a message for deletion\r\n"); if (this.CanEdit()) console.print("\1n\1h\1cE" + this.colors["tradInterfaceHelpScreenColor"] + ": Edit an existing message\r\n"); if (pChgSubBoardAllowed) console.print("\1n\1h\1cC" + this.colors["tradInterfaceHelpScreenColor"] + ": Change to another message sub-board\r\n"); console.print("\1n\1h\1cS" + this.colors["tradInterfaceHelpScreenColor"] + ": Select messages (for batch delete, etc.)\r\n"); console.print("\1n" + this.colors["tradInterfaceHelpScreenColor"] + " A message number or multiple numbers can be entered separated by commas or\r\n"); console.print("\1n" + this.colors["tradInterfaceHelpScreenColor"] + " spaces. Additionally, a range of numbers (separated by a dash) can be used.\r\n"); console.print("\1n" + this.colors["tradInterfaceHelpScreenColor"] + " Examples:\r\n"); console.print("\1n" + this.colors["tradInterfaceHelpScreenColor"] + " 125\r\n"); console.print("\1n" + this.colors["tradInterfaceHelpScreenColor"] + " 1,2,3\r\n"); console.print("\1n" + this.colors["tradInterfaceHelpScreenColor"] + " 1 2 3\r\n"); console.print("\1n" + this.colors["tradInterfaceHelpScreenColor"] + " 1,2,10-20\r\n"); console.print("\1n\1h\1cCTRL-D" + this.colors["tradInterfaceHelpScreenColor"] + ": Batch delete selected messages\r\n"); console.print("\1n\1h\1cQ" + this.colors["tradInterfaceHelpScreenColor"] + ": Quit\r\n"); console.print("\1n\1h\1c?" + this.colors["tradInterfaceHelpScreenColor"] + ": Show this help screen\r\n\r\n"); // If pPauseAtEnd is true, then output a newline and // prompt the user whether or not to continue. if (pPauseAtEnd) console.pause(); } // For the DigDistMsgReader Class: Displays help for the lightbar message list // // Parameters: // pDisplayHeader: Whether or not to display a help header at the beginning // pChgSubBoardAllowed: Whether or not changing to another sub-board is allowed // pPauseAtEnd: Boolean, whether or not to pause at the end. function DigDistMsgReader_DisplayLightbarMsgListHelp(pDisplayHeader, pChgSubBoardAllowed, pPauseAtEnd) { // If pDisplayHeader is true, then display the program information. if (pDisplayHeader) DisplayProgramInfo(); // Display information about the current sub-board and search results. console.print("\1n\1cCurrently reading \1g" + subBoardGrpAndName(this.subBoardCode)); console.crlf(); // If the user isn't reading personal messages (i.e., is reading a sub-board), // then output the total number of messages in the sub-board. We probably // shouldn't output the total number of messages in the "mail" area, because // that includes more than the current user's email. if (this.subBoardCode != "mail") { console.print("\1n\1cThere are a total of \1g" + this.msgbase.total_msgs + " \1cmessages in the current area."); console.crlf(); } // If there is currently a search (which also includes personal messages), // then output the number of search results/personal messages. if (this.SearchingAndResultObjsDefinedForCurSub()) { var numSearchResults = this.NumMessages(); var resultsWord = (numSearchResults > 1 ? "results" : "result"); console.print("\1n\1c"); if (this.readingPersonalEmail) console.print("You have \1g" + numSearchResults + " \1c" + (numSearchResults == 1 ? "message" : "messages") + "."); else { if (numSearchResults == 1) console.print("There is \1g1 \1csearch result."); else console.print("There are \1g" + numSearchResults + " \1csearch results."); } console.crlf(); } console.crlf(); displayTextWithLineBelow("Lightbar interface: Page navigation and message selection", false, this.colors["tradInterfaceHelpScreenColor"], "\1k\1h"); console.print(this.colors["tradInterfaceHelpScreenColor"]); console.print("The message lister will display a page of message header information. You\r\n"); console.print("may use the up and down arrows to navigate the list of messages. The\r\n"); console.print("currently-selected message will be highlighted as you navigate through\r\n"); console.print("the list. To read a message, navigate to the desired message and press\r\n"); console.print("the enter key. You can also read a message by typing its message number.\r\n"); console.print("To quit out of the message list, press the Q key.\r\n\r\n"); this.DisplayMessageListNotesHelp(); console.crlf(); console.crlf(); displayTextWithLineBelow("Summary of the keyboard commands:", false, this.colors["tradInterfaceHelpScreenColor"], "\1k\1h"); console.print(this.colors["tradInterfaceHelpScreenColor"]); console.print("\1n\1h\1cDown arrow" + this.colors["tradInterfaceHelpScreenColor"] + ": Move the cursor down/select the next message\r\n"); console.print("\1n\1h\1cUp arrow" + this.colors["tradInterfaceHelpScreenColor"] + ": Move the cursor up/select the previous message\r\n"); console.print("\1n\1h\1cN" + this.colors["tradInterfaceHelpScreenColor"] + ": Go to the next page\r\n"); console.print("\1n\1h\1cP" + this.colors["tradInterfaceHelpScreenColor"] + ": Go to the previous page\r\n"); console.print("\1n\1h\1cF" + this.colors["tradInterfaceHelpScreenColor"] + ": Go to the first page\r\n"); console.print("\1n\1h\1cL" + this.colors["tradInterfaceHelpScreenColor"] + ": Go to the last page\r\n"); console.print("\1n\1h\1cG" + this.colors["tradInterfaceHelpScreenColor"] + ": Go to a specific message by number (the message will be highlighted and\r\n" + " may appear at the top of the list)\r\n"); console.print("\1n\1h\1cENTER" + this.colors["tradInterfaceHelpScreenColor"] + ": Read the selected message\r\n"); console.print("\1n\1h\1cNumber" + this.colors["tradInterfaceHelpScreenColor"] + ": Read the message corresponding with that number\r\n"); if (this.CanDelete() || this.CanDeleteLastMsg()) console.print("\1n\1h\1cDEL" + this.colors["tradInterfaceHelpScreenColor"] + ": Mark the selected message for deletion\r\n"); if (this.CanEdit()) console.print("\1n\1h\1cE" + this.colors["tradInterfaceHelpScreenColor"] + ": Edit the selected message\r\n"); if (pChgSubBoardAllowed) console.print("\1n\1h\1cC" + this.colors["tradInterfaceHelpScreenColor"] + ": Change to another message sub-board\r\n"); console.print("\1n\1h\1cSpacebar" + this.colors["tradInterfaceHelpScreenColor"] + ": Select message (for batch delete, etc.)\r\n"); console.print("\1n\1h\1cCTRL-A" + this.colors["tradInterfaceHelpScreenColor"] + ": Select/de-select all messages\r\n"); console.print("\1n\1h\1cCTRL-D" + this.colors["tradInterfaceHelpScreenColor"] + ": Batch delete selected messages\r\n"); console.print("\1n\1h\1cQ" + this.colors["tradInterfaceHelpScreenColor"] + ": Quit\r\n"); console.print("\1n\1h\1c?" + this.colors["tradInterfaceHelpScreenColor"] + ": Show this help screen\r\n"); // If pPauseAtEnd is true, then pause. if (pPauseAtEnd) console.pause(); } // For the DigDistMsgReader class: Displays the message list notes for the // help screens. function DigDistMsgReader_DisplayMessageListNotesHelp() { displayTextWithLineBelow("Notes about the message list:", false, this.colors["tradInterfaceHelpScreenColor"], "\1n\1k\1h") console.print(this.colors["tradInterfaceHelpScreenColor"]); console.print("If a message has been marked for deletion, it will appear with a blinking\r\n"); console.print("red asterisk (\1n\1h\1r\1i*" + "\1n" + this.colors["tradInterfaceHelpScreenColor"] + ") in"); console.print(" after the message number in the message list."); } // For the DigDistMsgReader Class: Sets the traditional UI pause prompt text // strings, sLightbarModeHelpLine, the text string for the lightbar help line, // for the message lister interface. This checks with this.msgbase to determine // if the user is allowed to delete or edit messages, and if so, adds the // appropriate keys to the prompt & help text. function DigDistMsgReader_SetMsgListPauseTextAndLightbarHelpLine() { /* var helpLineHotkeyColor = "\1r"; var helpLineNormalColor = "\1b"; var helpLineParenColor = "\1m"; */ /* this.colors.lightbarMsgListHelpLineGeneralColor this.colors.lightbarMsgListHelpLineHotkeyColor this.colors.lightbarMsgListHelpLineParenColor */ if ((this.msgbase != null) && (this.msgbase.is_open)) { // Set the traditional UI pause prompt text. // If the user can delete messages, then append D as a valid key. // If the user can edit messages, then append E as a valid key. this.sStartContinuePrompt = "\1n\1c(" + this.colors["tradInterfaceContPromptHotkeyColor"] + "N\1n\1c)" + this.colors["tradInterfaceContPromptMainColor"] + "ext, \1n\1c(" + this.colors["tradInterfaceContPromptHotkeyColor"] + "L\1n\1c)" + this.colors["tradInterfaceContPromptMainColor"] + "ast, \1n\1c(" + this.colors["tradInterfaceContPromptHotkeyColor"] + "G\1n\1c)" + this.colors["tradInterfaceContPromptMainColor"] + "o, "; if (this.CanDelete() || this.CanDeleteLastMsg()) { this.sStartContinuePrompt += "\1n\1c(" + this.colors["tradInterfaceContPromptHotkeyColor"] + "D\1n\1c)" + this.colors["tradInterfaceContPromptMainColor"] + "el, "; } if (this.CanEdit()) { this.sStartContinuePrompt += "\1n\1c(" + this.colors["tradInterfaceContPromptHotkeyColor"] + "E\1n\1c)" + this.colors["tradInterfaceContPromptMainColor"] + "dit, "; } this.sStartContinuePrompt += "\1n\1c(" + this.colors["tradInterfaceContPromptHotkeyColor"] + "S\1n\1c)" + this.colors["tradInterfaceContPromptMainColor"] + "el, "; this.sStartContinuePrompt += "\1n\1c(" + this.colors["tradInterfaceContPromptHotkeyColor"] + "Q\1n\1c)" + this.colors["tradInterfaceContPromptMainColor"] + "uit, msg" + this.colors["tradInterfaceContPromptHotkeyColor"] + "#" + this.colors["tradInterfaceContPromptMainColor"] + ", " + this.colors["tradInterfaceContPromptHotkeyColor"] + "?" + this.colors["tradInterfaceContPromptMainColor"] + ": " + this.colors["tradInterfaceContPromptUserInputColor"]; this.sContinuePrompt = "\1n\1c(" + this.colors["tradInterfaceContPromptHotkeyColor"] + "N\1n\1c)" + this.colors["tradInterfaceContPromptMainColor"] + "ext, \1c(" + this.colors["tradInterfaceContPromptHotkeyColor"] + "P\1n\1c)" + this.colors["tradInterfaceContPromptMainColor"] + "rev, \1n\1c(" + this.colors["tradInterfaceContPromptHotkeyColor"] + "F\1n\1c)" + this.colors["tradInterfaceContPromptMainColor"] + "irst, \1n\1c(" + this.colors["tradInterfaceContPromptHotkeyColor"] + "L\1n\1c)" + this.colors["tradInterfaceContPromptMainColor"] + "ast, \1n\1c(" + this.colors["tradInterfaceContPromptHotkeyColor"] + "G\1n\1c)" + this.colors["tradInterfaceContPromptMainColor"] + "o, "; if (this.CanDelete() || this.CanDeleteLastMsg()) { this.sContinuePrompt += "\1n\1c(" + this.colors["tradInterfaceContPromptHotkeyColor"] + "D\1n\1c)" + this.colors["tradInterfaceContPromptMainColor"] + "el, "; } if (this.CanEdit()) { this.sContinuePrompt += "\1n\1c(" + this.colors["tradInterfaceContPromptHotkeyColor"] + "E\1n\1c)" + this.colors["tradInterfaceContPromptMainColor"] + "dit, "; } this.sContinuePrompt += "\1n\1c(" + this.colors["tradInterfaceContPromptHotkeyColor"] + "S\1n\1c)" + this.colors["tradInterfaceContPromptMainColor"] + "el, "; this.sContinuePrompt += "\1n\1c(" + this.colors["tradInterfaceContPromptHotkeyColor"] + "Q\1n\1c)" + this.colors["tradInterfaceContPromptMainColor"] + "uit, msg" + this.colors["tradInterfaceContPromptHotkeyColor"] + "#" + this.colors["tradInterfaceContPromptMainColor"] + ", " + this.colors["tradInterfaceContPromptHotkeyColor"] + "?" + this.colors["tradInterfaceContPromptMainColor"] + ": " + this.colors["tradInterfaceContPromptUserInputColor"]; this.sEndContinuePrompt = "\1n\1c(" + this.colors["tradInterfaceContPromptHotkeyColor"] + "P\1n\1c)" + this.colors["tradInterfaceContPromptMainColor"] + "rev, \1n\1c(" + this.colors["tradInterfaceContPromptHotkeyColor"] + "F\1n\1c)" + this.colors["tradInterfaceContPromptMainColor"] + "irst, \1n\1c(" + this.colors["tradInterfaceContPromptHotkeyColor"] + "G\1n\1c)" + this.colors["tradInterfaceContPromptMainColor"] + "o, "; if (this.CanDelete() || this.CanDeleteLastMsg()) { this.sEndContinuePrompt += "\1n\1c(" + this.colors["tradInterfaceContPromptHotkeyColor"] + "D\1n\1c)" + this.colors["tradInterfaceContPromptMainColor"] + "el, "; } if (this.CanEdit()) { this.sEndContinuePrompt += "\1n\1c(" + this.colors["tradInterfaceContPromptHotkeyColor"] + "E\1n\1c)" + this.colors["tradInterfaceContPromptMainColor"] + "dit, "; } this.sEndContinuePrompt += "\1n\1c(" + this.colors["tradInterfaceContPromptHotkeyColor"] + "S\1n\1c)" + this.colors["tradInterfaceContPromptMainColor"] + "el, "; this.sEndContinuePrompt += "\1n\1c(" + this.colors["tradInterfaceContPromptHotkeyColor"] + "Q\1n\1c)" + this.colors["tradInterfaceContPromptMainColor"] + "uit, msg" + this.colors["tradInterfaceContPromptHotkeyColor"] + "#" + this.colors["tradInterfaceContPromptMainColor"] + ", " + this.colors["tradInterfaceContPromptHotkeyColor"] + "?" + this.colors["tradInterfaceContPromptMainColor"] + ": " + this.colors["tradInterfaceContPromptUserInputColor"]; this.msgListOnlyOnePageContinuePrompt = "\1n\1c(" + this.colors["tradInterfaceContPromptHotkeyColor"] + "G\1n\1c)" + this.colors["tradInterfaceContPromptMainColor"] + "o, "; if (this.CanDelete() || this.CanDeleteLastMsg()) { this.msgListOnlyOnePageContinuePrompt += "\1n\1c(" + this.colors["tradInterfaceContPromptHotkeyColor"] + "D\1n\1c)" + this.colors["tradInterfaceContPromptMainColor"] + "el, "; } if (this.CanEdit()) { this.msgListOnlyOnePageContinuePrompt += "\1n\1c(" + this.colors["tradInterfaceContPromptHotkeyColor"] + "E\1n\1c)" + this.colors["tradInterfaceContPromptMainColor"] + "dit, "; } this.msgListOnlyOnePageContinuePrompt += "\1n\1c(" + this.colors["tradInterfaceContPromptHotkeyColor"] + "S\1n\1c)" + this.colors["tradInterfaceContPromptMainColor"] + "el, "; this.msgListOnlyOnePageContinuePrompt += "\1n\1c(" + this.colors["tradInterfaceContPromptHotkeyColor"] + "Q\1n\1c)" + this.colors["tradInterfaceContPromptMainColor"] + "uit, msg" + this.colors["tradInterfaceContPromptHotkeyColor"] + "#" + this.colors["tradInterfaceContPromptMainColor"] + ", " + this.colors["tradInterfaceContPromptHotkeyColor"] + "?" + this.colors["tradInterfaceContPromptMainColor"] + ": " + this.colors["tradInterfaceContPromptUserInputColor"]; // Set the lightbar help text for message listing var extraCommas = true; // Whether there's room for commas between the last options this.msgListLightbarModeHelpLine = this.colors.lightbarMsgListHelpLineHotkeyColor + UP_ARROW + this.colors.lightbarMsgListHelpLineGeneralColor + ", " + this.colors.lightbarMsgListHelpLineHotkeyColor + DOWN_ARROW + this.colors.lightbarMsgListHelpLineGeneralColor + ", " + this.colors.lightbarMsgListHelpLineHotkeyColor + "PgUp" + this.colors.lightbarMsgListHelpLineGeneralColor + "/" + this.colors.lightbarMsgListHelpLineHotkeyColor + "Dn" + this.colors.lightbarMsgListHelpLineGeneralColor + ", " + this.colors.lightbarMsgListHelpLineHotkeyColor + "ENTER" + this.colors.lightbarMsgListHelpLineGeneralColor + ", " + this.colors.lightbarMsgListHelpLineHotkeyColor + "HOME" + this.colors.lightbarMsgListHelpLineGeneralColor + ", " + this.colors.lightbarMsgListHelpLineHotkeyColor + "END"; // If the user can delete messages, then append DEL as a valid key. if (this.CanDelete() || this.CanDeleteLastMsg()) { this.msgListLightbarModeHelpLine += this.colors.lightbarMsgListHelpLineGeneralColor + ", " + this.colors.lightbarMsgListHelpLineHotkeyColor + "DEL"; extraCommas = false; } this.msgListLightbarModeHelpLine += this.colors.lightbarMsgListHelpLineGeneralColor + ", " + this.colors.lightbarMsgListHelpLineHotkeyColor + "#" + this.colors.lightbarMsgListHelpLineGeneralColor + ", "; // If the user can edit messages, then append E as a valid key. if (this.CanEdit()) { this.msgListLightbarModeHelpLine += this.colors.lightbarMsgListHelpLineHotkeyColor + "E" + this.colors.lightbarMsgListHelpLineParenColor + ")" + this.colors.lightbarMsgListHelpLineGeneralColor + "dit "; } this.msgListLightbarModeHelpLine += this.colors.lightbarMsgListHelpLineHotkeyColor + "F" + this.colors.lightbarMsgListHelpLineParenColor + ")" + this.colors.lightbarMsgListHelpLineGeneralColor + (extraCommas ? "irst pg, " : "irst pg ") + this.colors.lightbarMsgListHelpLineHotkeyColor + "L" + this.colors.lightbarMsgListHelpLineParenColor + ")" + this.colors.lightbarMsgListHelpLineGeneralColor + (extraCommas ? "ast pg, " : "ast pg ") + this.colors.lightbarMsgListHelpLineHotkeyColor + "G" + this.colors.lightbarMsgListHelpLineParenColor + ")" + this.colors.lightbarMsgListHelpLineGeneralColor + (extraCommas ? "o, " : "o ") + this.colors.lightbarMsgListHelpLineHotkeyColor + "Q" + this.colors.lightbarMsgListHelpLineParenColor + ")" + this.colors.lightbarMsgListHelpLineGeneralColor + (extraCommas ? "uit, " : "uit ") + this.colors.lightbarMsgListHelpLineHotkeyColor + "? "; } else { // this.msgbase is null, so construct the default pause & help text (without // the delete & edit keys). // Set the traditional UI pause prompt text this.sStartContinuePrompt = "\1n\1c(" + this.colors["tradInterfaceContPromptHotkeyColor"] + "N\1n\1c)" + this.colors["tradInterfaceContPromptMainColor"] + "ext, \1n\1c(" + this.colors["tradInterfaceContPromptHotkeyColor"] + "L\1n\1c)" + this.colors["tradInterfaceContPromptMainColor"] + "ast, \1n\1c(" + this.colors["tradInterfaceContPromptHotkeyColor"] + "Q\1n\1c)" + this.colors["tradInterfaceContPromptMainColor"] + "uit, msg " + this.colors["tradInterfaceContPromptHotkeyColor"] + "#" + this.colors["tradInterfaceContPromptMainColor"] + ", " + this.colors["tradInterfaceContPromptHotkeyColor"] + "?" + this.colors["tradInterfaceContPromptMainColor"] + ": " + this.colors["tradInterfaceContPromptUserInputColor"]; this.sContinuePrompt = "\1n\1c(" + this.colors["tradInterfaceContPromptHotkeyColor"] + "N\1n\1c)" + this.colors["tradInterfaceContPromptMainColor"] + "ext, \1c(" + this.colors["tradInterfaceContPromptHotkeyColor"] + "P\1n\1c)" + this.colors["tradInterfaceContPromptMainColor"] + "rev, \1n\1c(" + this.colors["tradInterfaceContPromptHotkeyColor"] + "F\1n\1c)" + this.colors["tradInterfaceContPromptMainColor"] + "irst, \1n\1c(" + this.colors["tradInterfaceContPromptHotkeyColor"] + "L\1n\1c)" + this.colors["tradInterfaceContPromptMainColor"] + "ast, \1n\1c(" + this.colors["tradInterfaceContPromptHotkeyColor"] + "Q\1n\1c)" + this.colors["tradInterfaceContPromptMainColor"] + "uit, msg " + this.colors["tradInterfaceContPromptHotkeyColor"] + "#" + this.colors["tradInterfaceContPromptMainColor"] + ", " + this.colors["tradInterfaceContPromptHotkeyColor"] + "?" + this.colors["tradInterfaceContPromptMainColor"] + ": " + this.colors["tradInterfaceContPromptUserInputColor"]; this.sEndContinuePrompt = "\1n\1c(" + this.colors["tradInterfaceContPromptHotkeyColor"] + "P\1n\1c)" + this.colors["tradInterfaceContPromptMainColor"] + "rev, \1n\1c(" + this.colors["tradInterfaceContPromptHotkeyColor"] + "F\1n\1c)" + this.colors["tradInterfaceContPromptMainColor"] + "irst, \1n\1c(" + this.colors["tradInterfaceContPromptHotkeyColor"] + "Q\1n\1c)" + this.colors["tradInterfaceContPromptMainColor"] + "uit, msg " + this.colors["tradInterfaceContPromptHotkeyColor"] + "#" + this.colors["tradInterfaceContPromptMainColor"] + ", " + this.colors["tradInterfaceContPromptHotkeyColor"] + "?" + this.colors["tradInterfaceContPromptMainColor"] + ": " + this.colors["tradInterfaceContPromptUserInputColor"]; this.msgListOnlyOnePageContinuePrompt = "\1n\1c(" + this.colors["tradInterfaceContPromptHotkeyColor"] + "Q\1n\1c)" + this.colors["tradInterfaceContPromptMainColor"] + "uit, msg " + this.colors["tradInterfaceContPromptHotkeyColor"] + "#" + this.colors["tradInterfaceContPromptMainColor"] + ", " + this.colors["tradInterfaceContPromptHotkeyColor"] + "?" + this.colors["tradInterfaceContPromptMainColor"] + ": " + this.colors["tradInterfaceContPromptUserInputColor"]; // Set the lightbar help line this.msgListLightbarModeHelpLine = this.colors.lightbarMsgListHelpLineHotkeyColor + UP_ARROW + this.colors.lightbarMsgListHelpLineGeneralColor + ", " + this.colors.lightbarMsgListHelpLineHotkeyColor + DOWN_ARROW + this.colors.lightbarMsgListHelpLineGeneralColor + ", " + this.colors.lightbarMsgListHelpLineHotkeyColor + "PgUp" + this.colors.lightbarMsgListHelpLineGeneralColor + "/" + this.colors.lightbarMsgListHelpLineHotkeyColor + "Dn" + this.colors.lightbarMsgListHelpLineGeneralColor + ", " + this.colors.lightbarMsgListHelpLineHotkeyColor + "ENTER" + this.colors.lightbarMsgListHelpLineGeneralColor + ", " + this.colors.lightbarMsgListHelpLineHotkeyColor + "HOME" + this.colors.lightbarMsgListHelpLineGeneralColor + ", " + this.colors.lightbarMsgListHelpLineHotkeyColor + "END" + this.colors.lightbarMsgListHelpLineGeneralColor + ", " + this.colors.lightbarMsgListHelpLineHotkeyColor + "F" + this.colors.lightbarMsgListHelpLineParenColor + ")" + this.colors.lightbarMsgListHelpLineGeneralColor + "irst, " + this.colors.lightbarMsgListHelpLineHotkeyColor + "L" + this.colors.lightbarMsgListHelpLineParenColor + ")" + this.colors.lightbarMsgListHelpLineGeneralColor + "ast, " + this.colors.lightbarMsgListHelpLineHotkeyColor + "Q" + this.colors.lightbarMsgListHelpLineParenColor + ")" + this.colors.lightbarMsgListHelpLineGeneralColor + "uit, " + this.colors.lightbarMsgListHelpLineHotkeyColor + "#" + this.colors.lightbarMsgListHelpLineGeneralColor + " or " + this.colors.lightbarMsgListHelpLineHotkeyColor + "?"; } // Add spaces to the end of sLightbarModeHelpLine up until one char // less than the width of the screen. var lbHelpLineLen = console.strlen(this.msgListLightbarModeHelpLine); var numChars = console.screen_columns - lbHelpLineLen - 3; if (numChars > 0) { // Gradient block characters: ���� // Add characters on the left and right of the line so that the // text is centered. var numLeft = Math.floor(numChars / 2); var numRight = numChars - numLeft; for (var i = 0; i < numLeft; ++i) this.msgListLightbarModeHelpLine = "�" + this.msgListLightbarModeHelpLine; this.msgListLightbarModeHelpLine = "\1n" + this.colors.lightbarMsgListHelpLineBkgColor + this.msgListLightbarModeHelpLine; this.msgListLightbarModeHelpLine += "\1n" + this.colors.lightbarMsgListHelpLineBkgColor; for (var i = 0; i < numRight; ++i) this.msgListLightbarModeHelpLine += "�"; } } // For the DigDistMsgReader Class: Sets the hotkey help line for the enhanced // reader mode function DigDistMsgReader_SetEnhancedReaderHelpLine() { // Generate it differently depending on whether the message base is open or // not, because some options will have to be checked in the message base // to determine whether deleting & editing messages is allowed. if ((this.msgbase != null) && (this.msgbase.is_open)) { this.enhReadHelpLine = this.colors.enhReaderHelpLineHotkeyColor + UP_ARROW + this.colors.enhReaderHelpLineGeneralColor + ", " + this.colors.enhReaderHelpLineHotkeyColor + DOWN_ARROW + this.colors.enhReaderHelpLineGeneralColor + ", " + this.colors.enhReaderHelpLineHotkeyColor + LEFT_ARROW + this.colors.enhReaderHelpLineGeneralColor +", " + this.colors.enhReaderHelpLineHotkeyColor + RIGHT_ARROW + this.colors.enhReaderHelpLineGeneralColor + ", " + this.colors.enhReaderHelpLineHotkeyColor + "PgUp" + this.colors.enhReaderHelpLineGeneralColor + "/" + this.colors.enhReaderHelpLineHotkeyColor + "Dn" + this.colors.enhReaderHelpLineGeneralColor + ", " + this.colors.enhReaderHelpLineHotkeyColor + "HOME" + this.colors.enhReaderHelpLineGeneralColor + ", " + this.colors.enhReaderHelpLineHotkeyColor + "END" + this.colors.enhReaderHelpLineGeneralColor + ", " + this.colors.enhReaderHelpLineHotkeyColor; if (this.CanDelete() || this.CanDeleteLastMsg()) this.enhReadHelpLine += "DEL" + this.colors.enhReaderHelpLineGeneralColor + ", " + this.colors.enhReaderHelpLineHotkeyColor; if (this.CanEdit() && (console.screen_columns > 87)) this.enhReadHelpLine += "E" + this.colors.enhReaderHelpLineParenColor + ")" + this.colors.enhReaderHelpLineGeneralColor + "dit, " + this.colors.enhReaderHelpLineHotkeyColor; this.enhReadHelpLine += "F" + this.colors.enhReaderHelpLineParenColor + ")" + this.colors.enhReaderHelpLineGeneralColor + "irst, " + this.colors.enhReaderHelpLineHotkeyColor + "L" + this.colors.enhReaderHelpLineParenColor + ")" + this.colors.enhReaderHelpLineGeneralColor + "ast, " + this.colors.enhReaderHelpLineHotkeyColor + "R" + this.colors.enhReaderHelpLineParenColor + ")" + this.colors.enhReaderHelpLineGeneralColor + "eply, " + this.colors.enhReaderHelpLineHotkeyColor + "C" + this.colors.enhReaderHelpLineParenColor + ")" + this.colors.enhReaderHelpLineGeneralColor + "hg area, " + this.colors.enhReaderHelpLineHotkeyColor + "Q" + this.colors.enhReaderHelpLineParenColor + ")" + this.colors.enhReaderHelpLineGeneralColor + "uit, " + this.colors.enhReaderHelpLineHotkeyColor + "?"; // Center the help text based on the console width var numCharsRemaining = console.screen_columns - console.strlen(this.enhReadHelpLine) - 1; var numCharsOnEachSide = Math.floor(numCharsRemaining/2); // Left side for (var i = 0; i < numCharsOnEachSide; ++i) this.enhReadHelpLine = " " + this.enhReadHelpLine; this.enhReadHelpLine = "\1n" + this.colors.enhReaderHelpLineBkgColor + this.enhReadHelpLine; // Right side for (var i = 0; i < numCharsOnEachSide; ++i) this.enhReadHelpLine += " "; //numCharsRemaining = console.screen_columns - console.strlen(this.enhReadHelpLine) - 5; numCharsRemaining = console.screen_columns - 1 - console.strlen(this.enhReadHelpLine); if (numCharsRemaining > 0) { for (var i = 0; i < numCharsRemaining; ++i) this.enhReadHelpLine += " "; } // Create a version without the change area option this.enhReadHelpLineWithoutChgArea = this.colors.enhReaderHelpLineHotkeyColor + UP_ARROW + this.colors.enhReaderHelpLineGeneralColor + ", " + this.colors.enhReaderHelpLineHotkeyColor + DOWN_ARROW + this.colors.enhReaderHelpLineGeneralColor + ", " + this.colors.enhReaderHelpLineHotkeyColor + LEFT_ARROW + this.colors.enhReaderHelpLineGeneralColor + ", " + this.colors.enhReaderHelpLineHotkeyColor + RIGHT_ARROW + this.colors.enhReaderHelpLineGeneralColor + ", " + this.colors.enhReaderHelpLineHotkeyColor + "PgUp" + this.colors.enhReaderHelpLineGeneralColor + "/" + this.colors.enhReaderHelpLineHotkeyColor + "Dn" + this.colors.enhReaderHelpLineGeneralColor + ", " + this.colors.enhReaderHelpLineHotkeyColor + "HOME" + this.colors.enhReaderHelpLineGeneralColor + ", " + this.colors.enhReaderHelpLineHotkeyColor + "END" + this.colors.enhReaderHelpLineGeneralColor + ", " + this.colors.enhReaderHelpLineHotkeyColor; if (this.CanDelete() || this.CanDeleteLastMsg()) this.enhReadHelpLineWithoutChgArea += "DEL" + this.colors.enhReaderHelpLineGeneralColor + ", " + this.colors.enhReaderHelpLineHotkeyColor; if (this.CanEdit()) this.enhReadHelpLineWithoutChgArea += "E" + this.colors.enhReaderHelpLineParenColor + ")" + this.colors.enhReaderHelpLineGeneralColor + "dit, " + this.colors.enhReaderHelpLineHotkeyColor; this.enhReadHelpLineWithoutChgArea += "F" + this.colors.enhReaderHelpLineParenColor + ")" + this.colors.enhReaderHelpLineGeneralColor + "irst, " + this.colors.enhReaderHelpLineHotkeyColor + "L" + this.colors.enhReaderHelpLineParenColor + ")" + this.colors.enhReaderHelpLineGeneralColor + "ast, " + this.colors.enhReaderHelpLineHotkeyColor + "R" + this.colors.enhReaderHelpLineParenColor + ")" + this.colors.enhReaderHelpLineGeneralColor + "eply, " + this.colors.enhReaderHelpLineHotkeyColor + "Q" + this.colors.enhReaderHelpLineParenColor + ")" + this.colors.enhReaderHelpLineGeneralColor + "uit, " + this.colors.enhReaderHelpLineHotkeyColor + "?"; // Center the help text based on the console width numCharsRemaining = console.screen_columns - console.strlen(this.enhReadHelpLineWithoutChgArea) - 2; numCharsOnEachSide = Math.floor(numCharsRemaining/2); // Left side for (var i = 0; i < numCharsOnEachSide; ++i) this.enhReadHelpLineWithoutChgArea = " " + this.enhReadHelpLineWithoutChgArea; this.enhReadHelpLineWithoutChgArea = "\1n" + this.colors.enhReaderHelpLineBkgColor + this.enhReadHelpLineWithoutChgArea; // Right side for (var i = 0; i < numCharsOnEachSide; ++i) this.enhReadHelpLineWithoutChgArea += " "; numCharsRemaining = console.screen_columns - console.strlen(this.enhReadHelpLineWithoutChgArea) - 1; if (numCharsRemaining > 0) { for (var i = 0; i < numCharsRemaining; ++i) this.enhReadHelpLineWithoutChgArea += " "; } } else { this.enhReadHelpLine = this.colors.enhReaderHelpLineHotkeyColor + UP_ARROW + this.colors.enhReaderHelpLineGeneralColor + ", " + this.colors.enhReaderHelpLineHotkeyColor + DOWN_ARROW + this.colors.enhReaderHelpLineGeneralColor + ", " + this.colors.enhReaderHelpLineHotkeyColor + LEFT_ARROW + this.colors.enhReaderHelpLineGeneralColor + ", " + this.colors.enhReaderHelpLineHotkeyColor + RIGHT_ARROW + this.colors.enhReaderHelpLineGeneralColor + ", " + this.colors.enhReaderHelpLineHotkeyColor + "PgUp" + this.colors.enhReaderHelpLineGeneralColor + "/" + this.colors.enhReaderHelpLineHotkeyColor + "Dn" + this.colors.enhReaderHelpLineGeneralColor + ", " + this.colors.enhReaderHelpLineHotkeyColor + "HOME" + this.colors.enhReaderHelpLineGeneralColor + ", " + this.colors.enhReaderHelpLineHotkeyColor + "END" + this.colors.enhReaderHelpLineGeneralColor + ", " + this.colors.enhReaderHelpLineHotkeyColor + "F" + this.colors.enhReaderHelpLineParenColor + ")" + this.colors.enhReaderHelpLineGeneralColor + "irst, " + this.colors.enhReaderHelpLineHotkeyColor + "L" + this.colors.enhReaderHelpLineParenColor + ")" + this.colors.enhReaderHelpLineGeneralColor + "ast, " + this.colors.enhReaderHelpLineHotkeyColor + "R" + this.colors.enhReaderHelpLineParenColor + ")" + this.colors.enhReaderHelpLineGeneralColor + "eply, " + this.colors.enhReaderHelpLineHotkeyColor + "C" + this.colors.enhReaderHelpLineParenColor + ")" + this.colors.enhReaderHelpLineGeneralColor + "hg area, " + this.colors.enhReaderHelpLineHotkeyColor + "Q" + this.colors.enhReaderHelpLineParenColor + ")" + this.colors.enhReaderHelpLineGeneralColor + "uit, " + this.colors.enhReaderHelpLineHotkeyColor + "?"; // Center the help text based on the console width var numCharsRemaining = console.screen_columns - console.strlen(this.enhReadHelpLine) - 2; var numCharsOnEachSide = Math.floor(numCharsRemaining/2); // Left side for (var i = 0; i < numCharsOnEachSide; ++i) this.enhReadHelpLine = " " + this.enhReadHelpLine; this.enhReadHelpLine = "\1n" + this.colors.enhReaderHelpLineBkgColor + this.enhReadHelpLine; // Right side for (var i = 0; i < numCharsOnEachSide; ++i) this.enhReadHelpLine += " "; numCharsRemaining = console.screen_columns - console.strlen(this.enhReadHelpLine) - 1; if (numCharsRemaining > 0) { for (var i = 0; i < numCharsRemaining; ++i) this.enhReadHelpLine += " "; } // Create a version without the change area option this.enhReadHelpLineWithoutChgArea = this.colors.enhReaderHelpLineHotkeyColor + UP_ARROW + this.colors.enhReaderHelpLineGeneralColor + ", " + this.colors.enhReaderHelpLineHotkeyColor + DOWN_ARROW + this.colors.enhReaderHelpLineGeneralColor + ", " + this.colors.enhReaderHelpLineHotkeyColor + LEFT_ARROW + this.colors.enhReaderHelpLineGeneralColor + ", " + this.colors.enhReaderHelpLineHotkeyColor + RIGHT_ARROW + this.colors.enhReaderHelpLineGeneralColor + ", " + this.colors.enhReaderHelpLineHotkeyColor + "PgUp" + this.colors.enhReaderHelpLineGeneralColor + "/" + this.colors.enhReaderHelpLineHotkeyColor + "Dn" + this.colors.enhReaderHelpLineGeneralColor + ", " + this.colors.enhReaderHelpLineHotkeyColor + "HOME" + this.colors.enhReaderHelpLineGeneralColor + ", " + this.colors.enhReaderHelpLineHotkeyColor + "END" + this.colors.enhReaderHelpLineGeneralColor + ", " + this.colors.enhReaderHelpLineHotkeyColor + "F" + this.colors.enhReaderHelpLineParenColor + ")" + this.colors.enhReaderHelpLineGeneralColor + "irst, " + this.colors.enhReaderHelpLineHotkeyColor + "L" + this.colors.enhReaderHelpLineParenColor + ")" + this.colors.enhReaderHelpLineGeneralColor + "ast, " + this.colors.enhReaderHelpLineHotkeyColor + "R" + this.colors.enhReaderHelpLineParenColor + ")" + this.colors.enhReaderHelpLineGeneralColor + "eply, " + this.colors.enhReaderHelpLineHotkeyColor + "Q" + this.colors.enhReaderHelpLineParenColor + ")" + this.colors.enhReaderHelpLineGeneralColor + "uit, " + this.colors.enhReaderHelpLineHotkeyColor + "?"; // Center the help text based on the console width numCharsRemaining = console.screen_columns - console.strlen(this.enhReadHelpLineWithoutChgArea) - 2; numCharsOnEachSide = Math.floor(numCharsRemaining/2); // Left side for (var i = 0; i < numCharsOnEachSide; ++i) this.enhReadHelpLineWithoutChgArea = " " + this.enhReadHelpLineWithoutChgArea; this.enhReadHelpLineWithoutChgArea = "\1n" + this.colors.enhReaderHelpLineBkgColor + this.enhReadHelpLineWithoutChgArea; // Right side for (var i = 0; i < numCharsOnEachSide; ++i) this.enhReadHelpLineWithoutChgArea += " "; numCharsRemaining = console.screen_columns - 1 - console.strlen(this.enhReadHelpLineWithoutChgArea); if (numCharsRemaining > 0) { for (var i = 0; i < numCharsRemaining; ++i) this.enhReadHelpLineWithoutChgArea += " "; } } } // For the DigDistMsgReader class: Reads the configuration file (by default, // DDMsgReader.cfg) and sets the object properties // accordingly. function DigDistMsgReader_ReadConfigFile() { this.cfgFileSuccessfullyRead = false; var themeFilename = ""; // In case a theme filename is specified // Open the main configuration file. First look for it in the sbbs/mods // directory, then sbbs/ctrl, then in the same directory as this script. var cfgFilename = system.mods_dir + this.cfgFilename; if (!file_exists(cfgFilename)) cfgFilename = system.ctrl_dir + this.cfgFilename; if (!file_exists(cfgFilename)) cfgFilename = gStartupPath + this.cfgFilename; var cfgFile = new File(cfgFilename); if (cfgFile.open("r")) { this.cfgFileSuccessfullyRead = true; var fileLine = null; // A line read from the file var equalsPos = 0; // Position of a = in the line var commentPos = 0; // Position of the start of a comment var setting = null; // A setting name (string) var settingUpper = null; // Upper-case setting name var value = null; // To store a value for a setting (string) var valueUpper = null; // Upper-cased value for a setting (string) while (!cfgFile.eof) { // Read the next line from the config file. fileLine = cfgFile.readln(2048); // fileLine should be a string, but I've seen some cases // where it isn't, so check its type. if (typeof(fileLine) != "string") continue; // If the line starts with with a semicolon (the comment // character) or is blank, then skip it. if ((fileLine.substr(0, 1) == ";") || (fileLine.length == 0)) continue; // If the line has a semicolon anywhere in it, then remove // everything from the semicolon onward. commentPos = fileLine.indexOf(";"); if (commentPos > -1) fileLine = fileLine.substr(0, commentPos); // Look for an equals sign, and if found, separate the line // into the setting name (before the =) and the value (after the // equals sign). equalsPos = fileLine.indexOf("="); if (equalsPos > 0) { // Read the setting & value, and trim leading & trailing spaces. setting = trimSpaces(fileLine.substr(0, equalsPos), true, false, true); settingUpper = setting.toUpperCase(); value = trimSpaces(fileLine.substr(equalsPos+1), true, false, true); valueUpper = value.toUpperCase(); // Set the appropriate valueUpper in the settings object. if (settingUpper == "LISTINTERFACESTYLE") this.msgListUseLightbarListInterface = (valueUpper == "LIGHTBAR"); else if (settingUpper == "READERINTERFACESTYLE") this.scrollingReaderInterface = (valueUpper == "SCROLLABLE"); else if (settingUpper == "READERINTERFACESTYLEFORANSIMESSAGES") this.useScrollingInterfaceForANSIMessages = (valueUpper == "SCROLLABLE"); else if (settingUpper == "DISPLAYBOARDINFOINHEADER") this.displayBoardInfoInHeader = (valueUpper == "TRUE"); // Note: this.reverseListOrder can be true, false, or "ASK" else if (settingUpper == "REVERSELISTORDER") this.reverseListOrder = (valueUpper == "ASK" ? "ASK" : (valueUpper == "TRUE")); else if (settingUpper == "PROMPTTOCONTINUELISTINGMESSAGES") this.promptToContinueListingMessages = (valueUpper == "TRUE"); else if (settingUpper == "PROMPTCONFIRMREADMESSAGE") this.promptToReadMessage = (valueUpper == "TRUE"); else if (settingUpper == "MSGLISTDISPLAYTIME") this.msgList_displayMessageDateImported = (valueUpper == "IMPORTED"); else if (settingUpper == "MSGAREALIST_LASTIMPORTEDMSG_TIME") this.msgAreaList_lastImportedMsg_showImportTime = (valueUpper == "IMPORTED"); else if (settingUpper == "STARTMODE") { if ((valueUpper == "READER") || (valueUpper == "READ")) this.startMode = READER_MODE_READ; else if ((valueUpper == "LISTER") || (valueUpper == "LIST")) this.startMode = READER_MODE_LIST; } else if (settingUpper == "TABSPACES") { var numSpaces = +value; // If greater than 0, then set this.numTabSpaces if (numSpaces > 0) this.numTabSpaces = numSpaces; } else if (settingUpper == "PAUSEAFTERNEWMSGSCAN") this.pauseAfterNewMsgScan = (valueUpper == "TRUE"); else if (settingUpper == "THEMEFILENAME") { // First look for the theme config file in the sbbs/mods // directory, then sbbs/ctrl, then the same directory as // this script. themeFilename = system.mods_dir + value; if (!file_exists(themeFilename)) themeFilename = system.ctrl_dir + value; if (!file_exists(themeFilename)) themeFilename = gStartupPath + value; } } } cfgFile.close(); } else { // Was unable to read the configuration file. Output a warning to the user // that defaults will be used and to notify the sysop. console.print("\1n"); console.crlf(); console.print("\1w\1hUnable to open the configuration file: \1y" + this.cfgFilename); console.crlf(); console.print("\1wDefault settings will be used. Please notify the sysop."); mswait(2000); } // If a theme filename was specified, then read the colors & strings // from it. if (themeFilename.length > 0) { var themeFile = new File(themeFilename); if (themeFile.open("r")) { var fileLine = null; // A line read from the file var equalsPos = 0; // Position of a = in the line var commentPos = 0; // Position of the start of a comment var setting = null; // A setting name (string) var value = null; // To store a value for a setting (string) while (!themeFile.eof) { // Read the next line from the config file. fileLine = themeFile.readln(2048); // fileLine should be a string, but I've seen some cases // where it isn't, so check its type. if (typeof(fileLine) != "string") continue; // If the line starts with with a semicolon (the comment // character) or is blank, then skip it. if ((fileLine.substr(0, 1) == ";") || (fileLine.length == 0)) continue; // If the line has a semicolon anywhere in it, then remove // everything from the semicolon onward. commentPos = fileLine.indexOf(";"); if (commentPos > -1) fileLine = fileLine.substr(0, commentPos); // Look for an equals sign, and if found, separate the line // into the setting name (before the =) and the value (after the // equals sign). equalsPos = fileLine.indexOf("="); if (equalsPos > 0) { // Read the setting (without leading/trailing spaces) & value setting = trimSpaces(fileLine.substr(0, equalsPos), true, false, true); value = fileLine.substr(equalsPos+1); // Colors if ((setting == "msgListHeaderMsgGroupTextColor") || (setting == "msgListHeaderMsgGroupNameColor") || (setting == "msgListHeaderSubBoardTextColor") || (setting == "msgListHeaderMsgSubBoardName") || (setting == "msgListColHeader") || (setting == "msgListMsgNumColor") || (setting == "msgListFromColor") || (setting == "msgListToColor") || (setting == "msgListSubjectColor") || (setting == "msgListDateColor") || (setting == "msgListTimeColor") || (setting == "msgListToUserMsgNumColor") || (setting == "msgListToUserFromColor") || (setting == "msgListToUserToColor") || (setting == "msgListToUserSubjectColor") || (setting == "msgListToUserDateColor") || (setting == "msgListToUserTimeColor") || (setting == "msgListFromUserMsgNumColor") || (setting == "msgListFromUserFromColor") || (setting == "msgListFromUserToColor") || (setting == "msgListFromUserSubjectColor") || (setting == "msgListFromUserDateColor") || (setting == "msgListFromUserTimeColor") || (setting == "msgListHighlightBkgColor") || (setting == "msgListMsgNumHighlightColor") || (setting == "msgListFromHighlightColor") || (setting == "msgListToHighlightColor") || (setting == "msgListSubjHighlightColor") || (setting == "msgListDateHighlightColor") || (setting == "msgListTimeHighlightColor") || (setting == "lightbarMsgListHelpLineBkgColor") || (setting == "lightbarMsgListHelpLineGeneralColor") || (setting == "lightbarMsgListHelpLineHotkeyColor") || (setting == "lightbarMsgListHelpLineParenColor") || (setting == "tradInterfaceContPromptMainColor") || (setting == "tradInterfaceContPromptHotkeyColor") || (setting == "tradInterfaceContPromptUserInputColor") || (setting == "msgBodyColor") || (setting == "readMsgConfirmColor") || (setting == "readMsgConfirmNumberColor") || (setting == "afterReadMsg_ListMorePromptColor") || (setting == "tradInterfaceHelpScreenColor") || (setting == "areaChooserMsgAreaNumColor") || (setting == "areaChooserMsgAreaDescColor") || (setting == "areaChooserMsgAreaNumItemsColor") || (setting == "areaChooserMsgAreaHeaderColor") || (setting == "areaChooserSubBoardHeaderColor") || (setting == "areaChooserMsgAreaMarkColor") || (setting == "areaChooserMsgAreaLatestDateColor") || (setting == "areaChooserMsgAreaLatestTimeColor") || (setting == "areaChooserMsgAreaBkgHighlightColor") || (setting == "areaChooserMsgAreaNumHighlightColor") || (setting == "areaChooserMsgAreaDescHighlightColor") || (setting == "areaChooserMsgAreaDateHighlightColor") || (setting == "areaChooserMsgAreaTimeHighlightColor") || (setting == "areaChooserMsgAreaNumItemsHighlightColor") || (setting == "lightbarAreaChooserHelpLineBkgColor") || (setting == "lightbarAreaChooserHelpLineGeneralColor") || (setting == "lightbarAreaChooserHelpLineHotkeyColor") || (setting == "lightbarAreaChooserHelpLineParenColor") || (setting == "scrollbarBGColor") || (setting == "scrollbarScrollBlockColor") || (setting == "enhReaderPromptSepLineColor") || (setting == "enhReaderHelpLineBkgColor") || (setting == "enhReaderHelpLineGeneralColor") || (setting == "enhReaderHelpLineHotkeyColor") || (setting == "enhReaderHelpLineParenColor") || (setting == "hdrLineLabelColor") || (setting == "hdrLineValueColor") || (setting == "selectedMsgMarkColor")) { // Trim leading & trailing spaces from the value when // setting a color. Also, replace any instances of "\1" // with the Synchronet attribute control character. this.colors[setting] = trimSpaces(value, true, false, true).replace(/\\1/g, "\1"); } // Text values else if ((setting == "scrollbarBGChar") || (setting == "scrollbarScrollBlockChar") || (setting == "goToPrevMsgAreaPromptText") || (setting == "goToNextMsgAreaPromptText") || (setting == "newMsgScanText") || (setting == "newToYouMsgScanText") || (setting == "allToYouMsgScanText") || (setting == "scanScopePromptText") || (setting == "goToMsgNumPromptText") || (setting == "msgScanCompleteText") || (setting == "msgScanAbortedText") || (setting == "deleteMsgNumPromptText") || (setting == "editMsgNumPromptText") || (setting == "noMessagesInSubBoardText") || (setting == "noSearchResultsInSubBoardText") || (setting == "invalidMsgNumText") || (setting == "readMsgNumPromptText") || (setting == "msgHasBeenDeletedText") || (setting == "noKludgeLinesForThisMsgText") || (setting == "searchingPersonalMailText") || (setting == "searchingSubBoardAbovePromptText") || (setting == "searchingSubBoardText") || (setting == "searchTextPromptText") || (setting == "fromNamePromptText") || (setting == "toNamePromptText") || (setting == "abortedText") || (setting == "loadingPersonalMailText") || (setting == "msgDelConfirmText") || (setting == "msgDeletedText") || (setting == "cannotDeleteMsgText_notYoursNotASysop") || (setting == "cannotDeleteMsgText_notLastPostedMsg") || (setting == "msgEditConfirmText") || (setting == "noPersonalEmailText")) { // Replace any instances of "\1" with the Synchronet // attribute control character this.text[setting] = value.replace(/\\1/g, "\1"); } } } themeFile.close(); // Ensure that scrollbarBGChar and scrollbarScrollBlockChar are // only one character. If they're longer, use only the first // character. if (this.text.scrollbarBGChar.length > 1) this.text.scrollbarBGChar = this.text.scrollbarBGChar.substr(0, 1); if (this.text.scrollbarScrollBlockChar.length > 1) this.text.scrollbarScrollBlockChar = this.text.scrollbarScrollBlockChar.substr(0, 1); } else { // Was unable to read the theme file. Output a warning to the user // that defaults will be used and to notify the sysop. this.cfgFileSuccessfullyRead = false; console.print("\1n"); console.crlf(); console.print("\1w\1hUnable to open the theme file: \1y" + themeFilename); console.crlf(); console.print("\1wDefault settings will be used. Please notify the sysop."); mswait(2000); } } } // For the DigDistMsgReader class: Lets the user edit an existing message. // // Parameters: // pMsgIndex: The index of the message to edit // // Return value: An object with the following parameters: // userCannotEdit: Boolean - True if the user can't edit, false if they can // userConfirmed: Boolean - Whether or not the user confirmed editing // msgEdited: Boolean - Whether or not the message was edited // newMsgIdx: The index (offset) of the new (edited) message that was saved. // If the message wasn't edited/saved, this will be -1. function DigDistMsgReader_EditExistingMsg(pMsgIndex) { var returnObj = new Object(); returnObj.userCannotEdit = false; returnObj.userConfirmed = false; returnObj.msgEdited = false; returnObj.newMsgIdx = -1; // Only let the user edit the message if they're a sysop or // if they wrote the message. var msgHeader = this.GetMsgHdrByIdx(pMsgIndex); if (!gIsSysop && (msgHeader.from != user.name) && (msgHeader.from != user.alias) && (msgHeader.from != user.handle)) { console.print("\1n\1h\1wCannot edit message #\1y" + +(pMsgIndex+1) + " \1wbecause it's not yours or you're not a sysop.\r\n\1p"); returnObj.userCannotEdit = true; return returnObj; } // Confirm the action with the user (default to no). returnObj.userConfirmed = !console.noyes(this.text.msgEditConfirmText.replace("%d", +(pMsgIndex+1))); if (!returnObj.userConfirmed) return returnObj; // Dump the message body to a temporary file in the node dir var originalMsgBody = this.msgbase.get_msg_body(true, pMsgIndex); var tempFilename = system.node_dir + "DDMsgLister_message.txt"; var tmpFile = new File(tempFilename); if (tmpFile.open("w")) { var wroteToTempFile = tmpFile.write(word_wrap(originalMsgBody, 79)); tmpFile.close(); // If we were able to write to the temp file, then let the user // edit the file. if (wroteToTempFile) { // The following lines set some attributes in the bbs object // in an attempt to make the "To" name and subject appear // correct in the editor. // TODO: On May 14, 2013, Digital Man said bbs.msg_offset will // probably be removed because it doesn't provide any benefit. // bbs.msg_number is a unique message identifier that won't // change, so it's probably best for scripts to use bbs.msg_number // instead of offsets. bbs.msg_to = msgHeader.to; bbs.msg_to_ext = msgHeader.to_ext; bbs.msg_subject = msgHeader.subject; bbs.msg_offset = msgHeader.offset; bbs.msg_number = msgHeader.number; // Let the user edit the temporary file console.editfile(tempFilename); // Load the temp file back into msgBodyColor and have this.msgbase // save the message. if (tmpFile.open("r")) { var newMsgBody = tmpFile.read(); tmpFile.close(); // If the new message body is different from the original message // body, then go ahead and save the message and mark the original // message for deletion. (Checking the new & original message // bodies seems to be the only way to check to see if the user // aborted out of the message editor.) if (newMsgBody != originalMsgBody) { var newHdr = { to: msgHeader.to, to_ext: msgHeader.to_ext, from: msgHeader.from, from_ext: msgHeader.from_ext, attr: msgHeader.attr, subject: msgHeader.subject }; var savedNewMsg = this.msgbase.save_msg(newHdr, newMsgBody); // If the message was successfully saved, then mark the original // message for deletion and output a message to the user. if (savedNewMsg) { returnObj.msgEdited = true; returnObj.newMsgIdx = this.msgbase.total_msgs - 1; var message = "\1n\1cThe edited message has been saved as a new message."; if (this.msgbase.remove_msg(true, pMsgIndex)) message += " The original has been\r\nmarked for deletion."; else message += " \1h\1yHowever, the original\r\ncould not be marked for deletion."; message += "\r\n\1p"; console.print(message); } else console.print("\r\n\1n\1h\1yError: \1wFailed to save the new message\r\n\1p"); } } else { console.print("\r\n\1n\1h\1yError: \1wUnable to read the temporary file\r\n"); console.print("Filename: \1b" + tempFilename + "\r\n"); console.pause(); } } else { console.print("\r\n\1n\1h\1yError: \1wUnable to write to temporary file\r\n"); console.print("Filename: \1b" + tempFilename + "\r\n"); console.pause(); } } else { console.print("\r\n\1n\1h\1yError: \1wUnable to open a temporary file for writing\r\n"); console.print("Filename: \1b" + tempFilename + "\r\n"); console.pause(); } // Delete the temporary file from disk. tmpFile.remove(); return returnObj; } // For the DigDistMsgReader Class: Returns whether or not the user can delete // their messages in the sub-board (distinct from being able to delete only // their last message). function DigDistMsgReader_CanDelete() { var canDelete = gIsSysop || this.readingPersonalEmail; if ((this.msgbase != null) && this.msgbase.is_open && (this.msgbase.cfg != null)) canDelete = canDelete || ((this.msgbase.cfg.settings & SUB_DEL) == SUB_DEL); return canDelete; } // For the DigDistMsgReader Class: Returns whether or not the user can delete // the last message they posted in the sub-board. function DigDistMsgReader_CanDeleteLastMsg() { var canDelete = gIsSysop; if ((this.msgbase != null) && this.msgbase.is_open && (this.msgbase.cfg != null)) canDelete = canDelete || ((this.msgbase.cfg.settings & SUB_DELLAST) == SUB_DELLAST); return canDelete; } // For the DigDistMsgReader Class: Returns whether or not the user can edit // messages. function DigDistMsgReader_CanEdit() { var canEdit = gIsSysop; if ((this.msgbase != null) && this.msgbase.is_open && (this.msgbase.cfg != null)) canEdit = canEdit || ((this.msgbase.cfg.settings & SUB_EDIT) == SUB_EDIT); return canEdit; } // For the DigDistMsgReader Class: Returns whether or not message quoting // is enabled. function DigDistMsgReader_CanQuote() { var canQuote = this.readingPersonalEmail || gIsSysop; if ((this.msgbase != null) && this.msgbase.is_open && (this.msgbase.cfg != null)) canQuote = canQuote || ((this.msgbase.cfg.settings & SUB_QUOTE) == SUB_QUOTE); return canQuote; } // For the DigDistMsgReader Class: Displays the stock Synchronet message header file for // a given message header. // // Parameters: // pMsgHdr: The message header object function DigDistMsgReader_DisplaySyncMsgHeader(pMsgHdr) { if ((pMsgHdr == null) || (typeof(pMsgHdr) != "object")) return; // Note: The message header has the following fields: // 'number': The message number // 'offset': The message offset // 'to': Who the message is directed to (string) // 'from' Who wrote the message (string) // 'subject': The message subject (string) // 'date': The date - Full text (string) // Generate a string containing the message's import date & time. //var dateTimeStr = strftime("%Y-%m-%d %H:%M:%S", msgHeader.when_imported_time) // Use the date text in the message header, without the time // zone offset at the end. var dateTimeStr = pMsgHdr["date"].replace(/ [-+][0-9]+$/, ""); // Check to see if there is a msghdr file in the sbbs/text/menu // directory. If there is, then use it to display the message // header information. Otherwise, output a default message header. var msgHdrFileOpened = false; var msgHdrFilename = this.GetMsgHdrFilenameFull(); if (msgHdrFilename.length > 0) { var msgHdrFile = new File(msgHdrFilename); if (msgHdrFile.open("r")) { msgHdrFileOpened = true; var fileLine = null; // To store a line read from the file while (!msgHdrFile.eof) { // Read the next line from the header file fileLine = msgHdrFile.readln(2048); // fileLine should be a string, but I've seen some cases // where it isn't, so check its type. if (typeof(fileLine) != "string") continue; // Since we're displaying the message information ouside of Synchronet's // message read prompt, this script now has to parse & replace some of // the @-codes in the message header line, since Synchronet doesn't know // that the user is reading a message. console.putmsg(this.ParseMsgAtCodes(fileLine, pMsgHdr, null, dateTimeStr, true)); console.crlf(); } msgHdrFile.close(); } } // If the msghdr file didn't open (or doesn't exist), then output the default // header. if (!msgHdrFileOpened) { // Generate a string describing the message attributes, then output the default // header. var allMsgAttrStr = makeAllMsgAttrStr(pMsgHdr); console.print("\1n\1w���������������������������������������������������������������������������۲��"); console.crlf(); console.print("\1n\1w�����\1cFrom\1w\1h: \1b" + pMsgHdr["from"].substr(0, console.screen_columns-12)); console.crlf(); console.print("\1n\1w�����\1cTo \1w\1h: \1b" + pMsgHdr["to"].substr(0, console.screen_columns-12)); console.crlf(); console.print("\1n\1w�����\1cSubj\1w\1h: \1b" + pMsgHdr["subject"].substr(0, console.screen_columns-12)); console.crlf(); console.print("\1n\1w�����\1cDate\1w\1h: \1b" + dateTimeStr.substr(0, console.screen_columns-12)); console.crlf(); console.print("\1n\1w�����\1cAttr\1w\1h: \1b" + allMsgAttrStr.substr(0, console.screen_columns-12)); console.crlf(); } } // For the DigDistMsgReader class: Returns the name of the msghdr file in the // sbbs/text/menu directory. If the user's terminal supports ANSI, this first // checks to see if an .ans version exists. Otherwise, checks to see if an // .asc version exists. If neither are found, this function will return an // empty string. function DigDistMsgReader_GetMsgHdrFilenameFull() { // If the user's terminal supports ANSI and msghdr.ans exists // in the text/menu directory, then use that one. Otherwise, // if msghdr.asc exists, then use that one. var ansiFileName = "menu/msghdr.ans"; var asciiFileName = "menu/msghdr.asc"; var msgHdrFilename = ""; if (console.term_supports(USER_ANSI) && file_exists(system.text_dir + ansiFileName)) msgHdrFilename = system.text_dir + ansiFileName; else if (file_exists(system.text_dir + asciiFileName)) msgHdrFilename = system.text_dir + asciiFileName; return msgHdrFilename; } // For the DigDistMsgReader class: Returns the number of messages in the current // sub-board. This will be either the number of headers in this.msgSearchHdrs // for the current sub-board (if non-empty and a search type specified) or // this.msgbase.total_msgs. // // Parameters: // pCheckDeletedAttributes: Optional boolean - Whether or not to check the // 'deleted' attributes of the messages and not // count deleted messages. Defaults to false. // // Return value: The number of messages function DigDistMsgReader_NumMessages(pCheckDeletedAttributes) { var checkDeletedAttributes = (typeof(pCheckDeletedAttributes) == "boolean" ? pCheckDeletedAttributes : false); var numMsgs = 0; if (this.SearchingAndResultObjsDefinedForCurSub()) numMsgs = this.msgSearchHdrs[this.subBoardCode].indexed.length; else if ((this.msgbase != null) && this.msgbase.is_open) numMsgs = this.msgbase.total_msgs; // If the caller wants to check the deleted attributes, then do so. if ((numMsgs > 0) && checkDeletedAttributes) { var msgHdr; var originalNumMsgs = numMsgs; for (var msgIdx = 0; msgIdx < originalNumMsgs; ++msgIdx) { msgHdr = this.GetMsgHdrByIdx(msgIdx); if ((msgHdr.attr & MSG_DELETE) == MSG_DELETE) { --numMsgs; if (numMsgs < 0) { numMsgs = 0; break; } } } } return numMsgs; } // For the DigDistMsgReader class: Returns whether there are any non-deleted // messages in the current sub-board. // // Return value: Boolean - Whether or not there are any non-deleted messages // in the current sub-board. function DigDistMsgReader_NonDeletedMessagesExist() { var messagesExist = false; var numMsgs = this.NumMessages(); if (numMsgs > 0) { var msgHdr; for (var msgIdx = 0; (msgIdx < numMsgs) && !messagesExist; ++msgIdx) { msgHdr = this.GetMsgHdrByIdx(msgIdx); if ((msgHdr.attr & MSG_DELETE) == 0) { messagesExist = true; break; } } } return messagesExist; } // For the DigDistMsgReader class: Returns the highest message number (1-based), either from this.msgSearchHdrs // (if it has search results for the current sub-board) or this.msgbase. If // there are no search results for the current sub-board in this.msgSearchHdrs, // the highest message number is the same as the total number of messages // in the sub-board (unless the Synchronet standard ever changes..). function DigDistMsgReader_HighestMessageNum() { var highestMessageNum = this.msgbase.total_msgs; if (this.msgSearchHdrs.hasOwnProperty(this.subBoardCode) && (this.msgSearchHdrs[this.subBoardCode].indexed.length > 0)) highestMessageNum = this.msgSearchHdrs[this.subBoardCode].indexed.length; return highestMessageNum; } // For the DigDistMsgReader class: Returns whether or not a message number (1-based) // is a valid and existing message number. This is intended for validating user // input where not all the message numbers are consecutive. // // Parameters: // pMsgNum: The message number to validate // // Return value: Boolean - Whether or not the message number function DigDistMsgReader_IsValidMessageNum(pMsgNum) { // The message numbers start at 1 if (pMsgNum < 1) return false; // If there are search results for the current sub-board, then check to see if // the message number exists in its indexed array. Otherwise, check with // this.msgbase. var msgNumIsValid = false; if (this.SearchingAndResultObjsDefinedForCurSub()) msgNumIsValid = ((pMsgNum > 0) && (pMsgNum <= this.msgSearchHdrs[this.subBoardCode].indexed.length)); else msgNumIsValid = ((pMsgNum > 0) && (pMsgNum <= this.msgbase.total_msgs)); return msgNumIsValid; } // For the DigDistMsgReader class: Returns a message header by index. Will look // in this.msgSearchHdrs if it's not empty, or from this.msgbase. This function // assumes that this.msgbase is open. // // Parameters: // pMsgIdx: The message index (0-based) function DigDistMsgReader_GetMsgHdrByIdx(pMsgIdx) { var msgHdr = null; if (this.msgSearchHdrs.hasOwnProperty(this.subBoardCode) && (this.msgSearchHdrs[this.subBoardCode].indexed.length > 0)) { if ((pMsgIdx >= 0) && (pMsgIdx < this.msgSearchHdrs[this.subBoardCode].indexed.length)) msgHdr = this.msgSearchHdrs[this.subBoardCode].indexed[pMsgIdx]; } else { if ((pMsgIdx >= 0) && (pMsgIdx < this.msgbase.total_msgs)) msgHdr = this.msgbase.get_msg_header(true, pMsgIdx, true); } return msgHdr; } // For the DigDistMsgReader class: Returns a message header by message number // (1-based). Will look in this.msgSearchHdrs if it's not empty, or from // this.msgbase. This function assumes that this.msgbase is open. // // Parameters: // pMsgNum: The message number (1-based) // // Return value: The message header for the message number, or null on error function DigDistMsgReader_GetMsgHdrByMsgNum(pMsgNum) { var msgHdr = null; if (this.msgSearchHdrs.hasOwnProperty(this.subBoardCode) && (this.msgSearchHdrs[this.subBoardCode].indexed.length > 0)) { if ((pMsgNum > 0) && (pMsgNum <= this.msgSearchHdrs[this.subBoardCode].indexed.length)) msgHdr = this.msgSearchHdrs[this.subBoardCode].indexed[pMsgNum-1]; } else { if ((pMsgNum > 0) && (pMsgNum <= this.msgbase.total_msgs)) msgHdr = this.msgbase.get_msg_header(true, pMsgNum-1, true); } return msgHdr; } // For the DigDistMsgReader class: Returns a message header by absolute message // number. If there is a problem, this method will return null. // // Parameters: // pMsgNum: The absolute message number // // Return value: The message header for the message number, or null on error function DigDistMsgReader_GetMsgHdrByAbsoluteNum(pMsgNum) { var msgHdr = null; if (this.msgbase == null) msgHdr = null; else if (!this.msgbase.is_open) msgHdr = null; else msgHdr = this.msgbase.get_msg_header(false, pMsgNum, true); return msgHdr; } // For the DigDistMsgReader class: Takes an absolute message number and returns // its message index (offset). On error, returns -1. // // Parameters: // pMsgNum: The absolute message number // // Return value: The message's index. On error, returns -1. function DigDistMsgReader_AbsMsgNumToIdx(pMsgNum) { return absMsgNumToIdx(this.msgbase, pMsgNum); } // For the DigDistMsgReader class: Takes a message index and returns // its absolute message number. On error, returns -1. // // Parameters: // pMsgIdx: The message index // // Return value: The message's absolute message number. On error, returns -1. function DigDistMsgReader_IdxToAbsMsgNum(pMsgIdx) { return idxToAbsMsgNum(this.msgbase, pMsgIdx); } // This function takes an absolute message number for a given messagebase objectand // and returns the message index (offset). On error, returns -1. // // Parameters: // pMsgbase: The messagebase object from which to retrieve the message header // pMsgNum: The absolute message number // // Return value: The message's index, or -1 on error. function absMsgNumToIdx(pMsgbase, pMsgNum) { if ((pMsgbase == null) || (typeof(pMsgbase) != "object")) return -1; if (!pMsgbase.is_open) return -1; var msgHdr = pMsgbase.get_msg_header(false, pMsgNum, true); if ((msgHdr == null) && gCmdLineArgVals.verboselogging) { writeToSysAndNodeLog("Message area " + pMsgbase.cfg.code + ": Tried to get message header for absolute message number " + pMsgNum + " but got a null header object."); } return (msgHdr != null ? msgHdr.offset : -1); } // Takes a message index and returns the message's absolute message number. // // Parameters: // pMsgbase: The messagebase object from which to retrieve the message header // pMsgIdx: The message index // // Return value: The absolue message number, or -1 on error. function idxToAbsMsgNum(pMsgbase, pMsgIdx) { if ((pMsgbase == null) || (typeof(pMsgbase) != "object")) return -1; if (!pMsgbase.is_open) return -1; var msgHdr = pMsgbase.get_msg_header(true, pMsgIdx, true); if ((msgHdr == null) && gCmdLineArgVals.verboselogging) { writeToSysAndNodeLog("Tried to get message header for message offset " + pMsgIdx + " but got a null header object."); } return (msgHdr != null ? msgHdr.number : -1); } // Prompts the user for a message number and keeps repeating if they enter and // invalid message number. It is assumed that the cursor has already been // placed where it needs to be for the prompt text, but a cursor position is // one of the parameters to this function so that this function can write an // error message if the message number inputted from the user is invalid. // // Parameters: // pCurPos: The starting position of the cursor (used for positioning the // cursor at the prompt text location to display an error message). // If not needed (i.e., in a traditional user interface or non-ANSI // terminal), this parameter can be null. // pPromptText: The text to use to prompt the user // pClearToEOLAfterPrompt: Whether or not to clear to the end of the line after // writing the prompt text (boolean) // pErrorPauseTimeMS: The time (in milliseconds) to pause after displaying the // error that the message number is invalid // pRepeat: Boolean - Whether or not to ask repeatedly until the user enters a // valid message number. Optional; defaults to false. // // Return value: The message number entered by the user. If the user didn't // enter a message number, this will be 0. function DigDistMsgReader_PromptForMsgNum(pCurPos, pPromptText, pClearToEOLAfterPrompt, pErrorPauseTimeMS, pRepeat) { var curPosValid = ((pCurPos != null) && (typeof(pCurPos) == "object") && pCurPos.hasOwnProperty("x") && (typeof(pCurPos.x) == "number") && pCurPos.hasOwnProperty("y") && (typeof(pCurPos.y) == "number")); var useCurPos = (console.term_supports(USER_ANSI) && curPosValid); var msgNum = 0; var promptCount = 0; var lastErrorLen = 0; // The length of the last error message var promptTextLen = console.strlen(pPromptText); var continueAskingMsgNum = false; do { ++promptCount; msgNum = 0; if (promptTextLen > 0) console.print(pPromptText); if (pClearToEOLAfterPrompt && useCurPos) { // The first time the user is being prompted, clear the line to the // end of the line. For subsequent times, clear the line from the // prompt text length to the error text length; if (promptCount == 1) console.cleartoeol("\1n"); else { if (lastErrorLen > promptTextLen) { console.print("\1n"); console.gotoxy(pCurPos.x+promptTextLen, pCurPos.y); var clearLen = lastErrorLen - promptTextLen; for (var counter = 0; counter < clearLen; ++counter) console.print(" "); } } console.gotoxy(pCurPos.x+promptTextLen, pCurPos.y); } msgNum = console.getnum(this.HighestMessageNum()); // If the message number is invalid, then output an error message. if (msgNum != 0) { if (!this.IsValidMessageNum(msgNum)) { // Output an error message that the message number is invalid if (useCurPos) { // TODO: Update to optionally not clear all of the line before writing // the error text, or specify how much of the line to clear. I want // to do that for the enhanced message reader when prompting for a // message number to read - I don't want this to clear the whole line // because that would erase the scrollbar character on the right. writeWithPause(pCurPos.x, pCurPos.y, "\1n" + this.text.invalidMsgNumText.replace("%d", msgNum) + "\1n", pErrorPauseTimeMS, "\1n", true); console.gotoxy(pCurPos); } else { console.print("\1n\1w\1h" + msgNum + " \1yis an invalid message number."); console.crlf(); if (pErrorPauseTimeMS > 0) mswait(pErrorPauseTimeMS); } // Set msgNum back to 0 to signify that the user didn't enter a (valid) // message number. msgNum = 0; lastErrorLen = 24 + msgNum.toString().length; continueAskingMsgNum = pRepeat; } else continueAskingMsgNum = false; } else continueAskingMsgNum = false; } while (continueAskingMsgNum) return msgNum; } // For the DigDistMsgReader class: Looks for complex @-code strings in a text line and // parses & replaces them appropriately with the appropriate info from the message header // object and/or message base object. This is more complex than simple text substitution // because message subject @-codes could include something like @MSG_SUBJECT-L######@ // or @MSG_SUBJECT-R######@ or @MSG_SUBJECT-L20@ or @MSG_SUBJECT-R20@. // // Parameters: // pTextLine: The line of text to search // pMsgHdr: The message header object // pDisplayMsgNum: The message number, if different from the number in the header // object (i.e., if doing a message search). This parameter can // be null, in which case the number in the header object will be // used. // pDateTimeStr: Optional formatted string containing the date & time. If this is // not provided, the current date & time will be used. // pAllowCLS: Optional boolean - Whether or not to allow the @CLS@ code. // Defaults to false. // // Return value: A string with the complex @-codes substituted in the line with the // appropriate message header information. function DigDistMsgReader_ParseMsgAtCodes(pTextLine, pMsgHdr, pDisplayMsgNum, pDateTimeStr, pAllowCLS) { var textLine = pTextLine; var dateTimeStr = (typeof(pDateTimeStr) == "string" ? pDateTimeStr : strftime("%Y-%m-%d %H:%M:%S")); // Message attribute strings var allMsgAttrStr = makeAllMsgAttrStr(pMsgHdr); var mainMsgAttrStr = makeMainMsgAttrStr(pMsgHdr.attr); var auxMsgAttrStr = makeAuxMsgAttrStr(pMsgHdr.auxattr); var netMsgAttrStr = makeNetMsgAttrStr(pMsgHdr.netattr); // An array of @-code strings without the trailing @, to be used for constructing // regular expressions to look for versions with justification & length specifiers. // The order of the strings in this array matters. For instance, @MSG_NUM_AND_TOTAL // needs to come before @MSG_NUM so that it gets processed properly, since they // both start out with the same text. var atCodeStrBases = ["@MSG_FROM", "@MSG_FROM_EXT", "@MSG_TO", "@MSG_TO_NAME", "@MSG_TO_EXT", "@MSG_SUBJECT", "@MSG_DATE", "@MSG_ATTR", "@MSG_AUXATTR", "@MSG_NETATTR", "@MSG_ALLATTR", "@MSG_NUM_AND_TOTAL", "@MSG_NUM", "@MSG_ID", "@MSG_REPLY_ID", "@MSG_TIMEZONE", "@GRP", "@GRPL", "@SUB", "@SUBL", "@BBS", "@BOARDNAME", "@ALIAS", "@SYSOP", "@CONF", "@DATE", "@DIR", "@DIRL", "@USER", "@NAME"]; // For each @-code, look for a version with justification & length specified and // replace accordingly. for (var atCodeStrBaseIdx in atCodeStrBases) { var atCodeStrBase = atCodeStrBases[atCodeStrBaseIdx]; // Synchronet @-codes can specify justification with -L or -R and width using a series // of non-numeric non-space characters (i.e., @MSG_SUBJECT-L#####@ or @MSG_SUBJECT-R######@). // So look for these types of format specifiers for the message subject and if found, // parse and replace appropriately. var multipleCharLenRegex = new RegExp(atCodeStrBase + "-[LR][^0-9 ]+@", "gi"); var atCodeMatchArray = textLine.match(multipleCharLenRegex); if ((atCodeMatchArray != null) && (atCodeMatchArray.length > 0)) { for (var idx in atCodeMatchArray) { // In this case, the subject length is the length of the whole format specifier. var substLen = atCodeMatchArray[idx].length; textLine = this.ReplaceMsgAtCodeFormatStr(pMsgHdr, pDisplayMsgNum, textLine, substLen, atCodeMatchArray[idx], pDateTimeStr, mainMsgAttrStr, auxMsgAttrStr, netMsgAttrStr, allMsgAttrStr); } } // Now, look for subject formatters with the length specified (i.e., @MSG_SUBJECT-L20@ or @MSG_SUBJECT-R20@) var numericLenSearchRegex = new RegExp(atCodeStrBase + "-[LR][0-9]+@", "gi"); atCodeMatchArray = textLine.match(numericLenSearchRegex); if ((atCodeMatchArray != null) && (atCodeMatchArray.length > 0)) { for (var idx in atCodeMatchArray) { // Extract the length specified between the -L or -R and the final @. var dashJustifyIndex = findDashJustifyIndex(atCodeMatchArray[idx]); var substLen = atCodeMatchArray[idx].substring(dashJustifyIndex+2, atCodeMatchArray[idx].length-1); textLine = this.ReplaceMsgAtCodeFormatStr(pMsgHdr, pDisplayMsgNum, textLine, substLen, atCodeMatchArray[idx], pDateTimeStr, mainMsgAttrStr, auxMsgAttrStr, netMsgAttrStr, allMsgAttrStr, dashJustifyIndex); } } } // In case there weren't any complex @-codes, do replacements for the basic // @-codes. Set the group & sub-board information as Personal Mail or the // sub-board currently being read. var grpIdx = -1; var groupName = ""; var groupDesc = ""; var subName = ""; var subDesc = ""; var msgConf = ""; var fileDir = ""; var fileDirLong = ""; if (this.readingPersonalEmail) { var subName = "Personal mail"; var subDesc = "Personal mail"; } else { grpIdx = msg_area.sub[this.subBoardCode].grp_index; groupName = msg_area.sub[this.subBoardCode].grp_name; groupDesc = msg_area.grp_list[grpIdx].description; subName = msg_area.sub[this.subBoardCode].name; subDesc = msg_area.sub[this.subBoardCode].description; msgConf = msg_area.grp_list[msg_area.sub[this.subBoardCode].grp_index].name + " " + msg_area.sub[this.subBoardCode].name; } if ((typeof(bbs.curlib) == "number") && (typeof(bbs.curdir) == "number")) { fileDir = file_area.lib_list[bbs.curlib].dir_list[bbs.curdir].name; fileDirLong = file_area.lib_list[bbs.curlib].dir_list[bbs.curdir].description; } var messageNum = (typeof(pDisplayMsgNum) == "number" ? pDisplayMsgNum : pMsgHdr["offset"]+1); var newTxtLine = textLine.replace(/@MSG_SUBJECT@/gi, pMsgHdr["subject"]) .replace(/@MSG_FROM@/gi, pMsgHdr["from"]) .replace(/@MSG_FROM_EXT@/gi, (typeof(pMsgHdr["from_ext"]) == "string" ? pMsgHdr["from_ext"] : "")) .replace(/@MSG_TO@/gi, pMsgHdr["to"]) .replace(/@MSG_TO_NAME@/gi, pMsgHdr["to"]) .replace(/@MSG_TO_EXT@/gi, (typeof(pMsgHdr["to_ext"]) == "string" ? pMsgHdr["to_ext"] : "")) .replace(/@MSG_DATE@/gi, pDateTimeStr) .replace(/@MSG_ATTR@/gi, mainMsgAttrStr) .replace(/@MSG_AUXATTR@/gi, auxMsgAttrStr) .replace(/@MSG_NETATTR@/gi, netMsgAttrStr) .replace(/@MSG_ALLATTR@/gi, allMsgAttrStr) .replace(/@MSG_NUM_AND_TOTAL@/gi, messageNum.toString() + "/" + this.NumMessages()) .replace(/@MSG_NUM@/gi, messageNum.toString()) .replace(/@MSG_ID@/gi, (typeof(pMsgHdr["id"]) == "string" ? pMsgHdr["id"] : "")) .replace(/@MSG_REPLY_ID@/gi, (typeof(pMsgHdr["reply_id"]) == "string" ? pMsgHdr["reply_id"] : "")) .replace(/@MSG_FROM_NET@/gi, (typeof(pMsgHdr["from_net_addr"]) == "string" ? pMsgHdr["from_net_addr"] : "")) .replace(/@MSG_TO_NET@/gi, (typeof(pMsgHdr["to_net_addr"]) == "string" ? pMsgHdr["to_net_addr"] : "")) .replace(/@MSG_TIMEZONE@/gi, system.zonestr(pMsgHdr["when_written_zone"])) .replace(/@GRP@/gi, groupName) .replace(/@GRPL@/gi, groupDesc) .replace(/@SUB@/gi, subName) .replace(/@SUBL@/gi, subDesc) .replace(/@CONF@/gi, msgConf) .replace(/@BBS@/gi, system.name) .replace(/@BOARDNAME@/gi, system.name) .replace(/@SYSOP@/gi, system.operator) .replace(/@DATE@/gi, system.datestr()) .replace(/@LOCATION@/gi, system.location) .replace(/@DIR@/gi, fileDir) .replace(/@DIRL@/gi, fileDirLong) .replace(/@ALIAS@/gi, user.alias) .replace(/@NAME@/gi, (user.name.length > 0 ? user.name : user.alias)) .replace(/@USER@/gi, user.alias); if (!pAllowCLS) newTxtLine = newTxtLine.replace(/@CLS@/gi, ""); return newTxtLine; } // For the DigDistMsgReader class: Helper for ParseMsgAtCodes(): Replaces a // given @-code format string in a text line with the appropriate message header // info or BBS system info. // // Parameters: // pMsgHdr: The object containing the message header information // pDisplayMsgNum: The message number, if different from the number in the header // object (i.e., if doing a message search). This parameter can // be null, in which case the number in the header object will be // used. // pTextLine: The text line in which to perform the replacement // pSpecifiedLen: The length extracted from the @-code format string // pAtCodeStr: The @-code format string, which will be replaced with the actual message info // pDateTimeStr: Formatted string containing the date & time // pMsgMainAttrStr: A string describing the main message attributes ('attr' property of header) // pMsgAuxAttrStr: A string describing the auxiliary message attributes ('auxattr' property of header) // pMsgNetAttrStr: A string describing the network message attributes ('netattr' property of header) // pMsgAllAttrStr: A string describing all message attributes // pDashJustifyIdx: Optional - The index of the -L or -R in the @-code string function DigDistMsgReader_ReplaceMsgAtCodeFormatStr(pMsgHdr, pDisplayMsgNum, pTextLine, pSpecifiedLen, pAtCodeStr, pDateTimeStr, pMsgMainAttrStr, pMsgAuxAttrStr, pMsgNetAttrStr, pMsgAllAttrStr, pDashJustifyIdx) { if (typeof(pDashJustifyIdx) != "number") pDashJustifyIdx = findDashJustifyIndex(pAtCodeStr); // Specify the format string with left or right justification based on the justification // character (either L or R). var formatStr = ((/L/i).test(pAtCodeStr.charAt(pDashJustifyIdx+1)) ? "%-" : "%") + pSpecifiedLen + "s"; // Specify the replacement text depending on the @-code string var replacementTxt = ""; if (pAtCodeStr.indexOf("@MSG_FROM") > -1) replacementTxt = pMsgHdr["from"].substr(0, pSpecifiedLen); else if (pAtCodeStr.indexOf("@MSG_FROM_EXT") > -1) replacementTxt = (typeof pMsgHdr["from_ext"] === "undefined" ? "" : pMsgHdr["from_ext"].substr(0, pSpecifiedLen)); else if ((pAtCodeStr.indexOf("@MSG_TO") > -1) || (pAtCodeStr.indexOf("@MSG_TO_NAME") > -1)) replacementTxt = pMsgHdr["to"].substr(0, pSpecifiedLen); else if (pAtCodeStr.indexOf("@MSG_TO_EXT") > -1) replacementTxt = (typeof pMsgHdr["to_ext"] === "undefined" ? "" : pMsgHdr["to_ext"].substr(0, pSpecifiedLen)); else if (pAtCodeStr.indexOf("@MSG_SUBJECT") > -1) replacementTxt = pMsgHdr["subject"].substr(0, pSpecifiedLen); else if (pAtCodeStr.indexOf("@MSG_DATE") > -1) replacementTxt = pDateTimeStr.substr(0, pSpecifiedLen); else if (pAtCodeStr.indexOf("@MSG_ATTR") > -1) replacementTxt = pMsgMainAttrStr.substr(0, pSpecifiedLen); else if (pAtCodeStr.indexOf("@MSG_AUXATTR") > -1) replacementTxt = pMsgAuxAttrStr.substr(0, pSpecifiedLen); else if (pAtCodeStr.indexOf("@MSG_NETATTR") > -1) replacementTxt = pMsgNetAttrStr.substr(0, pSpecifiedLen); else if (pAtCodeStr.indexOf("@MSG_ALLATTR") > -1) replacementTxt = pMsgAllAttrStr.substr(0, pSpecifiedLen); else if (pAtCodeStr.indexOf("@MSG_NUM_AND_TOTAL") > -1) { var messageNum = (typeof(pDisplayMsgNum) == "number" ? pDisplayMsgNum : pMsgHdr["offset"]+1); replacementTxt = (messageNum.toString() + "/" + this.NumMessages()).substr(0, pSpecifiedLen); // "number" is also absolute number } else if (pAtCodeStr.indexOf("@MSG_NUM") > -1) { var messageNum = (typeof(pDisplayMsgNum) == "number" ? pDisplayMsgNum : pMsgHdr["offset"]+1); replacementTxt = messageNum.toString().substr(0, pSpecifiedLen); // "number" is also absolute number } else if (pAtCodeStr.indexOf("@MSG_ID") > -1) replacementTxt = (typeof pMsgHdr["id"] === "undefined" ? "" : pMsgHdr["id"].substr(0, pSpecifiedLen)); else if (pAtCodeStr.indexOf("@MSG_REPLY_ID") > -1) replacementTxt = (typeof pMsgHdr["reply_id"] === "undefined" ? "" : pMsgHdr["reply_id"].substr(0, pSpecifiedLen)); else if (pAtCodeStr.indexOf("@MSG_TIMEZONE") > -1) replacementTxt = system.zonestr(pMsgHdr["when_written_zone"]).substr(0, pSpecifiedLen); else if (pAtCodeStr.indexOf("@GRP") > -1) { if (this.readingPersonalEmail) replacementTxt = "Personal mail".substr(0, pSpecifiedLen); else replacementTxt = msg_area.sub[this.subBoardCode].grp_name.substr(0, pSpecifiedLen); } else if (pAtCodeStr.indexOf("@GRPL") > -1) { if (this.readingPersonalEmail) replacementTxt = "Personal mail".substr(0, pSpecifiedLen); else { var grpIdx = msg_area.sub[this.subBoardCode].grp_index; replacementTxt = msg_area.grp_list[grpIdx].description.substr(0, pSpecifiedLen); } } else if (pAtCodeStr.indexOf("@SUB") > -1) { if (this.readingPersonalEmail) replacementTxt = "Personal mail".substr(0, pSpecifiedLen); else replacementTxt = msg_area.sub[this.subBoardCode].name.substr(0, pSpecifiedLen); } else if (pAtCodeStr.indexOf("@SUBL") > -1) { if (this.readingPersonalEmail) replacementTxt = "Personal mail".substr(0, pSpecifiedLen); else replacementTxt = msg_area.sub[this.subBoardCode].description.substr(0, pSpecifiedLen); } else if ((pAtCodeStr.indexOf("@BBS") > -1) || (pAtCodeStr.indexOf("@BOARDNAME") > -1)) replacementTxt = system.name.substr(0, pSpecifiedLen); else if (pAtCodeStr.indexOf("@SYSOP") > -1) replacementTxt = system.operator.substr(0, pSpecifiedLen); else if (pAtCodeStr.indexOf("@CONF") > -1) { var msgConfDesc = msg_area.grp_list[msg_area.sub[this.subBoardCode].grp_index].name + " " + msg_area.sub[this.subBoardCode].name; replacementTxt = msgConfDesc.substr(0, pSpecifiedLen); } else if (pAtCodeStr.indexOf("@DATE") > -1) replacementTxt = system.datestr().substr(0, pSpecifiedLen); else if (pAtCodeStr.indexOf("@DIR") > -1) { if ((typeof(bbs.curlib) == "number") && (typeof(bbs.curdir) == "number")) replacementTxt = file_area.lib_list[bbs.curlib].dir_list[bbs.curdir].name.substr(0, pSpecifiedLen); else replacementTxt = ""; } else if (pAtCodeStr.indexOf("@DIRL") > -1) { if ((typeof(bbs.curlib) == "number") && (typeof(bbs.curdir) == "number")) replacementTxt = file_area.lib_list[bbs.curlib].dir_list[bbs.curdir].description.substr(0, pSpecifiedLen); else replacementTxt = ""; } else if ((pAtCodeStr.indexOf("@ALIAS") > -1) || (pAtCodeStr.indexOf("@USER") > -1)) replacementTxt = user.alias.substr(0, pSpecifiedLen); else if (pAtCodeStr.indexOf("@NAME") > -1) // User name or alias { var userNameOrAlias = (user.name.length > 0 ? user.name : user.alias); replacementTxt = userNameOrAlias.substr(0, pSpecifiedLen); } // Do the text replacement (escape special characters in the @-code string so we can do a literal search) var searchRegex = new RegExp(pAtCodeStr.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&"), "gi"); return pTextLine.replace(searchRegex, format(formatStr, replacementTxt)); } // Helper for DigDistMsgReader_ParseMsgAtCodes() and // DigDistMsgReader_ReplaceMsgAtCodeFormatStr(): Returns the index of the -L or -R in // one of the @-code strings. // // Parameters: // pAtCodeStr: The @-code string to search // // Return value: The index of the -L or -R, or -1 if not found function findDashJustifyIndex(pAtCodeStr) { var strIndex = pAtCodeStr.indexOf("-"); if (strIndex > -1) { // If this part of the string is not -L or -R, then set strIndex to -1 // to signify that it was not found. var checkStr = pAtCodeStr.substr(strIndex, 2).toUpperCase(); if ((checkStr != "-L") && (checkStr != "-R")) strIndex = -1; } return strIndex; } // Finds the offset (index) of the next message prior to or after a given offset // that is not marked as deleted. If none is found, the return value will be -1. // This function requires that this.msgbase is open. // // Parameters: // pOffset: The message offset to search prior/after // pForward: Boolean - Whether or not to search forward (true) or backward (false). // If this is not specified, the default will be true (forward). // // Return value: The index of the next message prior/later that is not marked // as deleted, or -1 if none is found. function DigDistMsgReader_FindNextNonDeletedMsgIdx(pOffset, pForward) { // Sanity checking for the parameters & other things if ((typeof(pOffset) != "number") || (this.msgbase == null)) return -1; if (!this.msgbase.is_open) return -1; var searchForward = (typeof(pForward) == "boolean" ? pForward : true); var newMsgIdx = -1; if (searchForward) { // Search forward for a message that isn't marked for deletion. var numOfMessages = this.NumMessages(); if (pOffset < numOfMessages - 1) { for (var messageIdx = pOffset+1; (messageIdx < numOfMessages) && (newMsgIdx == -1); ++messageIdx) { var nextMsgHdr = this.GetMsgHdrByIdx(messageIdx); if ((nextMsgHdr != null) && ((nextMsgHdr.attr & MSG_DELETE) == 0)) newMsgIdx = messageIdx; } } } else { // Search backward for a message that isn't marked for deletion. if (pOffset > 0) { for (var messageIdx = pOffset-1; (messageIdx >= 0) && (newMsgIdx == -1); --messageIdx) { var prevMsgHdr = this.GetMsgHdrByIdx(messageIdx); if ((prevMsgHdr != null) && ((prevMsgHdr.attr & MSG_DELETE) == 0)) newMsgIdx = messageIdx; } } } return newMsgIdx; } // For the DigDistMsgReader class: Sets up the message base object for a new // sub-board. Leaves the msgbase object open. // // Parameters: // pNewSubBoardCode: The internal code of the new sub-board. // If this is not a string, then this function // will use bbs.cursub_code instead. // // Return value: An object with the following properties: // succeeded: Boolean - True if succeeded or false if not // lastReadMsgIdx: The index of the last message read in the sub-board. // function DigDistMsgReader_ChangeSubBoard(pNewSubBoardCode) { var retObj = new Object(); retObj.succeeded = false; retObj.lastReadMsgIdx = 0; var newSubBoardCode = bbs.cursub_code; if (typeof(pNewSubBoardCode) == "string") newSubBoardCode = pNewSubBoardCode; if (typeof(msg_area.sub[newSubBoardCode]) != "object") { console.clear("\1n"); console.print("\1n\1h\1y* \1wSomething has gone wrong. An invalid message sub-board code was"); console.crlf(); console.print("specified: " + newSubBoardCode); console.crlf(); console.pause(); return retObj; } // If the new sub-board code is different from the currently-set // sub-board code, then go ahead and change it. if (newSubBoardCode != this.subBoardCode) { if (this.msgbase != null) this.msgbase.close(); this.setSubBoardCode(newSubBoardCode); this.msgbase = new MsgBase(this.subBoardCode); } else if (this.msgbase == null) this.msgbase = new MsgBase(this.subBoardCode); // If the message base is not open, then open it. If the message base can't // be opened, then output an error and return. if (!this.msgbase.is_open) { if (!this.msgbase.open()) { console.clear("\1n"); console.print("\1n\1h\1y* \1wUnable to open message sub-board:"); console.crlf(); console.print(subBoardGrpAndName(this.subBoardCode)); console.crlf(); console.pause(); return retObj; } } // If there are no messages to display in the current sub-board, then just // return. Note: A message regarding there being no messages would have // already been shown, so we don't need to show an 'Empty sub-board' message // here. if (this.NumMessages() == 0) return retObj; // Get the index of the user's last-read message in this sub-board. retObj.lastReadMsgIdx = this.GetLastReadMsgIdx(); if (retObj.lastReadMsgIdx == -1) retObj.lastReadMsgIdx = 0; retObj.succeeded = true; return retObj; } // For the enhanced reader functionality of the DigDistMsgReader class: Sets up // the message base object for a new sub-board and refreshes the reader hotkey // help line on the bottom of the screen. Leaves the msgbase object open. // // Parameters: // pNewSubBoardCode: The internal code of the new sub-board. // If this is not a string, then this function // will use bbs.cursub_code instead. // // Return value: An object with the following properties: // succeeded: Boolean - True if succeeded or false if not // lastReadMsgIdx: The index of the last message read in the sub-board. // Will be 0 on error. function DigDistMsgReader_EnhancedReaderChangeSubBoard(pNewSubBoardCode) { var retObj = this.ChangeSubBoard(pNewSubBoardCode); if (retObj.succeeded && (this.NumMessages() > 0)) { // Clear the screen and refresh the help line at the bottom of the screen console.clear("\1n"); this.DisplayEnhancedMsgReadHelpLine(console.screen_rows); } return retObj; } // For the DigDistMsgReader class: Allows the user to reply to a message // // Parameters: // pMsgHdr: The header of the message to reply to // pMsgText: The text (body) of the message // pPrivate: Optional - Boolean to specify whether not this should be a private // reply using the user's QWK or FIDO, etc. address. Defaults to // false if not specified. // pMsgIdx: The message index (if there are search results, this might be // different than the message offset in the messagebase). This // is intended for use in deleting a private email after reading it. // // Return value: An object containing the following properties: // postSucceeded: Boolean - Whether or not the message post succeeded // msgWasDeleted: Boolean - Whether or not the message was deleted after // the user replied to it // msgbaseReOpened: Boolean - Whether or not the messagebase object // was successfully closed & re-opened to refresh the // messagebase information after posting the message function DigDistMsgReader_ReplyToMsg(pMsgHdr, pMsgText, pPrivate, pMsgIdx) { var retObj = new Object(); retObj.postSucceeded = false; retObj.msgWasDeleted = false; retObj.msgbaseReOpened = true; if ((pMsgHdr == null) || (typeof(pMsgHdr) != "object")) return retObj; // If the "no-reply" attribute is enabled for the message, then don't // let the user rpely. if ((pMsgHdr.attr & MSG_NOREPLY) == MSG_NOREPLY) { console.crlf(); console.print("\1n\1y\1hReplies are not allowed for this message."); console.crlf(); console.pause(); return retObj; } // Make sure the user is allowed to post a message before allowing them to // reply. If posting on a message sub-board, then we can check the can_read // property of the sub-board; otherwise, the user should be allowed to post // a message. var replyPrivately = (typeof(pPrivate) == "boolean" ? pPrivate : false); var canPost = true; if (!replyPrivately && (this.subBoardCode != "mail")) canPost = msg_area.sub[this.subBoardCode].can_post; if (!canPost) { console.crlf(); console.print("\1n\1y\1hYou are not allowed to post in this message area."); console.crlf(); console.pause(); return retObj; } retObj.postSucceeded = true; // No special behavior in the reply var replyMode = WM_NONE; // If quoting is allowed in the sub-board, then write QUOTES.TXT in // the node directory to allow the user to quote the original message. var quoteFile = null; if (this.CanQuote()) { quoteFile = new File(system.node_dir + "QUOTES.TXT"); if (quoteFile.open("w")) { if (typeof(pMsgText) == "string") { //quoteFile.write(word_wrap(pMsgText, 80/*79*/)); quoteFile.write(pMsgText); } else { var msgText = this.msgbase.get_msg_body(true, msgHeader.offset); //quoteFile.write(word_wrap(msgText, 80/*79*/)); quoteFile.write(msgText); } quoteFile.close(); // Let the user quote in the reply replyMode |= WM_QUOTE; } } // Note: The following commented-out code was a kludge that // no longer seems necessary with recent (3.15) builds of // Synchronet. /* // If posting in a local group, then the 'from' and 'to' names // in the message header must be swapped in order to have the // correct 'to' name in the reply. if (pMsgHdr.from_net_type == NET_NONE) { var fromBackup = pMsgHdr.from; pMsgHdr.from = pMsgHdr.to; pMsgHdr.to = fromBackup; } */ // If the user is listing personal e-mail, then we need to call // bbs.email() to leave a reply; otherwise, use bbs.post_msg(). if (this.readingPersonalEmail) { var privReplRetObj = this.DoPrivateReply(pMsgHdr, pMsgIdx, replyMode); retObj.postSucceeded = privReplRetObj.sendSucceeded; retObj.msgWasDeleted = privReplRetObj.msgWasDeleted; } else { // The user is posting in a public message sub-board. // Open a file in the node directory and write some information // about the current sub-board and message being read: // - The highest message number in the sub-board (last message) // - The total number of messages in the sub-board // - The number of the message being read // - The current sub-board code // This is for message editors that need to access the message // base (i.e., SlyEdit). Normally (in Synchronet's message read // propmt), this information is stored in bbs.smb_last_msg, // bbs.smb_total_msgs, and bbs.smb_curmsg, but this message lister // can't change those values. Thus, we need to write them to a file. var msgBaseInfoFile = new File(system.node_dir + "DDML_SyncSMBInfo.txt"); if (msgBaseInfoFile.open("w")) { msgBaseInfoFile.writeln(this.msgbase.last_msg.toString()); // Highest message # msgBaseInfoFile.writeln(this.NumMessages().toString()); // Total # messages // Message number (Note: For SlyEdit, requires SlyEdit 1.27 or newer). msgBaseInfoFile.writeln(pMsgHdr.number.toString()); // # of the message being read (New: 2013-05-14) // Old: Using either the message number or offset: /* // Message number/offset: // If the Synchronet version is at least 3.16 and the Synchronet compile // date is at least May 12, 2013, then use bbs.msg_number. Otherwise, // use bbs.smb_curmsg. bbs.msg_number is the absolute message number and // is always accurate, but bbs.msg_number only works properly in the // Synchronet 3.16 daily builds starting on May 12, 2013, which was right // after Digital Man committed his fix to make bbs.msg_number work properly. if ((system.version_num >= 3.16) && gSyncCompileDateAtLeast2013_05_12) msgBaseInfoFile.writeln(pMsgHdr.number.toString()); // # of the message being read (New: 2013-05-14) else msgBaseInfoFile.writeln(pOffset.toString()); // Offset of the message (for older builds of Synchronet) */ msgBaseInfoFile.writeln(this.subBoardCode); // Sub-board code msgBaseInfoFile.close(); } // Store the current total number of messages so that we can search new // messages if needed after the message is posted var numMessagesBefore = this.msgbase.total_msgs; // Let the user post the message. Then, delete the message base info // file. To be safe, and to ensure the messagebase object gets refreshed // with the latest information, close the messagebase object before // posting the message and re-open it afterward. On Linux, the messagebase // object doesn't seem to get refreshed with the number of messages in the // sub-board, etc., but on Windows that doesn't seem to be an issue. // If we are to send a private message, then let the user send the reply // as a private email. Otherwise, let the user post the reply as a public // message. this.msgbase.close(); if (replyPrivately) { var privReplRetObj = this.DoPrivateReply(pMsgHdr, pMsgIdx, replyMode); retObj.postSucceeded = privReplRetObj.sendSucceeded; retObj.msgWasDeleted = privReplRetObj.msgWasDeleted; } else { // Not a private message - Post as a public message retObj.postSucceeded = bbs.post_msg(this.subBoardCode, replyMode, pMsgHdr); console.pause(); } msgBaseInfoFile.remove(); retObj.msgbaseReOpened = this.msgbase.open(); // If the user replied to the message and a message search was done that // would populate the search results, then search the last messages to // include the user's reply in the message matches or other new messages // that may have been posted that match the user's search. // TODO: Make sure this works for a newscan & new-to-user scan if (retObj.postSucceeded && retObj.msgbaseReOpened && this.SearchTypePopulatesSearchResults() && this.msgSearchHdrs.hasOwnProperty(this.subBoardCode) && (this.msgbase.total_msgs > numMessagesBefore)) { if (!this.msgSearchHdrs.hasOwnProperty(this.subBoardCode)) this.msgSearchHdrs[this.subBoardCode] = searchMsgbase(this.subBoardCode, this.msgbase, this.searchType, this.searchString, this.readingPersonalEmailFromUser); else { var msgHeaders = searchMsgbase(this.subBoardCode, this.msgbase, this.searchType, this.searchString, this.readingPersonalEmailFromUser, numMessagesBefore, this.msgbase.total_msgs); var msgNum = 0; for (var i = 0; i < msgHeaders.indexed.length; ++i) { this.msgSearchHdrs[this.subBoardCode].indexed.push(msgHeaders.indexed[i]); msgNum = msgHeaders.indexed[i].offset + 1; } } } } // Delete the quote file if (quoteFile != null) quoteFile.remove(); return retObj; } // For the DigDistMsgReader class: This function performs a private message reply. Takes a message header // and returns a boolean for whether or not it succeeded in sending the reply. // // Parameters: // pMsgHdr: A message header object // pMsgIdx: The message index (if there are search results, this might be // different than the message offset in the messagebase). This // is intended for use in deleting a private email after reading it. // pReplyMode: Optional - A bitfield containing reply mode bits to use // in addition to the default network/email reply mode // // Return value: An object containing the following properties: // sendSucceeded: Boolean - Whether or not the message post succeeded // msgWasDeleted: Boolean - Whether or not the message was deleted after // the user replied to it function DigDistMsgReader_DoPrivateReply(pMsgHdr, pMsgIdx, pReplyMode) { var retObj = new Object(); retObj.sendSucceeded = true; retObj.msgWasDeleted = false; // Set up the initial reply mode bits var replyMode = WM_NONE; if (typeof(pReplyMode) == "number") replyMode |= pReplyMode; // If the message is not local, the send the reply as a network message. // Otherwise, send the reply as a local email. if (pMsgHdr.from_net_type != NET_NONE) { if ((typeof(pMsgHdr.from_net_addr) == "string") && (pMsgHdr.from_net_addr.length > 0)) { // Build the email address to reply to. If the original message is // internet email, then simply use the from_net_addr field from the // message header. Otherwise (i.e., on a networked sub-board), use // username@from_net_addr. var emailAddr = ""; if (pMsgHdr.from_net_type == NET_INTERNET) emailAddr = pMsgHdr.from_net_addr; else emailAddr = pMsgHdr.from + "@" + pMsgHdr.from_net_addr; // Prompt the user to verify the receiver's email address console.putmsg(bbs.text(Email), P_SAVEATR); console.ungetstr(emailAddr); emailAddr = console.getstr(60, K_LINE); if ((typeof(emailAddr) == "string") && (emailAddr.length > 0)) { replyMode |= WM_NETMAIL; console.ungetstr(pMsgHdr.subject); retObj.sendSucceeded = bbs.netmail(emailAddr, replyMode); console.pause(); } else { retObj.sendSucceeded = false; console.putmsg(bbs.text(Aborted), P_SAVEATR); console.pause(); } } else { retObj.sendSucceeded = false; console.crlf(); console.print("\1n\1h\1yThere is no network address for this message"); console.crlf(); } } else { // Replying to a local user replyMode |= WM_EMAIL; // Look up the user number of the "from" user name in the message header var userNumber = system.matchuser(pMsgHdr.from); if (userNumber != 0) { // Output a newline to avoid ugly overwriting of text on the screen in // case the sender wants to forward to netmail, then send email to the // sender. Note that if the send failed, that could be because the // user aborted the message. console.crlf(); retObj.sendSucceeded = bbs.email(userNumber, replyMode, "", pMsgHdr.subject); console.pause(); } } // If the user replied to a personal email, then ask the user if they want // to delete the message that was just replied to, and if so, delete it. if (retObj.sendSucceeded && this.readingPersonalEmail && (typeof(pMsgIdx) == "number")) { // Get the delete mail confirmation text from text.dat and replace // the %s with the "from" name in the message header, and use that // as the confirmation text. // Note: If the message was deleted, the DeleteMessage() method will // refresh the header in the search results, if there are any search // results. if (!console.noyes(bbs.text(DeleteMailQ).replace("%s", pMsgHdr.from))) retObj.msgWasDeleted = this.PromptAndDeleteMessage(pMsgIdx, null, null, null, false); } return retObj; } // For the DigDistMsgReader class: Displays the enhanced reader mode help screen. // // Parameters: // pDisplayChgAreaOpt: Optional boolean - Whether or not to show the "change area" option. // Defaults to true. // pDisplayDLAttachmentOpt: Optional boolean - Whether or not to display the "download attachments" // option. Defaults to false. function DigDistMsgReader_DisplayEnhancedReaderHelp(pDisplayChgAreaOpt, pDisplayDLAttachmentOpt) { if (console.term_supports(USER_ANSI)) console.clear("\1n"); var displayChgAreaOpt = (typeof(pDisplayChgAreaOpt) == "boolean" ? pDisplayChgAreaOpt : true); var displayDLAttachmentsOpt = (typeof(pDisplayDLAttachmentOpt) == "boolean" ? pDisplayDLAttachmentOpt : false); DisplayProgramInfo(); console.crlf(); // Display information about the current sub-board and search results. console.print("\1n\1cCurrently reading \1g" + subBoardGrpAndName(this.subBoardCode)); console.crlf(); // If the user isn't reading personal messages (i.e., is reading a sub-board), // then output the total number of messages in the sub-board. We probably // shouldn't output the total number of messages in the "mail" area, because // that includes more than the current user's email. if (!this.readingPersonalEmail) { console.print("\1n\1cThere are a total of \1g" + this.msgbase.total_msgs + " \1cmessages in the current area."); console.crlf(); } // If there is currently a search (which also includes personal messages), // then output the number of search results/personal messages. if (this.SearchingAndResultObjsDefinedForCurSub()) { var numSearchResults = this.NumMessages(); var resultsWord = (numSearchResults > 1 ? "results" : "result"); console.print("\1n\1c"); if (this.readingPersonalEmail) console.print("You have \1g" + numSearchResults + " \1c" + (numSearchResults == 1 ? "message" : "messages") + "."); else { if (numSearchResults == 1) console.print("There is \1g1 \1csearch result."); else console.print("There are \1g" + numSearchResults + " \1csearch results."); } console.crlf(); } // Display the enhanced reader keys console.crlf(); console.print("\1n\1cEnhanced reader mode keys"); console.crlf(); console.print("\1h\1k"); for (var i = 0; i < 25; ++i) console.print(HORIZONTAL_SINGLE); console.crlf(); var keyHelpLines = ["\1h\1cDown\1g/\1cup arrow \1g: \1n\1cScroll down\1g/\1cup in the message", "\1h\1cLeft\1g/\1cright arrow \1g: \1n\1cGo to the previous\1g/\1cnext message", "\1h\1cEnter \1g: \1n\1cGo to the next message", "\1h\1cPageUp\1g/\1cPageDown \1g: \1n\1cScroll up\1g/\1cdown a page in the message", "\1h\1cHOME \1g: \1n\1cGo to the top of the message", "\1h\1cEND \1g: \1n\1cGo to the bottom of the message"]; if (gIsSysop) { keyHelpLines.push("\1h\1cDEL \1g: \1n\1cDelete the current message"); keyHelpLines.push("\1h\1cCtrl-S \1g: \1n\1cSave the message (to the BBS machine)"); } else if (this.CanDelete() || this.CanDeleteLastMsg()) keyHelpLines.push("\1h\1cDEL \1g: \1n\1cDelete the current message (if it's yours)"); if (displayDLAttachmentsOpt) keyHelpLines.push("\1h\1cCtrl-A \1g: \1n\1cDownload attachments"); // If not reading personal email or doing a search/scan, then include the // text for the message threading keys. if (!this.readingPersonalEmail && !this.SearchingAndResultObjsDefinedForCurSub()) { // Thread ID keys: For Synchronet 3.16 and above, include the text "thread ID" // in the help line, since Synchronet 3.16 has the thread_id field in the message // headers. var threadIDLine = "\1h\1c( \1n\1cor \1h) \1g: \1n\1cGo to the previous\1g/\1cnext message in the thread"; /* if (system.version_num >= 31600) threadIDLine += " (thread ID)"; */ keyHelpLines.push(threadIDLine); keyHelpLines.push("\1h\1c< \1n\1cor \1h> \1g: \1n\1cGo to the previous\1g/\1cnext message by title (subject)"); keyHelpLines.push("\1h\1c{ \1n\1cor \1h} \1g: \1n\1cGo to the previous\1g/\1cnext message by author"); keyHelpLines.push("\1h\1c[ \1n\1cor \1h] \1g: \1n\1cGo to the previous\1g/\1cnext message by 'To user'"); } keyHelpLines.push("\1h\1cF \1n\1cor \1hL \1g: \1n\1cGo to the first\1g/\1clast message in the sub-board"); if (displayChgAreaOpt) { if (this.doingMultiSubBoardScan) keyHelpLines.push("\1h\1c+ \1g: \1n\1cGo to the next message sub-board"); else keyHelpLines.push("\1h\1c-\1n\1c or \1h+ \1g: \1n\1cGo to the previous\1g/\1cnext message sub-board"); keyHelpLines.push("\1h\1cC \1g: \1n\1cChange to a different message sub-board"); } else if (this.doingMultiSubBoardScan) keyHelpLines.push("\1h\1c+ \1g: \1n\1cGo to the next message sub-board"); if (gIsSysop) keyHelpLines.push("\1h\1cE \1g: \1n\1cEdit the current message"); keyHelpLines.push("\1h\1cM \1g: \1n\1cList messages in the current sub-board"); if (gIsSysop) keyHelpLines.push("\1h\1cH \1n\1cor \1hK \1g: \1n\1cDisplay extended header info\1g/\1ckludge lines for the message"); keyHelpLines.push("\1h\1cR \1g: \1n\1cReply to the current message"); if (!this.readingPersonalEmail) { keyHelpLines.push("\1h\1cI \1g: \1n\1cPrivately reply to the current message (via email/NetMail)"); keyHelpLines.push("\1h\1cP \1g: \1n\1cPost a message on the sub-board"); } keyHelpLines.push("\1h\1cNumber \1g: \1n\1cGo to a specific message by number"); keyHelpLines.push("\1h\1cSpacebar \1g: \1n\1cSelect message (for batch delete, etc.)"); keyHelpLines.push(" \1n\1cFor batch delete, open the message list and use CTRL-D."); keyHelpLines.push("\1h\1cQ \1g: \1n\1cQuit back to the BBS"); for (var idx = 0; idx < keyHelpLines.length; ++idx) { console.print("\1n" + keyHelpLines[idx]); console.crlf(); } // Pause and let the user press a key to continue. Note: For some reason, // with console.pause(), not all of the message on the screen would get // refreshed. So instead, we display the system's pause text and input a // key from the user. Calling getKeyWithESCChars() to input a key from the // user to allow for multi-key sequence inputs like PageUp, PageDown, F1, // etc. without printing extra characters on the screen. console.print("\1n" + bbs.text(Pause)); // The "Press a key" text in text.dat getKeyWithESCChars(K_NOSPIN|K_NOCRLF|K_NOECHO); } // For the DigDistMsgReader class: Displays the enhanced reader mode message // header information for a particular message header. // // Parameters: // pMsgHdr: The message header object containing message header info // pDisplayMsgNum: The message number to display, if different from the number // in the header object. This can be null, in which case the // number in the header object will be used. // pStartScreenRow: The row on the screen at which to start displaying the // header information. Will be used if the user's terminal // supports ANSI. function DigDistMsgReader_DisplayEnhancedMsgHdr(pMsgHdr, pDisplayMsgNum, pStartScreenRow) { if ((pMsgHdr == null) || (typeof(pMsgHdr) != "object")) return; if (this.enhMsgHeaderLines == null) return; if ((this.enhMsgHeaderLines.length == 0) || (this.enhMsgHeaderWidth == 0)) return; // Create a formatted date & time string and a string describing the // message attributes var dateTimeStr = pMsgHdr["date"].replace(/ [-+][0-9]+$/, ""); // If the user's terminal supports ANSI, we can move the cursor and // display the header where specified. if (console.term_supports(USER_ANSI)) { // Display the message header information. Make sure the header lines are // centered properly in case the user's terminal is more than 80 characters // wide. var screenX = Math.floor(console.screen_columns/2) - Math.floor(this.enhMsgHeaderWidth/2); if (console.screen_columns > 80) ++screenX; var screenY = (typeof(pStartScreenRow) == "number" ? pStartScreenRow : 1); for (var hdrFileIdx = 0; hdrFileIdx < this.enhMsgHeaderLines.length; ++hdrFileIdx) { console.gotoxy(screenX, screenY++); console.putmsg(this.ParseMsgAtCodes(this.enhMsgHeaderLines[hdrFileIdx], pMsgHdr, pDisplayMsgNum, dateTimeStr, false)); } } else { // The user's terminal doesn't support ANSI - So just output the header // lines. for (var hdrFileIdx = 0; hdrFileIdx < this.enhMsgHeaderLines.length; ++hdrFileIdx) { console.putmsg(this.ParseMsgAtCodes(this.enhMsgHeaderLines[hdrFileIdx], pMsgHdr, pDisplayMsgNum, dateTimeStr, false)); } } } // For the DigDistMsgReader class: Displays the whole/initial scrollbar for a message // in enhanced reader mode. // // Parameters: // pSolidBlockStartRow: The starting row for the solid/bright blocks // pNumSolidBlocks: The number of solid/bright blocks to write function DigDistMsgReader_DisplayEnhancedReaderWholeScrollbar(pSolidBlockStartRow, pNumSolidBlocks) { //console.print("\1n"); var numSolidBlocksWritten = 0; var wroteBrightBlockColor = false; var wroteDimBlockColor = false; for (var screenY = this.msgAreaTop; screenY <= this.msgAreaBottom; ++screenY) { console.gotoxy(this.msgAreaRight+1, screenY); if ((screenY >= pSolidBlockStartRow) && (numSolidBlocksWritten < pNumSolidBlocks)) { if (!wroteBrightBlockColor) { //console.print("\1h\1w"); console.print("\1n" + this.colors.scrollbarScrollBlockColor); wroteBrightBlockColor = true; wroteDimBlockColor = false; } //console.print(BLOCK2); console.print(this.text.scrollbarScrollBlockChar); ++numSolidBlocksWritten; } else { if (!wroteDimBlockColor) { //console.print("\1h\1k"); console.print("\1n" + this.colors.scrollbarBGColor); wroteDimBlockColor = true; } //console.print(BLOCK1); console.print(this.text.scrollbarBGChar); } } } // For the DigDistMsgReader class: Updates the scrollbar for a message, for use // in enhanced reader mode. This does only the necessary character updates to // minimize the number of characters that need to be updated on the screen. // // Parameters: // pNewStartRow: The new (current) start row for solid/bright blocks // pOldStartRow: The old start row for solid/bright blocks // pNumSolidBlocks: The number of solid/bright blocks function DigDistMsgReader_UpdateEnhancedReaderScollbar(pNewStartRow, pOldStartRow, pNumSolidBlocks) { // Calculate the difference in the start row. If the difference is positive, // then the solid block section has moved down; if the diff is negative, the // solid block section has moved up. var solidBlockStartRowDiff = pNewStartRow - pOldStartRow; var oldLastRow = pOldStartRow + pNumSolidBlocks - 1; var newLastRow = pNewStartRow + pNumSolidBlocks - 1; if (solidBlockStartRowDiff > 0) { // The solid block section has moved down if (pNewStartRow > oldLastRow) { // No overlap // Write dim blocks over the old solid block section //console.print("\1n\1h\1k"); console.print("\1n" + this.colors.scrollbarBGColor); for (var screenY = pOldStartRow; screenY <= oldLastRow; ++screenY) { console.gotoxy(this.msgAreaRight+1, screenY); //console.print(BLOCK1); console.print(this.text.scrollbarBGChar); } // Write solid blocks in the new locations //console.print("\1w"); console.print("\1n" + this.colors.scrollbarScrollBlockColor); for (var screenY = pNewStartRow; screenY <= newLastRow; ++screenY) { console.gotoxy(this.msgAreaRight+1, screenY); //console.print(BLOCK2); console.print(this.text.scrollbarScrollBlockChar); } } else { // There is some overlap // Write dim blocks on top //console.print("\1n\1h\1k"); console.print("\1n" + this.colors.scrollbarBGColor); for (var screenY = pOldStartRow; screenY < pNewStartRow; ++screenY) { console.gotoxy(this.msgAreaRight+1, screenY); //console.print(BLOCK1); console.print(this.text.scrollbarBGChar); } // Write bright blocks on the bottom //console.print("\1w"); console.print("\1n" + this.colors.scrollbarScrollBlockColor); for (var screenY = oldLastRow+1; screenY <= newLastRow; ++screenY) { console.gotoxy(this.msgAreaRight+1, screenY); //console.print(BLOCK2); console.print(this.text.scrollbarScrollBlockChar); } } } else if (solidBlockStartRowDiff < 0) { // The solid block section has moved up if (pOldStartRow > newLastRow) { // No overlap // Write dim blocks over the old solid block section //console.print("\1n\1h\1k"); console.print("\1n" + this.colors.scrollbarBGColor); for (var screenY = pOldStartRow; screenY <= oldLastRow; ++screenY) { console.gotoxy(this.msgAreaRight+1, screenY); //console.print(BLOCK1); console.print(this.text.scrollbarBGChar); } // Write solid blocks in the new locations //console.print("\1w"); console.print("\1n" + this.colors.scrollbarScrollBlockColor); for (var screenY = pNewStartRow; screenY <= newLastRow; ++screenY) { console.gotoxy(this.msgAreaRight+1, screenY); //console.print(BLOCK2); console.print(this.text.scrollbarScrollBlockChar); } } else { // There is some overlap // Write bright blocks on top //console.print("\1n\1h\1w"); console.print("\1n" + this.colors.scrollbarScrollBlockColor); var endRow = pOldStartRow; for (var screenY = pNewStartRow; screenY < endRow; ++screenY) { console.gotoxy(this.msgAreaRight+1, screenY); //console.print(BLOCK2); console.print(this.text.scrollbarScrollBlockChar); } // Write dim blocks on the bottom //console.print("\1k"); console.print("\1n" + this.colors.scrollbarBGColor); endRow = pOldStartRow + pNumSolidBlocks; for (var screenY = pNewStartRow+pNumSolidBlocks; screenY < endRow; ++screenY) { console.gotoxy(this.msgAreaRight+1, screenY); //console.print(BLOCK1); console.print(this.text.scrollbarBGChar); } } } } // For the DigDistMsgReader class: Returns whether a particular message is // marked as deleted. If the message base object is not open or the given // offset is out of bounds, this method will return true. // // Parameters: // pOffset: The offset of the message to check // // Return value: Boolean - Whether or not the message is marked as deleted function DigDistMsgReader_MessageIsDeleted(pOffset) { if ((this.msgbase == null) || (!this.msgbase.is_open)) return true; if ((pOffset < 0) || (pOffset >= this.NumMessages())) return true; // Get the message's header and see if it's marked as deleted var msgDeleted = true; var msgHdr = this.GetMsgHdrByIdx(pOffset); if (msgHdr != null) msgDeleted = ((msgHdr.attr & MSG_DELETE) == MSG_DELETE); return msgDeleted; } // For the DigDistMsgReader class: Returns whether a particular message is the // last post in the sub-board from the current logged-in user. // // Parameters: // pOffset: The offset of the message to check // // Return value: Boolean - Whether or not the message is the last post in the // sub-board from the current logged-in user. function DigDistMsgReader_MessageIsLastFromUser(pOffset) { var msgIstLastFromUser = false; if ((this.msgbase != null) && this.msgbase.is_open && (this.msgbase.cfg != null)) { // TODO: Update to handle search results? if ((pOffset >= 0) && (pOffset < this.msgbase.total_msgs)) { // First, see if the message at pOffset was posted by the user. If it // is, then look for the last message posted by the logged-in user and // if found, see if that message has the same offset as the offset // passed in. var msgHdr = this.msgbase.get_msg_header(true, pOffset, true); if (userHandleAliasNameMatch(msgHdr["to"])) { var lastMsgOffsetFromUser = -1; for (var msgOffset = this.msgbase.total_msgs-1; (msgOffset >= pOffset) && (lastMsgOffsetFromUser == -1); --msgOffset) { msgHdr = this.msgbase.get_msg_header(true, msgOffset, true); if (userHandleAliasNameMatch(msgHdr["to"])) lastMsgOffsetFromUser = msgOffset; } // See if the passed-in offset is the last message we found from // the logged-in user. msgIstLastFromUser = (lastMsgOffsetFromUser == pOffset); } } } return msgIstLastFromUser; } // For the DigDistMsgReader class enhanced reader mode: Displays an error at the // bottom of the message area for a moment, then refreshes the last 2 lines in // the message area. If the message string that is passed in is empty or not // a string, then this will simply refresh the last 2 lines of the message area. // // Parameters: // pErrorMsg: The error message to show // pMessageLines: The array of lines from the message being displayed // pTopLineIdx: The index of the line being displayed at the top of the message area // pMsgLineFormatStr: Optional - The format string for message lines function DigDistMsgReader_DisplayEnhReaderError(pErrorMsg, pMessageLines, pTopLineIdx, pMsgLineFormatStr) { var msgLineFormatStr = ""; if (typeof(pMsgLineFormatStr) == "string") msgLineFormatStr = pMsgLineFormatStr; else msgLineFormatStr = "%-" + this.msgAreaWidth + "s"; var originalCurpos = console.getxy(); // Move the cursor to the 2nd to last row of the screen and // show the error. Ideally, I'd like // to put the cursor on the last row of the screen for this, but // console.getnum() lets the enter key shift everything on screen // up one row, and there's no way to avoid that. So, to optimize // screen refreshing, the cursor is placed on the 2nd to the last // row on the screen to prompt for the message number. var promptPos = { x: this.msgAreaLeft, y: console.screen_rows-1 }; // Write a line of characters above where the prompt will be placed, // to help get the user's attention. console.gotoxy(promptPos.x, promptPos.y-1); console.print("\1n" + this.colors.enhReaderPromptSepLineColor); for (var lineCounter = 0; lineCounter < this.msgAreaWidth; ++lineCounter) console.print(HORIZONTAL_SINGLE); // Clear the inside of the message area, so as not to overwrite // the scrollbar character console.print("\1n"); console.gotoxy(promptPos); for (var lineCounter = 0; lineCounter < this.msgAreaWidth; ++lineCounter) console.print(" "); // Show the error if a valid error message was passed in if ((typeof(pErrorMsg) == "string") && (console.strlen(pErrorMsg) > 0)) { writeWithPause(this.msgAreaLeft, console.screen_rows-1, pErrorMsg, ERROR_PAUSE_WAIT_MS, "\1n", true); } // Figure out the indexes of the message for the last lines of // the message and update that line on the screen. // If the index is valid, then output that message line; otherwise, // output a blank line. --promptPos.y; var msgLine = null; var msgLineIndex = pTopLineIdx + this.msgAreaHeight - 2; for (; msgLineIndex <= pTopLineIdx + this.msgAreaHeight - 1; ++msgLineIndex) { console.gotoxy(promptPos); console.print("\1n" + this.colors["msgBodyColor"]); if ((msgLineIndex >= 0) && (msgLineIndex < pMessageLines.length)) { console.print(pMessageLines[msgLineIndex]); // Already shortened to fit console.print("\1n" + this.colors["msgBodyColor"]); // In case colors changed // Clear the rest of the line printf("%" + +(this.msgAreaWidth-console.strlen(pMessageLines[msgLineIndex])) + "s", ""); } else printf(msgLineFormatStr, ""); ++promptPos.y; } // Move the cursor back to its original position console.gotoxy(originalCurpos); } // For the DigDistMsgReader class enhanced reader mode: Prompts for a yes/no // question at the bottom of the message area and refreshes the last 2 lines in // the message area when the user has given an answer. If the question string // that is passed in is empty or not a string, then this will simply refresh the // last 2 lines of the message area, and the return value will default to true. // // Parameters: // pQuestion: The yes/no question to display, without the ? on the end // pMessageLines: The array of lines from the message being displayed // pTopLineIdx: The index of the line being displayed at the top of the message area // pMsgLineFormatStr: The format string for message lines // pSolidScrollBlockStartRow: The starting row for solid scroll blocks (purely for // the kludge of updating the last scrollbar block on the // screen because the yes/no function erases it) // pNumSolidScrollBlocks: The number of solid scroll blocks (purely for the kludge of // updating the last scrollbar block on the screen because the // yes/no function erases it) // pNoYes: Optional boolean - Whether the default response should be No instead of // Yes. Defaults to false (for a default Yes response). // // Return value: Boolean - True if the user selected yes, or false if the user selected no. // If the question string passed in is 0-length or not a valid string, the // return value will be true. function DigDistMsgReader_EnhReaderPromptYesNo(pQuestion, pMessageLines, pTopLineIdx, pMsgLineFormatStr, pSolidScrollBlockStartRow, pNumSolidScrollBlocks, pDefaultNo) { var msgLineFormatStr = ""; if (typeof(pMsgLineFormatStr) == "string") msgLineFormatStr = pMsgLineFormatStr; else msgLineFormatStr = "%-" + +(this.msgAreaWidth) + "s"; var originalCurpos = console.getxy(); // Move the cursor to the 2nd to last row of the screen and // show the error. Ideally, I'd like // to put the cursor on the last row of the screen for this, but // console.getnum() lets the enter key shift everything on screen // up one row, and there's no way to avoid that. So, to optimize // screen refreshing, the cursor is placed on the 2nd to the last // row on the screen to prompt for the message number. var promptPos = { x: this.msgAreaLeft, y: console.screen_rows-1 }; // Write a line of characters above where the prompt will be placed, // to help get the user's attention. console.gotoxy(promptPos.x, promptPos.y-1); console.print("\1n" + this.colors.enhReaderPromptSepLineColor); for (var lineCounter = 0; lineCounter < this.msgAreaWidth; ++lineCounter) console.print(HORIZONTAL_SINGLE); // Clear the inside of the message area, so as not to overwrite // the scrollbar character console.print("\1n"); console.gotoxy(promptPos); for (var lineCounter = 0; lineCounter < this.msgAreaWidth; ++lineCounter) console.print(" "); // Prompt the question if a valid question string was passed in var yesNoResponse = true; if ((typeof(pQuestion) == "string") && (console.strlen(pQuestion) > 0)) { console.gotoxy(this.msgAreaLeft, console.screen_rows-1); var defaultResponseNo = (typeof(pDefaultNo) == "boolean" ? pDefaultNo : false); if (defaultResponseNo) yesNoResponse = !console.noyes(pQuestion); else yesNoResponse = console.yesno(pQuestion); // Kludge: Update the last scroll block on the screen, since the yes/no // prompt erases it. //var scrollBlockChar = "\1n\1h\1k" + BLOCK1; // Dim scroll block // Dim scroll block var scrollBlockChar = this.colors.scrollbarBGColor + this.text.scrollbarBGChar; if ((pSolidScrollBlockStartRow >= console.screen_rows-1) || (pSolidScrollBlockStartRow + pNumSolidScrollBlocks - 1 >= console.screen_rows-1)) { //scrollBlockChar = "\1n\1h\1w" + BLOCK2; // Bright, solid scroll block // Bright, solid scroll block scrollBlockChar = this.colors.scrollbarScrollBlockColor + this.text.scrollbarScrollBlockChar; } console.gotoxy(console.screen_columns, console.screen_rows-1); console.print(scrollBlockChar); } // Figure out the indexes of the message for the last lines of // the message and update that line on the screen. // If the index is valid, then output that message line; otherwise, // output a blank line. --promptPos.y; var msgLine = null; var msgLineIndex = pTopLineIdx + this.msgAreaHeight - 2; for (; msgLineIndex <= pTopLineIdx + this.msgAreaHeight - 1; ++msgLineIndex) { console.gotoxy(promptPos); console.print("\1n" + this.colors["msgBodyColor"]); if ((msgLineIndex >= 0) && (msgLineIndex < pMessageLines.length)) { console.print(pMessageLines[msgLineIndex]); // Already shortened to fit console.print("\1n" + this.colors["msgBodyColor"]); // In case colors changed // Clear the rest of the line printf("%" + +(this.msgAreaWidth-console.strlen(pMessageLines[msgLineIndex])) + "s", ""); } else printf(msgLineFormatStr, ""); ++promptPos.y; } // Move the cursor back to its original position console.gotoxy(originalCurpos); return yesNoResponse; } // For the DigDistMsgReader class: Allows the user to delete a message. Checks // whether the message was posted by the user and prompt for confirmation to // delete it. Checks for delete or delete_last permission. If the sub-board has // delete_last permission enabled, this checks whether the message is the user's // last post on the sub-board and only lets them delete if so. // // Parameters: // pOffset: The offset of the message to be deleted // pPromptLoc: Optional - An object containing x and y properties for the location // on the console of the prompt/error messages // pClearPromptRowAtFirstUse: Optional - A boolean to specify whether or not to // clear the remainder of the prompt row the first // time text is written in that row. // pPromptRowWidth: Optional - The width of the prompt row (if pProptLoc is valid) // pConfirmDelete: Optional boolean - Whether or not to confirm deleting the // message. Defaults to true. // pAttachments: Optional - An array of file attachment information returned by // determineMsgAttachments() // // Return value: Boolean - Whether or not the message was deleted function DigDistMsgReader_PromptAndDeleteMessage(pOffset, pPromptLoc, pClearPromptRowAtFirstUse, pPromptRowWidth, pConfirmDelete, pAttachments) { // Sanity checking if ((pOffset == null) || (typeof(pOffset) != "number")) return false; if ((this.msgbase == null) || (!this.msgbase.is_open)) return false; if (!this.CanDelete() && !this.CanDeleteLastMsg()) return false; if ((pOffset < 0) || (pOffset >= this.NumMessages())) return false; var promptLocValid = ((pPromptLoc != null) && (typeof(pPromptLoc) == "object") && (typeof(pPromptLoc.x) == "number") && (typeof(pPromptLoc.y) == "number")); var msgNum = pOffset + 1; var msgWasDeleted = false; var msgHeader = this.GetMsgHdrByIdx(pOffset); // Only let the user delete one of their own messages or the user // is a sysop. var cannotDeleteError = this.text.cannotDeleteMsgText_notYoursNotASysop.replace("%d", msgNum); var canDeleteMessage = false; if (this.CanDelete()) { if (msgHeader != null) canDeleteMessage = gIsSysop || userHandleAliasNameMatch(msgHeader.from) || this.readingPersonalEmail; else canDeleteMessage = false; } else if (this.CanDeleteLastMsg()) { canDeleteMessage = gIsSysop || this.MessageIsLastFromUser(pOffset); if (!canDeleteMessage) cannotDeleteError = this.text.cannotDeleteMsgText_notLastPostedMsg.replace("%d", msgNum); } if (canDeleteMessage) { // Determine whether or not to delete the message. First, if we are to // have the user confirm whether to delete the message, then ask the // user to confirm first. If we're not to have the user confirm, then // go ahead and delete the message. var deleteMsg = true; // True in case of not confirming deletion var confirmDeleteMsg = (typeof(pConfirmDelete) == "boolean" ? pConfirmDelete : true); if (confirmDeleteMsg) { var delConfirmText = "\1n" + this.text.msgDelConfirmText.replace("%d", msgNum); if (promptLocValid) { // If the caller wants to clear the remainder of the row where the prompt // text will be, then do it. if (pClearPromptRowAtFirstUse) { // Adding 5 to the prompt text to account for the ? and "[X] " that // will be added when console.noyes() is called var promptTxtLen = console.strlen(delConfirmText) + 5; var numCharsRemaining = 0; if (typeof(pPromptRowWidth) == "number") numCharsRemaining = pPromptRowWidth - promptTxtLen; else numCharsRemaining = console.screen_columns - pPromptLoc.x - promptTxtLen; console.print("\1n"); console.gotoxy(pPromptLoc.x+promptTxtLen, pPromptLoc.y); for (var i = 0; i < numCharsRemaining; ++i) console.print(" "); } // Move the cursor to the prompt location console.gotoxy(pPromptLoc); } deleteMsg = !console.noyes(delConfirmText); } // If we are to delete the message, then delete it. if (deleteMsg) { msgWasDeleted = this.msgbase.remove_msg(true, msgHeader.offset); if (msgWasDeleted) { // If there are attachments, then delete them. if (Object.prototype.toString.call(pAttachments) === "[object Array]") { if (pAttachments.length > 0) { for (var attachIdx = 0; attachIdx < pAttachments.length; ++attachIdx) { if (file_exists(pAttachments[attachIdx].fullyPathedFilename)) file_remove(pAttachments[attachIdx].fullyPathedFilename); } } } // In case there are search results, refresh the header in the search // results to enable the deleted attribute. this.RefreshSearchResultMsgHdr(pOffset, MSG_DELETE); // Output a message saying the message has been marked for deletion if (promptLocValid) console.gotoxy(pPromptLoc); console.print("\1n" + this.text.msgDeletedText.replace("%d", msgNum)); if (promptLocValid) console.inkey(K_NOSPIN|K_NOCRLF|K_NOECHO, ERROR_PAUSE_WAIT_MS); else { console.crlf(); console.pause(); } } } } else { if (promptLocValid) console.gotoxy(pPromptLoc); console.print(cannotDeleteError); if (promptLocValid) console.inkey(K_NOSPIN|K_NOCRLF|K_NOECHO, ERROR_PAUSE_WAIT_MS); else { console.crlf(); console.pause(); } } return msgWasDeleted; } // For the DigDistMsgReader class: Allows the user to batch delete selected messages. // Prompts the user for confirmation to delete the selected messages. // // Parameters: // pPromptLoc: Optional - An object containing x and y properties for the location // on the console of the prompt/error messages // pClearPromptRowAtFirstUse: Optional - A boolean to specify whether or not to // clear the remainder of the prompt row the first // time text is written in that row. // pPromptRowWidth: Optional - The width of the prompt row (if pProptLoc is valid) // pConfirmDelete: Optional boolean - Whether or not to confirm deleting the // message. Defaults to true. // // Return value: Boolean - Whether or not all messages were deleted function DigDistMsgReader_PromptAndDeleteSelectedMessages(pPromptLoc, pClearPromptRowAtFirstUse, pPromptRowWidth, pConfirmDelete) { var promptLocValid = ((pPromptLoc != null) && (typeof(pPromptLoc) == "object") && (typeof(pPromptLoc.x) == "number") && (typeof(pPromptLoc.y) == "number")); var allMsgsWereDeleted = false; // If the user is allowed to delete all selected messages, then go ahead // and let the user do it. if (this.AllSelectedMessagesCanBeDeleted()) { // Determine whether or not to delete the message. First, if we are to // have the user confirm whether to delete the message, then ask the // user to confirm first. If we're not to have the user confirm, then // go ahead and delete the message. var deleteMsgs = true; // True in case of not confirming deletion var confirmDeleteMsgs = (typeof(pConfirmDelete) == "boolean" ? pConfirmDelete : true); if (confirmDeleteMsgs) { if (promptLocValid) { // If the caller wants to clear the remainder of the row where the prompt // text will be, then do it. if (pClearPromptRowAtFirstUse) { // Adding 5 to the prompt text to account for the ? and "[X] " that // will be added when console.noyes() is called var promptTxtLen = console.strlen(this.text.delSelectedMsgsConfirmText) + 5; var numCharsRemaining = 0; if (typeof(pPromptRowWidth) == "number") numCharsRemaining = pPromptRowWidth - promptTxtLen; else numCharsRemaining = console.screen_columns - pPromptLoc.x - promptTxtLen; console.print("\1n"); console.gotoxy(pPromptLoc.x+promptTxtLen, pPromptLoc.y); for (var i = 0; i < numCharsRemaining; ++i) console.print(" "); } // Move the cursor to the prompt location console.gotoxy(pPromptLoc); } deleteMsgs = !console.noyes(this.text.delSelectedMsgsConfirmText); } // If we are to delete the messages, then delete it. if (deleteMsgs) { var deleteRetObj = this.DeleteSelectedMessages(); allMsgsWereDeleted = deleteRetObj.deletedAll; // If all selected messages were successfully deleted, then output // a success message. Otherwise, output an error. var statusMsg = ""; if (deleteRetObj.deletedAll) statusMsg = "\1n\1cAll selected messages were deleted."; else statusMsg = "\1n\1h\1y* Failure to delete all selected messages"; if (promptLocValid) { console.gotoxy(pPromptLoc); console.print("\1n"); console.cleartoeol(); } else console.crlf(); console.print(statusMsg); if (promptLocValid) console.inkey(K_NOSPIN|K_NOCRLF|K_NOECHO, ERROR_PAUSE_WAIT_MS); else { console.crlf(); console.pause(); } } } else { // The user is not allowed to delete all selected messages if (promptLocValid) console.gotoxy(pPromptLoc); console.print(this.text.cannotDeleteAllSelectedMsgsText); if (promptLocValid) console.inkey(K_NOSPIN|K_NOCRLF|K_NOECHO, ERROR_PAUSE_WAIT_MS); else { console.crlf(); console.pause(); } } return allMsgsWereDeleted; } /////////////////////////////////////////////////////////////////////////////////// // Methods for message group/sub-board choosing // For the DigDistMsgReader class: Writes the line of key help at the bottom // row of the screen. function DigDistMsgReader_WriteLightbarChgMsgAreaKeysHelpLine() { console.gotoxy(1, console.screen_rows); console.print(this.lightbarAreaChooserHelpLine); } // For the DigDistMsgReader class: Outputs the header line to appear above // the list of message groups. // // Parameters: // pNumPages: The number of pages. This is optional; if this is // not passed, then it won't be used. // pPageNum: The page number. This is optional; if this is not passed, // then it won't be used. function DigDistMsgReader_WriteGrpListTopHdrLine(pNumPages, pPageNum) { var descStr = "Description"; if ((typeof(pPageNum) == "number") && (typeof(pNumPages) == "number")) descStr += " (Page " + pPageNum + " of " + pNumPages + ")"; else if ((typeof(pPageNum) == "number") && (typeof(pNumPages) != "number")) descStr += " (Page " + pPageNum + ")"; else if ((typeof(pPageNum) != "number") && (typeof(pNumPages) == "number")) descStr += " (" + pNumPages + (pNumPages == 1 ? " page)" : " pages)"); printf(this.msgGrpListHdrPrintfStr, "Group#", descStr, "# Sub-Boards"); console.cleartoeol("\1n"); } // For the DigDistMsgReader class: Outputs the first header line to appear // above the sub-board list for a message group. // // Parameters: // pGrpIndex: The index of the message group (assumed to be valid) // pNumPages: The number of pages. This is optional; if this is // not passed, then it won't be used. // pPageNum: The page number. This is optional; if this is not passed, // then it won't be used. function DMsgAreaChooser_WriteSubBrdListHdr1Line(pGrpIndex, pNumPages, pPageNum) { var descFormatStr = "\1n" + this.colors.areaChooserSubBoardHeaderColor + "Sub-boards of \1h%-25s \1n" + this.colors.areaChooserSubBoardHeaderColor; if ((typeof(pPageNum) == "number") && (typeof(pNumPages) == "number")) descFormatStr += "(Page " + pPageNum + " of " + pNumPages + ")"; else if ((typeof(pPageNum) == "number") && (typeof(pNumPages) != "number")) descFormatStr += "(Page " + pPageNum + ")"; else if ((typeof(pPageNum) != "number") && (typeof(pNumPages) == "number")) descFormatStr += "(" + pNumPages + (pNumPages == 1 ? " page)" : " pages)"); printf(descFormatStr, msg_area.grp_list[pGrpIndex].description.substr(0, 25)); console.cleartoeol("\1n"); } // For the DigDistMsgReader class: Lets the user choose a message group and // sub-board via numeric input, using a lightbar interface (if enabled and // if the user's terminal uses ANSI) or a traditional user interface. function DigDistMsgReader_SelectMsgArea() { if (this.msgListUseLightbarListInterface && console.term_supports(USER_ANSI)) this.SelectMsgArea_Lightbar(); else this.SelectMsgArea_Traditional(); } // For the DigDistMsgReader class: Lets the user choose a message group and // sub-board via numeric input, using a lightbar user interface. function DigDistMsgReader_SelectMsgArea_Lightbar() { // If there are no message groups, then don't let the user // choose one. if (msg_area.grp_list.length == 0) { console.clear("\1n"); console.print("\1y\1hThere are no message groups.\r\n\1p"); return; } // Returns the index of the bottommost message group that can be displayed // on the screen. // // Parameters: // pTopGrpIndex: The index of the topmost message group displayed on screen // pNumItemsPerPage: The number of items per page function getBottommostGrpIndex(pTopGrpIndex, pNumItemsPerPage) { var bottomGrpIndex = pTopGrpIndex + pNumItemsPerPage - 1; // If bottomGrpIndex is beyond the last index, then adjust it. if (bottomGrpIndex >= msg_area.grp_list.length) bottomGrpIndex = msg_area.grp_list.length - 1; return bottomGrpIndex; } // Figure out the index of the user's currently-selected message group var selectedGrpIndex = msg_area.sub[this.subBoardCode].grp_index; // Older code: /* var selectedGrpIndex = 0; if ((bbs.curgrp != null) && (typeof(bbs.curgrp) == "number")) selectedGrpIndex = bbs.curgrp; */ var listStartRow = 2; // The row on the screen where the list will start var listEndRow = console.screen_rows - 1; // Row on screen where list will end var topMsgGrpIndex = 0; // The index of the message group at the top of the list // Figure out the index of the last message group to appear on the screen. var numItemsPerPage = listEndRow - listStartRow + 1; var bottomMsgGrpIndex = getBottommostGrpIndex(topMsgGrpIndex, numItemsPerPage); // Figure out how many pages are needed to list all the sub-boards. var numPages = Math.ceil(msg_area.grp_list.length / numItemsPerPage); // Figure out the top index for the last page. var topIndexForLastPage = (numItemsPerPage * numPages) - numItemsPerPage; // If the highlighted row is beyond the current screen, then // go to the appropriate page. if (selectedGrpIndex > bottomMsgGrpIndex) { var nextPageTopIndex = 0; while (selectedGrpIndex > bottomMsgGrpIndex) { nextPageTopIndex = topMsgGrpIndex + numItemsPerPage; if (nextPageTopIndex < msg_area.grp_list.length) { // Adjust topMsgGrpIndex and bottomMsgGrpIndex, and // refresh the list on the screen. topMsgGrpIndex = nextPageTopIndex; bottomMsgGrpIndex = getBottommostGrpIndex(topMsgGrpIndex, numItemsPerPage); } else break; } // If we didn't find the correct page for some reason, then set the // variables to display page 1 and select the first message group. var foundCorrectPage = ((topMsgGrpIndex < msg_area.grp_list.length) && (selectedGrpIndex >= topMsgGrpIndex) && (selectedGrpIndex <= bottomMsgGrpIndex)); if (!foundCorrectPage) { topMsgGrpIndex = 0; bottomMsgGrpIndex = getBottommostGrpIndex(topMsgGrpIndex, numItemsPerPage); selectedGrpIndex = 0; } } // Clear the screen, write the help line and group list header, and output // a screenful of message groups. console.clear("\1n"); this.WriteChgMsgAreaKeysHelpLine(); // Make a backup of the current message group & sub-board indexes so // that later we can tell if the user chose something different. var oldGrp = msg_area.sub[this.subBoardCode].grp_index; var oldSub = msg_area.sub[this.subBoardCode].index; // Older: /* var oldGrp = bbs.curgrp; var oldSub = bbs.cursub; */ // Input loop - Let the user choose a message group & sub-board var curpos = new Object(); curpos.x = 1; curpos.y = 1; console.gotoxy(curpos); var pageNum = calcPageNum(topMsgGrpIndex, numItemsPerPage); this.WriteGrpListHdrLine(numPages, pageNum); this.ListScreenfulOfMsgGrps(topMsgGrpIndex, listStartRow, listEndRow, false, false); // Start of the input loop. var highlightScrenRow = 0; // The row on the screen for the highlighted group var userInput = ""; // Will store a keypress from the user var retObj = null; // To store the return value of choosing a sub-board var continueChoosingMsgArea = true; while (continueChoosingMsgArea) { // Highlight the currently-selected message group highlightScrenRow = listStartRow + (selectedGrpIndex - topMsgGrpIndex); curpos.y = highlightScrenRow; if ((highlightScrenRow > 0) && (highlightScrenRow < console.screen_rows)) { console.gotoxy(1, highlightScrenRow); this.WriteMsgGroupLine(selectedGrpIndex, true); } // Get a key from the user (upper-case) and take action based upon it. userInput = console.getkey(K_UPPER | K_NOCRLF); switch (userInput) { case KEY_UP: // Move up one message group in the list if (selectedGrpIndex > 0) { // If the previous group index is on the previous page, then // display the previous page. var previousGrpIndex = selectedGrpIndex - 1; if (previousGrpIndex < topMsgGrpIndex) { // Adjust topMsgGrpIndex and bottomMsgGrpIndex, and // refresh the list on the screen. topMsgGrpIndex -= numItemsPerPage; bottomMsgGrpIndex = getBottommostGrpIndex(topMsgGrpIndex, numItemsPerPage); this.ListScreenfulOfMsgGrps(topMsgGrpIndex, listStartRow, listEndRow, false, true); } else { // Display the current line un-highlighted. console.gotoxy(1, curpos.y); this.WriteMsgGroupLine(selectedGrpIndex, false); } selectedGrpIndex = previousGrpIndex; } break; case KEY_DOWN: // Move down one message group in the list if (selectedGrpIndex < msg_area.grp_list.length - 1) { // If the next group index is on the next page, then display // the next page. var nextGrpIndex = selectedGrpIndex + 1; if (nextGrpIndex > bottomMsgGrpIndex) { // Adjust topMsgGrpIndex and bottomMsgGrpIndex, and // refresh the list on the screen. topMsgGrpIndex += numItemsPerPage; bottomMsgGrpIndex = getBottommostGrpIndex(topMsgGrpIndex, numItemsPerPage); this.ListScreenfulOfMsgGrps(topMsgGrpIndex, listStartRow, listEndRow, false, true); } else { // Display the current line un-highlighted. console.gotoxy(1, curpos.y); this.WriteMsgGroupLine(selectedGrpIndex, false); } selectedGrpIndex = nextGrpIndex; } break; case KEY_HOME: // Go to the top message group on the screen if (selectedGrpIndex > topMsgGrpIndex) { // Display the current line un-highlighted, then adjust // selectedGrpIndex. console.gotoxy(1, curpos.y); this.WriteMsgGroupLine(selectedGrpIndex, false); selectedGrpIndex = topMsgGrpIndex; // Note: curpos.y is set at the start of the while loop. } break; case KEY_END: // Go to the bottom message group on the screen if (selectedGrpIndex < bottomMsgGrpIndex) { // Display the current line un-highlighted, then adjust // selectedGrpIndex. console.gotoxy(1, curpos.y); this.WriteMsgGroupLine(selectedGrpIndex, false); selectedGrpIndex = bottomMsgGrpIndex; // Note: curpos.y is set at the start of the while loop. } break; case KEY_ENTER: // Select the currently-highlighted message group retObj = this.SelectSubBoard_Lightbar(selectedGrpIndex); // If the user chose a sub-board, then set bbs.curgrp and // bbs.cursub, and don't continue the input loop anymore. if (retObj.subBoardChosen) { bbs.curgrp = selectedGrpIndex; bbs.cursub = retObj.subBoardIndex; continueChoosingMsgArea = false; } else { // A sub-board was not chosen, so we'll have to re-draw // the header and list of message groups. console.gotoxy(1, 1); this.WriteGrpListHdrLine(numPages, pageNum); this.ListScreenfulOfMsgGrps(topMsgGrpIndex, listStartRow, listEndRow, false, true); } break; case 'N': // Go to the next page var nextPageTopIndex = topMsgGrpIndex + numItemsPerPage; if (nextPageTopIndex < msg_area.grp_list.length) { // Adjust topMsgGrpIndex and bottomMsgGrpIndex, and // refresh the list on the screen. topMsgGrpIndex = nextPageTopIndex; pageNum = calcPageNum(topMsgGrpIndex, numItemsPerPage); bottomMsgGrpIndex = getBottommostGrpIndex(topMsgGrpIndex, numItemsPerPage); this.UpdateMsgAreaPageNumInHeader(pageNum, numPages, true, false); this.ListScreenfulOfMsgGrps(topMsgGrpIndex, listStartRow, listEndRow, false, true); selectedGrpIndex = topMsgGrpIndex; } break; case 'P': // Go to the previous page var prevPageTopIndex = topMsgGrpIndex - numItemsPerPage; if (prevPageTopIndex >= 0) { // Adjust topMsgGrpIndex and bottomMsgGrpIndex, and // refresh the list on the screen. topMsgGrpIndex = prevPageTopIndex; pageNum = calcPageNum(topMsgGrpIndex, numItemsPerPage); bottomMsgGrpIndex = getBottommostGrpIndex(topMsgGrpIndex, numItemsPerPage); this.UpdateMsgAreaPageNumInHeader(pageNum, numPages, true, false); this.ListScreenfulOfMsgGrps(topMsgGrpIndex, listStartRow, listEndRow, false, true); selectedGrpIndex = topMsgGrpIndex; } break; case 'F': // Go to the first page if (topMsgGrpIndex > 0) { topMsgGrpIndex = 0; pageNum = calcPageNum(topMsgGrpIndex, numItemsPerPage); bottomMsgGrpIndex = getBottommostGrpIndex(topMsgGrpIndex, numItemsPerPage); this.UpdateMsgAreaPageNumInHeader(pageNum, numPages, true, false); this.ListScreenfulOfMsgGrps(topMsgGrpIndex, listStartRow, listEndRow, false, true); selectedGrpIndex = 0; } break; case 'L': // Go to the last page if (topMsgGrpIndex < topIndexForLastPage) { topMsgGrpIndex = topIndexForLastPage; pageNum = calcPageNum(topMsgGrpIndex, numItemsPerPage); bottomMsgGrpIndex = getBottommostGrpIndex(topMsgGrpIndex, numItemsPerPage); this.UpdateMsgAreaPageNumInHeader(pageNum, numPages, true, false); this.ListScreenfulOfMsgGrps(topMsgGrpIndex, listStartRow, listEndRow, false, true); selectedGrpIndex = topIndexForLastPage; } break; case 'Q': // Quit continueChoosingMsgArea = false; break; case '?': // Show help this.ShowChooseMsgAreaHelpScreen(true, true); console.pause(); // Refresh the screen this.WriteChgMsgAreaKeysHelpLine(); console.gotoxy(1, 1); this.WriteGrpListHdrLine(numPages, pageNum); this.ListScreenfulOfMsgGrps(topMsgGrpIndex, listStartRow, listEndRow, false, true); break; default: // If the user entered a numeric digit, then treat it as // the start of the message group number. if (userInput.match(/[0-9]/)) { var originalCurpos = curpos; // Put the user's input back in the input buffer to // be used for getting the rest of the message number. console.ungetstr(userInput); // Move the cursor to the bottom of the screen and // prompt the user for the message number. console.gotoxy(1, console.screen_rows); console.clearline("\1n"); console.print("\1cChoose group #: \1h"); userInput = console.getnum(msg_area.grp_list.length); // If the user made a selection, then let them choose a // sub-board from the group. if (userInput > 0) { var msgGroupIndex = userInput - 1; retObj = this.SelectSubBoard_Lightbar(msgGroupIndex); // If the user chose a sub-board, then set bbs.curgrp and // bbs.cursub, and don't continue the input loop anymore. if (retObj.subBoardChosen) { // Set the current group & sub-board bbs.curgrp = msgGroupIndex; bbs.cursub = retObj.subBoardIndex; continueChoosingMsgArea = false; } else { // A sub-board was not chosen, so we'll have to re-draw // the header and list of message groups. console.gotoxy(1, 1); this.WriteGrpListHdrLine(numPages, pageNum); this.ListScreenfulOfMsgGrps(topMsgGrpIndex, listStartRow, listEndRow, false, true); } } else { // The user didn't make a selection. So, we need to refresh // the screen due to everything being moved up one line. this.WriteChgMsgAreaKeysHelpLine(); console.gotoxy(1, 1); this.WriteGrpListHdrLine(numPages, pageNum); this.ListScreenfulOfMsgGrps(topMsgGrpIndex, listStartRow, listEndRow, false, true); } } break; } } // If the user chose a different message group & sub-board, then reset the // lister index & cursor variables, as well as this.subBoardCode, etc. if ((bbs.curgrp != oldGrp) || (bbs.cursub != oldSub)) { this.tradListTopMsgIdx = -1; this.lightbarListTopMsgIdx = -1; this.lightbarListSelectedMsgIdx = -1; this.lightbarListCurPos = null; this.setSubBoardCode(msg_area.grp_list[bbs.curgrp].sub_list[bbs.cursub].code); // Re-create the msgbase object for the new sub-board. Don't open it yet, // as that is done in the read/list methods. if (this.msgbase != null) { if (this.msgbase.is_open) this.msgbase.close(); this.msgbase = null; } this.msgbase = new MsgBase(this.subBoardCode); } } // For the DigDistMsgReader class: Lets the user choose a sub-board within a // message group, with a lightbar interface. Does not set bbs.cursub. // // Parameters: // pGrpIndex: The index of the message group to choose from. This is // optional; if not specified, bbs.curgrp will be used. // pMarkIndex: An index of a message group to display the "current" mark // next to. This is optional; if left off, this will default to // the current sub-board. // // Return value: An object containing the following values: // subBoardChosen: Boolean - Whether or not a sub-board was chosen. // subBoardIndex: Numeric - The sub-board that was chosen (if any). // Will be -1 if none chosen. function DigDistMsgReader_SelectSubBoard_Lightbar(pGrpIndex, pMarkIndex) { // Create the return object. var retObj = new Object(); retObj.subBoardChosen = false; retObj.subBoardIndex = -1; var grpIndex = 0; if (typeof(pGrpIndex) == "number") grpIndex = pGrpIndex; else grpIndex = msg_area.sub[this.subBoardCode].grp_index; // Older: /* else if ((bbs.curgrp != null) && (typeof(bbs.curgrp) == "number")) grpIndex = bbs.curgrp; */ // Double-check grpIndex if (grpIndex < 0) grpIndex = 0; else if (grpIndex >= msg_area.grp_list.length) grpIndex = msg_area.grp_list.length - 1; var markIndex = 0; if ((pMarkIndex != null) && (typeof(pMarkIndex) == "number")) markIndex = pMarkIndex; else markIndex = msg_area.sub[this.subBoardCode].index; // Older: /* else if ((bbs.cursub != null) && (typeof(bbs.cursub) == "number") && (bbs.curgrp == pGrpIndex)) { markIndex = bbs.cursub; } */ // Double-check markIndex if (markIndex < 0) markIndex = 0; else if (markIndex >= msg_area.grp_list[grpIndex].sub_list.length) markIndex = msg_area.grp_list[grpIndex].sub_list.length - 1; // Ensure that the sub-board printf information is created for // this message group. this.BuildSubBoardPrintfInfoForGrp(grpIndex); // If there are no sub-boards in the given message group, then show // an error and return. if (msg_area.grp_list[grpIndex].sub_list.length == 0) { console.clear("\1n"); console.print("\1y\1hThere are no sub-boards in the chosen group.\r\n\1p"); return retObj; } // Returns the index of the bottommost sub-board that can be displayed on // the screen. // // Parameters: // pTopSubIndex: The index of the topmost sub-board displayed on screen // pNumItemsPerPage: The number of items per page function getBottommostSubIndex(pTopSubIndex, pNumItemsPerPage) { var bottomGrpIndex = topSubIndex + pNumItemsPerPage - 1; // If bottomGrpIndex is beyond the last index, then adjust it. if (bottomGrpIndex >= msg_area.grp_list[grpIndex].sub_list.length) bottomGrpIndex = msg_area.grp_list[grpIndex].sub_list.length - 1; return bottomGrpIndex; } // Figure out the index of the user's currently-selected sub-board. var selectedSubIndex = 0; if (msg_area.sub[this.subBoardCode].grp_index == pGrpIndex) selectedSubIndex = msg_area.sub[this.subBoardCode].index; /* var selectedSubIndex = 0; if ((bbs.cursub != null) && (typeof(bbs.cursub) == "number")) { if ((bbs.curgrp != null) && (typeof(bbs.curgrp) == "number") && (bbs.curgrp == pGrpIndex)) { selectedSubIndex = bbs.cursub; } } */ var listStartRow = 3; // The row on the screen where the list will start var listEndRow = console.screen_rows - 1; // Row on screen where list will end var topSubIndex = 0; // The index of the message group at the top of the list // Figure out the index of the last message group to appear on the screen. var numItemsPerPage = listEndRow - listStartRow + 1; var bottomSubIndex = getBottommostSubIndex(topSubIndex, numItemsPerPage); // Figure out how many pages are needed to list all the sub-boards. var numPages = Math.ceil(msg_area.grp_list[grpIndex].sub_list.length / numItemsPerPage); // Figure out the top index for the last page. var topIndexForLastPage = (numItemsPerPage * numPages) - numItemsPerPage; // If the highlighted row is beyond the current screen, then // go to the appropriate page. if (selectedSubIndex > bottomSubIndex) { var nextPageTopIndex = 0; while (selectedSubIndex > bottomSubIndex) { nextPageTopIndex = topSubIndex + numItemsPerPage; if (nextPageTopIndex < msg_area.grp_list[grpIndex].sub_list.length) { // Adjust topSubIndex and bottomSubIndex, and // refresh the list on the screen. topSubIndex = nextPageTopIndex; bottomSubIndex = getBottommostSubIndex(topSubIndex, numItemsPerPage); } else break; } // If we didn't find the correct page for some reason, then set the // variables to display page 1 and select the first message group. var foundCorrectPage = ((topSubIndex < msg_area.grp_list[grpIndex].sub_list.length) && (selectedSubIndex >= topSubIndex) && (selectedSubIndex <= bottomSubIndex)); if (!foundCorrectPage) { topSubIndex = 0; bottomSubIndex = getBottommostSubIndex(topSubIndex, numItemsPerPage); selectedSubIndex = 0; } } // Clear the screen, write the help line and group list header, and output // a screenful of message groups. console.clear("\1n"); var pageNum = calcPageNum(topSubIndex, numItemsPerPage); this.WriteSubBrdListHdr1Line(grpIndex, numPages, pageNum); this.WriteChgMsgAreaKeysHelpLine(); var curpos = new Object(); curpos.x = 1; curpos.y = 2; console.gotoxy(curpos); printf(this.subBoardListHdrPrintfStr, "Sub #", "Name", "# Posts", "Latest date & time"); this.ListScreenfulOfSubBrds(grpIndex, topSubIndex, listStartRow, listEndRow, false, false); // Start of the input loop. var highlightScrenRow = 0; // The row on the screen for the highlighted group var userInput = ""; // Will store a keypress from the user var continueChoosingSubBrd = true; while (continueChoosingSubBrd) { // Highlight the currently-selected message group highlightScrenRow = listStartRow + (selectedSubIndex - topSubIndex); curpos.y = highlightScrenRow; if ((highlightScrenRow > 0) && (highlightScrenRow < console.screen_rows)) { console.gotoxy(1, highlightScrenRow); this.WriteMsgSubBoardLine(grpIndex, selectedSubIndex, true); } // Get a key from the user (upper-case) and take action based upon it. userInput = console.getkey(K_UPPER | K_NOCRLF); switch (userInput) { case KEY_UP: // Move up one message group in the list if (selectedSubIndex > 0) { // If the previous group index is on the previous page, then // display the previous page. var previousSubIndex = selectedSubIndex - 1; if (previousSubIndex < topSubIndex) { // Adjust topSubIndex and bottomSubIndex, and // refresh the list on the screen. topSubIndex -= numItemsPerPage; bottomSubIndex = getBottommostSubIndex(topSubIndex, numItemsPerPage); pageNum = calcPageNum(topSubIndex, numItemsPerPage); this.UpdateMsgAreaPageNumInHeader(pageNum, numPages, false, false); this.ListScreenfulOfSubBrds(grpIndex, topSubIndex, listStartRow, listEndRow, false, true); } else { // Display the current line un-highlighted. console.gotoxy(1, curpos.y); this.WriteMsgSubBoardLine(grpIndex, selectedSubIndex, false); } selectedSubIndex = previousSubIndex; } break; case KEY_DOWN: // Move down one message group in the list if (selectedSubIndex < msg_area.grp_list[grpIndex].sub_list.length - 1) { // If the next group index is on the next page, then display // the next page. var nextGrpIndex = selectedSubIndex + 1; if (nextGrpIndex > bottomSubIndex) { // Adjust topSubIndex and bottomSubIndex, and // refresh the list on the screen. topSubIndex += numItemsPerPage; bottomSubIndex = getBottommostSubIndex(topSubIndex, numItemsPerPage); pageNum = calcPageNum(topSubIndex, numItemsPerPage); this.UpdateMsgAreaPageNumInHeader(pageNum, numPages, false, false); this.ListScreenfulOfSubBrds(grpIndex, topSubIndex, listStartRow, listEndRow, false, true); } else { // Display the current line un-highlighted. console.gotoxy(1, curpos.y); this.WriteMsgSubBoardLine(grpIndex, selectedSubIndex, false); } selectedSubIndex = nextGrpIndex; } break; case KEY_HOME: // Go to the top message group on the screen if (selectedSubIndex > topSubIndex) { // Display the current line un-highlighted, then adjust // selectedSubIndex. console.gotoxy(1, curpos.y); this.WriteMsgSubBoardLine(grpIndex, selectedSubIndex, false); selectedSubIndex = topSubIndex; // Note: curpos.y is set at the start of the while loop. } break; case KEY_END: // Go to the bottom message group on the screen if (selectedSubIndex < bottomSubIndex) { // Display the current line un-highlighted, then adjust // selectedSubIndex. console.gotoxy(1, curpos.y); this.WriteMsgSubBoardLine(grpIndex, selectedSubIndex, false); selectedSubIndex = bottomSubIndex; // Note: curpos.y is set at the start of the while loop. } break; case KEY_ENTER: // Select the currently-highlighted sub-board // Validate the sub-board choice. If a search is specified, the // validator function will search for messages in the selected // sub-board and will return true if there are messages to read // there or false if not. If there is no search specified, // the validator function will return a 'true' value. var msgAreaValidRetval = this.ValidateMsgAreaChoice(grpIndex, selectedSubIndex, curpos); if (msgAreaValidRetval.msgAreaGood) { continueChoosingSubBrd = false; retObj.subBoardChosen = true; retObj.subBoardIndex = selectedSubIndex; } else { // Output the error that was returned by the validator function console.gotoxy(1, curpos.y); console.cleartoeol("\1n"); console.print("\1h\1y" + msgAreaValidRetval.errorMsg); mswait(ERROR_PAUSE_WAIT_MS); console.print("\1n"); continueChoosingSubBrd = true; retObj.subBoardChosen = false; retObj.subBoardIndex = -1; } break; case 'N': // Go to the next page var nextPageTopIndex = topSubIndex + numItemsPerPage; if (nextPageTopIndex < msg_area.grp_list[grpIndex].sub_list.length) { // Adjust topSubIndex and bottomSubIndex, and // refresh the list on the screen. topSubIndex = nextPageTopIndex; pageNum = calcPageNum(topSubIndex, numItemsPerPage); bottomSubIndex = getBottommostSubIndex(topSubIndex, numItemsPerPage); this.UpdateMsgAreaPageNumInHeader(pageNum, numPages, false, false); this.ListScreenfulOfSubBrds(grpIndex, topSubIndex, listStartRow, listEndRow, false, true); selectedSubIndex = topSubIndex; } break; case 'P': // Go to the previous page var prevPageTopIndex = topSubIndex - numItemsPerPage; if (prevPageTopIndex >= 0) { // Adjust topSubIndex and bottomSubIndex, and // refresh the list on the screen. topSubIndex = prevPageTopIndex; pageNum = calcPageNum(topSubIndex, numItemsPerPage); bottomSubIndex = getBottommostSubIndex(topSubIndex, numItemsPerPage); this.UpdateMsgAreaPageNumInHeader(pageNum, numPages, false, false); this.ListScreenfulOfSubBrds(grpIndex, topSubIndex, listStartRow, listEndRow, false, true); selectedSubIndex = topSubIndex; } break; case 'F': // Go to the first page if (topSubIndex > 0) { topSubIndex = 0; pageNum = calcPageNum(topSubIndex, numItemsPerPage); bottomSubIndex = getBottommostSubIndex(topSubIndex, numItemsPerPage); this.UpdateMsgAreaPageNumInHeader(pageNum, numPages, false, false); this.ListScreenfulOfSubBrds(grpIndex, topSubIndex, listStartRow, listEndRow, false, true); selectedSubIndex = 0; } break; case 'L': // Go to the last page if (topSubIndex < topIndexForLastPage) { topSubIndex = topIndexForLastPage; pageNum = calcPageNum(topSubIndex, numItemsPerPage); bottomSubIndex = getBottommostSubIndex(topSubIndex, numItemsPerPage); this.UpdateMsgAreaPageNumInHeader(pageNum, numPages, false, false); this.ListScreenfulOfSubBrds(grpIndex, topSubIndex, listStartRow, listEndRow, false, true); selectedSubIndex = topIndexForLastPage; } break; case 'Q': // Quit continueChoosingSubBrd = false; break; case '?': // Show help this.ShowChooseMsgAreaHelpScreen(true, true); console.pause(); // Refresh the screen console.gotoxy(1, 1); this.WriteSubBrdListHdr1Line(grpIndex, numPages, pageNum); console.cleartoeol("\1n"); this.WriteChgMsgAreaKeysHelpLine(); console.gotoxy(1, 2); printf(this.subBoardListHdrPrintfStr, "Sub #", "Name", "# Posts", "Latest date & time"); this.ListScreenfulOfSubBrds(grpIndex, topSubIndex, listStartRow, listEndRow, false, true); break; default: // If the user entered a numeric digit, then treat it as // the start of the message sub-board number. if (userInput.match(/[0-9]/)) { var originalCurpos = curpos; // Put the user's input back in the input buffer to // be used for getting the rest of the message number. console.ungetstr(userInput); // Move the cursor to the bottom of the screen and // prompt the user for the message number. console.gotoxy(1, console.screen_rows); console.clearline("\1n"); console.print("\1cSub-board #: \1h"); userInput = console.getnum(msg_area.grp_list[grpIndex].sub_list.length); // If the user made a selection, then set it in the // return object and don't continue the input loop. if (userInput > 0) { // Validate the sub-board choice. If a search is specified, the // validator function will search for messages in the selected // sub-board and will return true if there are messages to read // there or false if not. If there is no search specified, // the validator function will return a 'true' value. var msgAreaValidRetval = this.ValidateMsgAreaChoice(grpIndex, selectedSubIndex, curpos); if (msgAreaValidRetval.msgAreaGood) { continueChoosingSubBrd = false; retObj.subBoardChosen = true; retObj.subBoardIndex = userInput - 1; } else { // Output the error that was returned by the validator function console.print("\1n\1y\1h" + msgAreaValidRetval.errorMsg); mswait(ERROR_PAUSE_WAIT_MS); // Set our loop variables so that we continue the sub-board // choosing loop. continueChoosingSubBrd = true; retObj.subBoardChosen = false; retObj.subBoardIndex = -1; // Since the message area selection failed, we need to // re-draw the screen due to everything being moved up one // line. console.gotoxy(1, 1); this.WriteSubBrdListHdr1Line(grpIndex, numPages, pageNum); console.cleartoeol("\1n"); this.WriteChgMsgAreaKeysHelpLine(); console.gotoxy(1, 2); printf(this.subBoardListHdrPrintfStr, "Sub #", "Name", "# Posts", "Latest date & time"); this.ListScreenfulOfSubBrds(grpIndex, topSubIndex, listStartRow, listEndRow, false, true); } } else { // The user didn't enter a selection. Now we need to re-draw // the screen due to everything being moved up one line. console.gotoxy(1, 1); this.WriteSubBrdListHdr1Line(grpIndex, numPages, pageNum); console.cleartoeol("\1n"); this.WriteChgMsgAreaKeysHelpLine(); console.gotoxy(1, 2); printf(this.subBoardListHdrPrintfStr, "Sub #", "Name", "# Posts", "Latest date & time"); this.ListScreenfulOfSubBrds(grpIndex, topSubIndex, listStartRow, listEndRow, false, true); } } break; } } return retObj; } // For the DigDistMsgReader class: Lets the user choose a message group and // sub-board via numeric input, using a traditional user interface. function DigDistMsgReader_SelectMsgArea_Traditional() { // If there are no message groups, then don't let the user // choose one. if (msg_area.grp_list.length == 0) { console.clear("\1n"); console.print("\1y\1hThere are no message groups.\r\n\1p"); return; } // Make a backup of the current message group & sub-board indexes so // that later we can tell if the user chose something different. var oldGrp = msg_area.sub[this.subBoardCode].grp_index; var oldSub = msg_area.sub[this.subBoardCode].index; // Older: /* var oldGrp = bbs.curgrp; var oldSub = bbs.cursub; */ // Show the message groups & sub-boards and let the user choose one. var selectedGrp = 0; // The user's selected message group var selectedSubBoard = 0; // The user's selected sub-board var continueChoosingMsgGroup = true; while (continueChoosingMsgGroup) { // Clear the BBS command string to make sure there are no extra // commands in there that could cause weird things to happen. bbs.command_str = ""; console.clear("\1n"); this.ListMsgGrps(); console.crlf(); console.print("\1n\1b\1h� \1n\1cWhich, \1hQ\1n\1cuit, or [\1h" + +(msg_area.sub[this.subBoardCode].grp_index+1) + "\1n\1c]: \1h"); // Older: /* console.print("\1n\1b\1h� \1n\1cWhich, \1hQ\1n\1cuit, or [\1h" + +(bbs.curgrp+1) + "\1n\1c]: \1h"); */ // Accept Q (quit) or a file library number selectedGrp = console.getkeys("Q", msg_area.grp_list.length); // If the user just pressed enter (selectedGrp would be blank), // default to the current group. if (selectedGrp.toString() == "") selectedGrp = msg_area.sub[this.subBoardCode].grp_index + 1; // Older: /* if (selectedGrp.toString() == "") selectedGrp = bbs.curgrp + 1; */ if (selectedGrp.toString() == "Q") continueChoosingMsgGroup = false; else { // If the user specified a message group number, then // set it and let the user choose a sub-board within // the group. if (selectedGrp > 0) { // Set the default sub-board #: The current sub-board, or if the // user chose a different group, then this should be set // to the first sub-board. var defaultSubBoard = msg_area.sub[this.subBoardCode].index + 1; if (selectedGrp-1 != msg_area.sub[this.subBoardCode].grp_index) defaultSubBoard = 1; // Older: /* var defaultSubBoard = bbs.cursub + 1; if (selectedGrp-1 != bbs.curgrp) defaultSubBoard = 1; */ var continueChoosingSubBoard = true; while (continueChoosingSubBoard) { console.clear("\1n"); this.ListSubBoardsInMsgGroup(selectedGrp-1, defaultSubBoard-1); console.crlf(); console.print("\1n\1b\1h� \1n\1cWhich, \1hQ\1n\1cuit, or [\1h" + defaultSubBoard + "\1n\1c]: \1h"); // Accept Q (quit) or a sub-board number selectedSubBoard = console.getkeys("Q", msg_area.grp_list[selectedGrp - 1].sub_list.length); // If the user just pressed enter (selectedSubBoard would be blank), // default the selected directory. if (selectedSubBoard.toString() == "") selectedSubBoard = defaultSubBoard; // If the user chose to quit out of the sub-board list, then // return to the message group list. if (selectedSubBoard.toString() == "Q") continueChoosingSubBoard = false; // If the user chose a message sub-board, then validate the user's // sub-board choice; if that succeeds, then change the user's // sub-board to that and quit out of the chooser loops. else if (selectedSubBoard > 0) { // Validate the sub-board choice. If a search is specified, the // validator function will search for messages in the selected // sub-board and will return true if there are messages to read // there or false if not. If there is no search specified, // the validator function will return a 'true' value. var selectedGrpIdx = selectedGrp - 1; var selectedSubIdx = selectedSubBoard - 1; var msgAreaValidRetval = this.ValidateMsgAreaChoice(selectedGrpIdx, selectedSubIdx); if (msgAreaValidRetval.msgAreaGood) { bbs.curgrp = selectedGrpIdx; bbs.cursub = selectedSubIdx; continueChoosingSubBoard = false; continueChoosingMsgGroup = false; } else { // Output the error returned by the validator function console.print("\1n\1h\1y" + msgAreaValidRetval.errorMsg); mswait(ERROR_PAUSE_WAIT_MS); // Set our loop variables to continue allowing the user to // choose a message sub-board continueChoosingSubBoard = true; continueChoosingMsgGroup = true; } } } } } } // If the user chose a different message group & sub-board, then reset the // lister index & cursor variables, as well as this.subBoardCode, etc. //msg_area.sub[this.subBoardCode].grp_index if ((bbs.curgrp != oldGrp) || (bbs.cursub != oldSub)) { this.tradListTopMsgIdx = -1; this.lightbarListTopMsgIdx = -1; this.lightbarListSelectedMsgIdx = -1; this.lightbarListCurPos = null; this.setSubBoardCode(msg_area.grp_list[bbs.curgrp].sub_list[bbs.cursub].code); // Re-create the msgbase object for the new sub-board. Don't open it yet, // as that is done in the read/list methods. if (this.msgbase != null) { if (this.msgbase.is_open) this.msgbase.close(); this.msgbase = null; } this.msgbase = new MsgBase(this.subBoardCode); } } // For the DigDistMsgReader class: Lists all message groups (for the traditional // user interface). function DigDistMsgReader_ListMsgGrps_Traditional() { // Print the header this.WriteGrpListHdrLine(); console.print("\1n"); // List the message groups for (var i = 0; i < msg_area.grp_list.length; ++i) { console.crlf(); this.WriteMsgGroupLine(i, false); } } // For the DigDistMsgReader class: Lists the sub-boards in a message group, // for the traditional user interface. // // Parameters: // pGrpIndex: The index of the message group (0-based) // pMarkIndex: An index of a message group to highlight. This // is optional; if left off, this will default to // the current sub-board. // pSortType: Optional - A string describing how to sort the list (if desired): // "none": Default behavior - Sort by sub-board # // "dateAsc": Sort by date, ascending // "dateDesc": Sort by date, descending // "description": Sort by description function DigDistMsgReader_ListSubBoardsInMsgGroup_Traditional(pGrpIndex, pMarkIndex, pSortType) { // Default to the current message group & sub-board if pGrpIndex // and pMarkIndex aren't specified. var grpIndex = bbs.curgrp; if ((pGrpIndex != null) && (typeof(pGrpIndex) == "number")) grpIndex = pGrpIndex; var highlightIndex = bbs.cursub; if ((pMarkIndex != null) && (typeof(pMarkIndex) == "number")) highlightIndex = pMarkIndex; // Make sure grpIndex and highlightIndex are valid (they might not be for // brand-new users). if ((grpIndex == null) || (typeof(grpIndex) == "undefined")) grpIndex = 0; if ((highlightIndex == null) || (typeof(highlightIndex) == "undefined")) highlightIndex = 0; // Ensure that the sub-board printf information is created for // this message group. this.BuildSubBoardPrintfInfoForGrp(grpIndex); // Print the headers this.WriteSubBrdListHdr1Line(grpIndex); console.crlf(); printf(this.subBoardListHdrPrintfStr, "Sub #", "Name", "# Posts", "Latest date & time"); console.print("\1n"); // List each sub-board in the message group. var subBoardArray = null; // For sorting, if desired var newestDate = new Object(); // For storing the date of the newest post in a sub-board var msgBase = null; // For opening the sub-boards with a MsgBase object var msgHeader = null; // For getting the date & time of the newest post in a sub-board var subBoardNum = 0; // 0-based sub-board number (because the array index is the number as a str) // If a sort type is specified, then add the sub-board information to // subBoardArray so that it can be sorted. if ((typeof(pSortType) == "string") && (pSortType != "") && (pSortType != "none")) { subBoardArray = new Array(); var subBoardInfo = null; for (var arrSubBoardNum in msg_area.grp_list[grpIndex].sub_list) { // Open the current sub-board with the msgBase object. msgBase = new MsgBase(msg_area.grp_list[grpIndex].sub_list[arrSubBoardNum].code); if (msgBase.open()) { subBoardInfo = new MsgSubBoardInfo(); subBoardInfo.subBoardNum = +(arrSubBoardNum); subBoardInfo.description = msg_area.grp_list[grpIndex].sub_list[arrSubBoardNum].description; subBoardInfo.numPosts = msgBase.total_msgs; // Get the date & time when the last message was imported. if (msgBase.total_msgs > 0) { msgHeader = msgBase.get_msg_header(true, msgBase.total_msgs-1, true); if (this.msgAreaList_lastImportedMsg_showImportTime) subBoardInfo.newestPostDate = msgHeader.when_imported_time else subBoardInfo.newestPostDate = msgHeader.when_written_time; } } msgBase.close(); subBoardArray.push(subBoardInfo); } // Free some memory? delete msgBase; // Possibly sort the sub-board list. if (pSortType == "dateAsc") { subBoardArray.sort(function(pA, pB) { // Return -1, 0, or 1, depending on whether pA's date comes // before, is equal to, or comes after pB's date. var returnValue = 0; if (pA.newestPostDate < pB.newestPostDate) returnValue = -1; else if (pA.newestPostDate > pB.newestPostDate) returnValue = 1; return returnValue; }); } else if (pSortType == "dateDesc") { subBoardArray.sort(function(pA, pB) { // Return -1, 0, or 1, depending on whether pA's date comes // after, is equal to, or comes before pB's date. var returnValue = 0; if (pA.newestPostDate > pB.newestPostDate) returnValue = -1; else if (pA.newestPostDate < pB.newestPostDate) returnValue = 1; return returnValue; }); } else if (pSortType == "description") { // Binary safe string comparison // // version: 909.322 // discuss at: http://phpjs.org/functions/strcmp // + original by: Waldo Malqui Silva // + input by: Steve Hilder // + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) // + revised by: gorthaur // * example 1: strcmp( 'waldo', 'owald' ); // * returns 1: 1 // * example 2: strcmp( 'owald', 'waldo' ); // * returns 2: -1 subBoardArray.sort(function(pA, pB) { return ((pA.description == pB.description) ? 0 : ((pA.description > pB.description) ? 1 : -1)); }); } // Display the sub-board list. for (var i = 0; i < subBoardArray.length; ++i) { console.crlf(); console.print((subBoardArray[i].subBoardNum == highlightIndex) ? "\1n" + this.colors.areaChooserMsgAreaMarkColor + "*" : " "); printf(this.subBoardListPrintfInfo[grpIndex].printfStr, +(subBoardArray[i].subBoardNum+1), subBoardArray[i].description.substr(0, this.subBoardNameLen), subBoardArray[i].numPosts, strftime("%Y-%m-%d", subBoardArray[i].newestPostDate), strftime("%H:%M:%S", subBoardArray[i].newestPostDate)); } } // If no sort type is specified, then output the sub-board information in // order of sub-board number. else { for (var arrSubBoardNum in msg_area.grp_list[grpIndex].sub_list) { // Open the current sub-board with the msgBase object. msgBase = new MsgBase(msg_area.grp_list[grpIndex].sub_list[arrSubBoardNum].code); if (msgBase.open()) { // Get the date & time when the last message was imported. if (msgBase.total_msgs > 0) { msgHeader = msgBase.get_msg_header(true, msgBase.total_msgs-1, true); // Construct the date & time strings of the latest post if (this.msgAreaList_lastImportedMsg_showImportTime) { newestDate.date = strftime("%Y-%m-%d", msgHeader.when_imported_time); newestDate.time = strftime("%H:%M:%S", msgHeader.when_imported_time); } else { newestDate.date = strftime("%Y-%m-%d", msgHeader.when_written_time); newestDate.time = strftime("%H:%M:%S", msgHeader.when_written_time); } } else newestDate.date = newestDate.time = ""; // Print the sub-board information subBoardNum = +(arrSubBoardNum); console.crlf(); console.print((subBoardNum == highlightIndex) ? "\1n" + this.colors.areaChooserMsgAreaMarkColor + "*" : " "); printf(this.subBoardListPrintfInfo[grpIndex].printfStr, +(subBoardNum+1), msg_area.grp_list[grpIndex].sub_list[arrSubBoardNum].description.substr(0, this.subBoardListPrintfInfo[grpIndex].nameLen), msgBase.total_msgs, newestDate.date, newestDate.time); msgBase.close(); } // Free some memory? delete msgBase; } } } ////////////////////////////////////////////// // Message group list stuff (lightbar mode) // ////////////////////////////////////////////// // Displays a screenful of message groups, for the lightbar interface. // // Parameters: // pStartIndex: The message group index to start at (0-based) // pStartScreenRow: The row on the screen to start at (1-based) // pEndScreenRow: The row on the screen to end at (1-based) // pClearScreenFirst: Boolean - Whether or not to clear the screen first // pBlankToEndRow: Boolean - Whether or not to write blank lines to the end // screen row if there aren't enough message groups to fill // the screen. function DigDistMsgReader_listScreenfulOfMsgGrps(pStartIndex, pStartScreenRow, pEndScreenRow, pClearScreenFirst, pBlankToEndRow) { // Check the parameters; If they're bad, then just return. if ((typeof(pStartIndex) != "number") || (typeof(pStartScreenRow) != "number") || (typeof(pEndScreenRow) != "number")) { return; } if ((pStartIndex < 0) || (pStartIndex >= msg_area.grp_list.length)) return; if ((pStartScreenRow < 1) || (pStartScreenRow > console.screen_rows)) return; if ((pEndScreenRow < 1) || (pEndScreenRow > console.screen_rows)) return; // If pStartScreenRow is greather than pEndScreenRow, then swap them. if (pStartScreenRow > pEndScreenRow) { var temp = pStartScreenRow; pStartScreenRow = pEndScreenRow; pEndScreenRow = temp; } // Calculate the ending index to use for the message groups array. var endIndex = pStartIndex + (pEndScreenRow-pStartScreenRow); if (endIndex >= msg_area.grp_list.length) endIndex = msg_area.grp_list.length - 1; var onePastEndIndex = endIndex + 1; // Clear the screen, go to the specified screen row, and display the message // group information. if (pClearScreenFirst) console.clear("\1n"); console.gotoxy(1, pStartScreenRow); var grpIndex = pStartIndex; for (; grpIndex < onePastEndIndex; ++grpIndex) { this.WriteMsgGroupLine(grpIndex, false); if (grpIndex < endIndex) console.crlf(); } // If pBlankToEndRow is true and we're not at the end row yet, then // write blank lines to the end row. if (pBlankToEndRow) { var screenRow = pStartScreenRow + (endIndex - pStartIndex) + 1; if (screenRow <= pEndScreenRow) { for (; screenRow <= pEndScreenRow; ++screenRow) { console.gotoxy(1, screenRow); console.clearline("\1n"); } } } } // For the DigDistMsgReader class - Writes a message group information line. // // Parameters: // pGrpIndex: The index of the message group to write (assumed to be valid) // pHighlight: Boolean - Whether or not to write the line highlighted. function DigDistMsgReader_writeMsgGroupLine(pGrpIndex, pHighlight) { // TODO: If pHighlight is true, that causes the screen to be cleared // and the line is written on the first row of the console. console.print("\1n"); // Write the highlight background color if pHighlight is true. if (pHighlight) console.print(this.colors.areaChooserMsgAreaBkgHighlightColor); // Write the message group information line console.print(((typeof(bbs.curgrp) == "number") && (pGrpIndex == msg_area.sub[this.subBoardCode].grp_index)) ? this.colors.areaChooserMsgAreaMarkColor + "*" : " "); printf((pHighlight ? this.msgGrpListHilightPrintfStr : this.msgGrpListPrintfStr), +(pGrpIndex+1), msg_area.grp_list[pGrpIndex].description.substr(0, this.msgGrpDescLen), msg_area.grp_list[pGrpIndex].sub_list.length); console.cleartoeol("\1n"); } ////////////////////////////////////////////////// // Message sub-board list stuff (lightbar mode) // ////////////////////////////////////////////////// // Updates the page number text in the group list header line on the screen. // // Parameters: // pPageNum: The page number // pNumPages: The total number of pages // pGroup: Boolean - Whether or not this is for the group header. If so, // then this will go to the right location for the group page text // and use this.colors.areaChooserMsgAreaHeaderColor for the text. // Otherwise, this will go to the right place for the sub-board page // text and use the sub-board header color. // pRestoreCurPos: Optional - Boolean - If true, then move the cursor back // to the position where it was before this function was called function DigDistMsgReader_updateMsgAreaPageNumInHeader(pPageNum, pNumPages, pGroup, pRestoreCurPos) { var originalCurPos = null; if (pRestoreCurPos) originalCurPos = console.getxy(); if (pGroup) { console.gotoxy(29, 1); console.print("\1n" + this.colors.areaChooserMsgAreaHeaderColor + pPageNum + " of " + pNumPages + ") "); } else { console.gotoxy(51, 1); console.print("\1n" + this.colors.areaChooserSubBoardHeaderColor + pPageNum + " of " + pNumPages + ") "); } if (pRestoreCurPos) console.gotoxy(originalCurPos); } // Displays a screenful of message sub-boards, for the lightbar interface. // // Parameters: // pGrpIndex: The index of the message group (0-based) // pStartSubIndex: The message sub-board index to start at (0-based) // pStartScreenRow: The row on the screen to start at (1-based) // pEndScreenRow: The row on the screen to end at (1-based) // pClearScreenFirst: Boolean - Whether or not to clear the screen first // pBlankToEndRow: Boolean - Whether or not to write blank lines to the end // screen row if there aren't enough message groups to fill // the screen. function DigDistMsgReader_ListScreenfulOfSubBrds(pGrpIndex, pStartSubIndex, pStartScreenRow, pEndScreenRow, pClearScreenFirst, pBlankToEndRow) { // Check the parameters; If they're bad, then just return. if ((typeof(pGrpIndex) != "number") || (typeof(pStartSubIndex) != "number") || (typeof(pStartScreenRow) != "number") || (typeof(pEndScreenRow) != "number")) { return; } if ((pGrpIndex < 0) || (pGrpIndex >= msg_area.grp_list.length)) return; if ((pStartSubIndex < 0) || (pStartSubIndex >= msg_area.grp_list[pGrpIndex].sub_list.length)) { return; } if ((pStartScreenRow < 1) || (pStartScreenRow > console.screen_rows)) return; if ((pEndScreenRow < 1) || (pEndScreenRow > console.screen_rows)) return; // If pStartScreenRow is greather than pEndScreenRow, then swap them. if (pStartScreenRow > pEndScreenRow) { var temp = pStartScreenRow; pStartScreenRow = pEndScreenRow; pEndScreenRow = temp; } // Calculate the ending index to use for the sub-board array. var endIndex = pStartSubIndex + (pEndScreenRow-pStartScreenRow); if (endIndex >= msg_area.grp_list[pGrpIndex].sub_list.length) endIndex = msg_area.grp_list[pGrpIndex].sub_list.length - 1; var onePastEndIndex = endIndex + 1; // Clear the screen and go to the specified screen row. if (pClearScreenFirst) console.clear("\1n"); console.gotoxy(1, pStartScreenRow); // Start listing the sub-boards. var subIndex = pStartSubIndex; for (; subIndex < onePastEndIndex; ++subIndex) { this.WriteMsgSubBoardLine(pGrpIndex, subIndex, false); if (subIndex < endIndex) console.crlf(); } // If pBlankToEndRow is true and we're not at the end row yet, then // write blank lines to the end row. if (pBlankToEndRow) { var screenRow = pStartScreenRow + (endIndex - pStartSubIndex) + 1; if (screenRow <= pEndScreenRow) { for (; screenRow <= pEndScreenRow; ++screenRow) { console.gotoxy(1, screenRow); console.clearline("\1n"); } } } } // For the DigDistMsgReader class: Writes a message sub-board information line for // the message area chooser functionality. // // Parameters: // pGrpIndex: The index of the message group (assumed to be valid) // pSubIndex: The index of the sub-board within the message group to write (assumed to be valid) // pHighlight: Boolean - Whether or not to write the line highlighted. function DigDistMsgReader_WriteMsgSubBrdLine(pGrpIndex, pSubIndex, pHighlight) { console.print("\1n"); // Write the highlight background color if pHighlight is true. if (pHighlight) console.print(this.colors.areaChooserMsgAreaBkgHighlightColor); // Determine if pGrpIndex and pSubIndex specify the user's // currently-selected group and sub-board. var currentSub = false; if ((typeof(bbs.curgrp) == "number") && (typeof(bbs.cursub) == "number")) currentSub = ((pGrpIndex == msg_area.sub[this.subBoardCode].grp_index) && (pSubIndex == msg_area.sub[this.subBoardCode].index)); // Open the current sub-board with the msgBase object (so that we can get // the date & time of the last imporeted message). var msgBase = new MsgBase(msg_area.grp_list[pGrpIndex].sub_list[pSubIndex].code); if (msgBase.open()) { var newestDate = new Object(); // For storing the date of the newest post // Get the date & time when the last message was imported. if (msgBase.total_msgs > 0) { msgHeader = msgBase.get_msg_header(true, msgBase.total_msgs-1, true); // Construct the date & time strings of the latest post if (this.msgAreaList_lastImportedMsg_showImportTime) { newestDate.date = strftime("%Y-%m-%d", msgHeader.when_imported_time); newestDate.time = strftime("%H:%M:%S", msgHeader.when_imported_time); } else { newestDate.date = strftime("%Y-%m-%d", msgHeader.when_written_time); newestDate.time = strftime("%H:%M:%S", msgHeader.when_written_time); } } else newestDate.date = newestDate.time = ""; // Print the sub-board information line. console.print(currentSub ? this.colors.areaChooserMsgAreaMarkColor + "*" : " "); printf((pHighlight ? this.subBoardListPrintfInfo[pGrpIndex].highlightPrintfStr : this.subBoardListPrintfInfo[pGrpIndex].printfStr), +(pSubIndex+1), msg_area.grp_list[pGrpIndex].sub_list[pSubIndex].description.substr(0, this.subBoardListPrintfInfo[pGrpIndex].nameLen), msgBase.total_msgs, newestDate.date, newestDate.time); msgBase.close(); delete msgBase; } } /////////////////////////////////////////////// // Other functions for the msg. area chooser // /////////////////////////////////////////////// // For the DigDistMsgReader class: Shows the help screen // // Parameters: // pLightbar: Boolean - Whether or not to show lightbar help. If // false, then this function will show regular help. // pClearScreen: Boolean - Whether or not to clear the screen first function DigDistMsgReader_showChooseMsgAreaHelpScreen(pLightbar, pClearScreen) { if (pClearScreen && console.term_supports(USER_ANSI)) console.clear("\1n"); else console.print("\1n"); DisplayProgramInfo(); console.crlf(); console.print("\1n\1c\1hMessage area (sub-board) chooser"); console.crlf(); console.print("\1k��������������������������������\1n"); console.crlf(); console.print("\1cFirst, a listing of message groups is displayed. One can be chosen by typing"); console.crlf(); console.print("its number. Then, a listing of sub-boards within that message group will be"); console.crlf(); console.print("shown, and one can be chosen by typing its number."); console.crlf(); if (pLightbar) { console.crlf(); console.print("\1n\1cThe lightbar interface also allows up & down navigation through the lists:"); console.crlf(); console.print("\1k\1h��������������������������������������������������������������������������"); console.crlf(); console.print("\1n\1c\1hUp arrow\1n\1c: Move the cursor up one line"); console.crlf(); console.print("\1hDown arrow\1n\1c: Move the cursor down one line"); console.crlf(); console.print("\1hENTER\1n\1c: Select the current group/sub-board"); console.crlf(); console.print("\1hHOME\1n\1c: Go to the first item on the screen"); console.crlf(); console.print("\1hEND\1n\1c: Go to the last item on the screen"); console.crlf(); console.print("\1hF\1n\1c: Go to the first page"); console.crlf(); console.print("\1hL\1n\1c: Go to the last page"); console.crlf(); } console.crlf(); console.print("Additional keyboard commands:"); console.crlf(); console.print("\1k\1h�����������������������������"); console.crlf(); console.print("\1n\1c\1h?\1n\1c: Show this help screen"); console.crlf(); console.print("\1hQ\1n\1c: Quit"); console.crlf(); } // Builds sub-board printf format information for a message group. // The widths of the description & # messages columns are calculated // based on the greatest number of messages in a sub-board for the // message group. // // Parameters: // pGrpIndex: The index of the message group function DigDistMsgReader_BuildSubBoardPrintfInfoForGrp(pGrpIndex) { // If the array of sub-board printf strings doesn't contain the printf // strings for this message group, then figure out the largest number // of messages in the message group and add the printf strings. if (typeof(this.subBoardListPrintfInfo[pGrpIndex]) == "undefined") { var greatestNumMsgs = getGreatestNumMsgs(pGrpIndex); this.subBoardListPrintfInfo[pGrpIndex] = new Object(); this.subBoardListPrintfInfo[pGrpIndex].numMsgsLen = greatestNumMsgs.toString().length; // Sub-board name length: With a # items length of 4, this should be // 47 for an 80-column display. this.subBoardListPrintfInfo[pGrpIndex].nameLen = console.screen_columns - this.areaNumLen - this.subBoardListPrintfInfo[pGrpIndex].numMsgsLen - this.dateLen - this.timeLen - 7; // Create the printf strings this.subBoardListPrintfInfo[pGrpIndex].printfStr = " " + this.colors.areaChooserMsgAreaNumColor + "%" + this.areaNumLen + "d " + this.colors.areaChooserMsgAreaDescColor + "%-" + this.subBoardListPrintfInfo[pGrpIndex].nameLen + "s " + this.colors.areaChooserMsgAreaNumItemsColor + "%" + this.subBoardListPrintfInfo[pGrpIndex].numMsgsLen + "d " + this.colors.areaChooserMsgAreaLatestDateColor + "%" + this.dateLen + "s " + this.colors.areaChooserMsgAreaLatestTimeColor + "%" + this.timeLen + "s"; this.subBoardListPrintfInfo[pGrpIndex].highlightPrintfStr = "\1n" + this.colors.areaChooserMsgAreaBkgHighlightColor + " " + "\1n" + this.colors.areaChooserMsgAreaBkgHighlightColor + this.colors.areaChooserMsgAreaNumHighlightColor + "%" + this.areaNumLen + "d \1n" + this.colors.areaChooserMsgAreaBkgHighlightColor + this.colors.areaChooserMsgAreaDescHighlightColor + "%-" + this.subBoardListPrintfInfo[pGrpIndex].nameLen + "s \1n" + this.colors.areaChooserMsgAreaBkgHighlightColor + this.colors.areaChooserMsgAreaNumItemsHighlightColor + "%" + this.subBoardListPrintfInfo[pGrpIndex].numMsgsLen + "d \1n" + this.colors.areaChooserMsgAreaBkgHighlightColor + this.colors.areaChooserMsgAreaDateHighlightColor + "%" + this.dateLen + "s \1n" + this.colors.areaChooserMsgAreaBkgHighlightColor + this.colors.areaChooserMsgAreaTimeHighlightColor + "%" + this.timeLen + "s\1n"; } } // Returns an array of strings containing extended message header information, // such as the kludge lines (for FidoNet-style networks), etc. // For each kludge line, there will be a label string for the info line, the // info line itself (wrapped to fit the message area width), then a blank // line (except for the last kludge line). The info lines that this method // retrieves will only be retrieved if they exist in the given message header. // // Parameters: // pMsgHdr: A message header // pKludgeOnly: Boolean - Whether or not to only get the kludge lines. If false, // then all header fields will be retrieved. // // Return value: An array of strings containing the extended message header information function DigDistMsgReader_GetExtdMsgHdrInfo(pMsgHdr, pKludgeOnly) { // If pMsgHdr is not valid, then just return an empty array. if (typeof(pMsgHdr) != "object") return new Array(); var msgHdrInfoLines = new Array(); var hdrInfoLineFields = new Array(); var kludgeOnly = (typeof(pKludgeOnly) == "boolean" ? pKludgeOnly : false); if (kludgeOnly) { hdrInfoLineFields.push({ field: "ftn_msgid", label: "MSG ID:" }); hdrInfoLineFields.push({ field: "X-FTN-MSGID", label: "MSG ID:" }); hdrInfoLineFields.push({ field: "ftn_reply", label: "Reply ID:" }); hdrInfoLineFields.push({ field: "X-FTN-REPLY", label: "Reply ID:" }); hdrInfoLineFields.push({ field: "ftn_area", label: "Area tag:" }); hdrInfoLineFields.push({ field: "X-FTN-AREA", label: "Area tag:" }); hdrInfoLineFields.push({ field: "ftn_flags", label: "Flags:" }); hdrInfoLineFields.push({ field: "ftn_pid", label: "Program ID:" }); hdrInfoLineFields.push({ field: "ftn_tid", label: "Tosser ID:" }); hdrInfoLineFields.push({ field: "X-FTN-TID", label: "Tosser ID:" }); hdrInfoLineFields.push({ field: "X-FTN-Kludge", label: "Kludge:" }); hdrInfoLineFields.push({ field: "X-FTN-SEEN-BY", label: "Seen-By:" }); hdrInfoLineFields.push({ field: "X-FTN-PATH", label: "Path:" }); hdrInfoLineFields.push({ field: "when_written_time", label: "When written time:" }); hdrInfoLineFields.push({ field: "when_imported_time", label: "When imported time:" }); } // This function returns the number of non-blank lines in a header info array. // // Return value: An object with the following properties: // numNonBlankLines: The number of non-blank lines in the array // firstNonBlankLineIdx: The index of the first non-blank line // lastNonBlankLineIdx: The index of the last non-blank line function findHdrFieldDataArrayNonBlankLines(pHdrArray) { var retObj = new Object(); retObj.numNonBlankLines = 0; retObj.firstNonBlankLineIdx = -1; retObj.lastNonBlankLineIdx = -1; for (var lineIdx = 0; lineIdx < pHdrArray.length; ++lineIdx) { if (pHdrArray[lineIdx].length > 0) { ++retObj.numNonBlankLines; if (retObj.firstNonBlankLineIdx == -1) retObj.firstNonBlankLineIdx = lineIdx; retObj.lastNonBlankLineIdx = lineIdx; } } return retObj; } // Counts the number of elements in a header field_list array with the // same type, starting at a given index. // // Parameters: // pFieldList: The field_list array in a message header // pStartIdx: The index of the starting element to start counting at // // Return value: The number of elements with the same type as the start index element function fieldListCountSameTypes(pFieldList, pStartIdx) { if (typeof(pFieldList) == "undefined") return 0; if (typeof(pStartIdx) != "number") return 0; if ((pStartIdx < 0) || (pStartIdx >= pFieldList.length)) return 0; var itemCount = 1; for (var idx = pStartIdx+1; idx < pFieldList.length; ++idx) { if (pFieldList[idx].type == pFieldList[pStartIdx].type) ++itemCount; else break; } return itemCount; } // Get the header fields var addTheField = true; var customFieldLabel = ""; var propCounter = 1; for (var prop in pMsgHdr) { addTheField = true; customFieldLabel = ""; if (prop == "field_list") { var hdrFieldLabel = ""; var lastHdrFieldLabel = null; var addBlankLineAfterIdx = -1; for (var fieldI = 0; fieldI < pMsgHdr.field_list.length; ++fieldI) { // TODO: Some field types can be in the array multiple times but only // the last is valid. For those, only get the last one: // 32 (Reply To) // 33 (Reply To agent) // 34 (Reply To net type) // 35 (Reply To net address) // 36 (Reply To extended) // 37 (Reply To position) // 38 (Reply To Organization) hdrFieldLabel = "\1n" + this.colors.hdrLineLabelColor + msgHdrFieldListTypeToLabel(pMsgHdr.field_list[fieldI].type) + "\1n"; var infoLineWrapped = pMsgHdr.field_list[fieldI].data; var infoLineWrappedArray = lfexpand(infoLineWrapped).split("\r\n"); var hdrArrayNonBlankLines = findHdrFieldDataArrayNonBlankLines(infoLineWrappedArray); if (hdrArrayNonBlankLines.numNonBlankLines > 0) { if (hdrArrayNonBlankLines.numNonBlankLines == 1) { var addExtraBlankLineAtEnd = false; var hdrItem = "\1n" + this.colors.hdrLineValueColor + infoLineWrappedArray[hdrArrayNonBlankLines.firstNonBlankLineIdx] + "\1n"; // If the header field label is different, then add it to the // header info lines if ((lastHdrFieldLabel == null) || (hdrFieldLabel != lastHdrFieldLabel)) { var numFieldItemsWithSameType = fieldListCountSameTypes(pMsgHdr.field_list, fieldI); if (numFieldItemsWithSameType > 1) { msgHdrInfoLines.push(""); msgHdrInfoLines.push(hdrFieldLabel); addExtraBlankLineAtEnd = true; addBlankLineAfterIdx = fieldI + numFieldItemsWithSameType - 1; } else { hdrItem = hdrFieldLabel + " " + hdrItem; numFieldItemsWithSameType = -1; } } if (strip_ctrl(hdrItem).length < this.msgAreaWidth) msgHdrInfoLines.push(hdrItem); else { // If the header field label is different, then add a blank line // to the header info lines if ((lastHdrFieldLabel == null) || (hdrFieldLabel != lastHdrFieldLabel)) msgHdrInfoLines.push(""); msgHdrInfoLines.push(hdrFieldLabel); msgHdrInfoLines.push(infoLineWrappedArray[hdrArrayNonBlankLines.firstNonBlankLineIdx]); if ((lastHdrFieldLabel == null) || (hdrFieldLabel != lastHdrFieldLabel)) msgHdrInfoLines.push(""); } } else { // If the header field label is different, then add it to the // header info lines if ((lastHdrFieldLabel == null) || (hdrFieldLabel != lastHdrFieldLabel)) { msgHdrInfoLines.push(""); msgHdrInfoLines.push(hdrFieldLabel + "!"); } var infoLineWrapped = pMsgHdr.field_list[fieldI].data; var infoLineWrappedArray = lfexpand(infoLineWrapped).split("\r\n"); for (var lineIdx = 0; lineIdx < infoLineWrappedArray.length; ++lineIdx) { if (infoLineWrappedArray[lineIdx].length > 0) msgHdrInfoLines.push(infoLineWrappedArray[lineIdx]); } // If the header field label is different, then add a blank line to the // header info lines if ((lastHdrFieldLabel == null) || (hdrFieldLabel != lastHdrFieldLabel)) msgHdrInfoLines.push(""); } if (addBlankLineAfterIdx == fieldI) msgHdrInfoLines.push(""); } lastHdrFieldLabel = hdrFieldLabel; } } else { // See if we should add this field addTheField = true; if (hdrInfoLineFields.length > 0) { addTheField = false; for (var infoLineFieldIdx = 0; infoLineFieldIdx < hdrInfoLineFields.length; ++infoLineFieldIdx) { if (prop == hdrInfoLineFields[infoLineFieldIdx].field) { addTheField = true; customFieldLabel = hdrInfoLineFields[infoLineFieldIdx].label; break; } } } if (!addTheField) continue; var propLabel = ""; if (customFieldLabel.length > 0) propLabel = "\1n" + this.colors.hdrLineLabelColor + customFieldLabel + "\1n"; else { // Remove underscores from the property for the label propLabel = prop.replace(/_/g, " "); // Apply good-looking capitalization to the property label if ((propLabel == "id") || (propLabel == "ftn tid")) propLabel = propLabel.toUpperCase(); else if (propLabel == "ftn area") propLabel = "FTN Area"; else if (propLabel == "ftn pid") propLabel = "Program ID"; else if (propLabel == "thread id") propLabel = "Thread ID"; else if (propLabel == "attr") propLabel = "Attributes"; else if (propLabel == "auxattr") propLabel = "Auxiliary attributes"; else if (propLabel == "netattr") propLabel = "Network attributes"; else propLabel = capitalizeFirstChar(propLabel); // Add the label color and trailing colon to the label text propLabel = "\1n" + this.colors.hdrLineLabelColor + propLabel + ":\1n"; } var infoLineWrapped = word_wrap(pMsgHdr[prop], this.msgAreaWidth); var infoLineWrappedArray = lfexpand(infoLineWrapped).split("\r\n"); var itemValue = ""; for (var lineIdx = 0; lineIdx < infoLineWrappedArray.length; ++lineIdx) { if (infoLineWrappedArray[lineIdx].length > 0) { // Set itemValue to the value that should be displayed if (prop == "when_written_time") //itemValue = system.timestr(pMsgHdr.when_written_time); itemValue = system.timestr(pMsgHdr.when_written_time) + " " + system.zonestr(pMsgHdr.when_written_zone); else if (prop == "when_imported_time") //itemValue = system.timestr(pMsgHdr.when_imported_time); itemValue = system.timestr(pMsgHdr.when_imported_time) + " " + system.zonestr(pMsgHdr.when_imported_zone); else if ((prop == "when_imported_zone") || (prop == "when_written_zone")) itemValue = system.zonestr(pMsgHdr[prop]); else if (prop == "attr") itemValue = makeMainMsgAttrStr(pMsgHdr[prop], "None"); else if (prop == "auxattr") itemValue = makeAuxMsgAttrStr(pMsgHdr[prop], "None"); else if (prop == "netattr") itemValue = makeNetMsgAttrStr(pMsgHdr[prop], "None"); else itemValue = infoLineWrappedArray[lineIdx]; // Add the value color to the value text itemValue = "\1n" + this.colors.hdrLineValueColor + itemValue + "\1n"; var hdrItem = propLabel + " " + itemValue; if (strip_ctrl(hdrItem).length < this.msgAreaWidth) msgHdrInfoLines.push(hdrItem); else { // If this isn't the first header property, then add an empty // line to the info lines array for spacing if (propCounter > 1) msgHdrInfoLines.push(""); msgHdrInfoLines.push(propLabel); // In case the item value is too long to fit into the // message reading area, split it and add all the split lines. var itemValueWrapped = word_wrap(itemValue, this.msgAreaWidth); var itemValueWrappedArray = lfexpand(itemValueWrapped).split("\r\n"); for (var lineIdx = 0; lineIdx < itemValueWrappedArray.length; ++lineIdx) { if (itemValueWrappedArray[lineIdx].length > 0) msgHdrInfoLines.push(itemValueWrappedArray[lineIdx]); } // If this isn't the first header property, then add an empty // line to the info lines array for spacing if (propCounter > 1) msgHdrInfoLines.push(""); } } } } ++propCounter; } // If some info lines were added, then insert a header line & blank line to // the beginning of the array, and remove the last empty line from the array. if (msgHdrInfoLines.length > 0) { if (kludgeOnly) { msgHdrInfoLines.splice(0, 0, "\1n\1c\1hMessage Information/Kludge Lines\1n"); msgHdrInfoLines.splice(1, 0, "\1n\1g\1h--------------------------------\1n"); } else { msgHdrInfoLines.splice(0, 0, "\1n\1c\1hMessage Headers\1n"); msgHdrInfoLines.splice(1, 0, "\1n\1g\1h---------------\1n"); } if (msgHdrInfoLines[msgHdrInfoLines.length-1].length == 0) msgHdrInfoLines.pop(); } return msgHdrInfoLines; } // For the DigDistMsgReader class: Gets & prepares message information for // the enhanced reader. // // Parameters: // pMsgHdr: The message header // pWordWrap: Boolean - Whether or not to word-wrap the message to fit into the // display area. This is optional and defaults to true. This should // be true for normal use; the only time this should be false is when // saving the message to a file. // pDetermineAttachments: Boolean - Whether or not to parse the message text to // get attachments from it. This is optional and defaults // to true. If false, then the message text will be left // intact with any base64-encoded attachments that may be // in the message text (for multi-part MIME messages). // pGetB64Data: Boolean - Whether or not to get the Base64-encoded data for // base64-encoded attachments (i.e., in multi-part MIME emails). // This is optional and defaults to true. This is only used when // pDetermineAttachments is true. // pMsgBody: Optional - A string containing the message body. If this is not included // or is not a string, then this method will retrieve the message body. // pMsgHasANSICodes: Optional boolean - If the caller already knows whether the // message text has ANSI codes, the caller can pass this parameter. // // Return value: An object with the following properties: // msgText: The unaltered message text // messageLines: An array containing the message lines, wrapped to // the message area width // topMsgLineIdxForLastPage: The top message line index for the last page // msgFractionShown: The fraction of the message shown // numSolidScrollBlocks: The number of solid scrollbar blocks // numNonSolidScrollBlocks: The number of non-solid scrollbar blocks // solidBlockStartRow: The starting row on the screen for the scrollbar blocks // attachments: An array of the attached filenames (as strings) // displayFrame: A Frame object for displaying the message with // a scrollable interface. Used when the message // contains ANSI, for instance. If this object is // null, then the reader should use its own scrolling // interface. Also, this will only be available if // the BBS machine has frame.js in sbbs/exec/load. // displayFrameScrollbar: A ScrollBar object to work with displayFrame. // If scrollbar.js is not available on the BBS machine, // this will be null. function DigDistMsgReader_GetMsgInfoForEnhancedReader(pMsgHdr, pWordWrap, pDetermineAttachments, pGetB64Data, pMsgBody, pMsgHasANSICodes) { var retObj = new Object(); var determineAttachments = true; if (typeof(pDetermineAttachments) == "boolean") determineAttachments = pDetermineAttachments; var getB64Data = true; if (typeof(pGetB64Data) == "boolean") getB64Data = pGetB64Data; var msgBody = (typeof(pMsgBody) == "string" ? pMsgBody : this.msgbase.get_msg_body(true, pMsgHdr.offset)); if (determineAttachments) { var msgInfo = determineMsgAttachments(pMsgHdr, msgBody, getB64Data); retObj.msgText = msgInfo.msgText; retObj.attachments = msgInfo.attachments; } else { retObj.msgText = msgBody; retObj.attachments = []; } var msgTextAltered = retObj.msgText; // Will alter the message text, but not yet // Only interpret @-codes if the user is reading personal email. There // are many @-codes that do some action such as move the cursor, execute a // script, etc., and I don't want users on message networks to do anything // malicious to users on other BBSes. if (this.readingPersonalEmail) msgTextAltered = replaceAtCodesInStr(msgTextAltered, pMsgHdr); // Or this.ParseMsgAtCodes(msgTextAltered, pMsgHdr) to replace only some @ codes msgTextAltered = msgTextAltered.replace(/\t/g, this.tabReplacementText); // Convert other BBS color codes to Synchronet attribute codes if the settings // to do so are enabled. if ((system.settings & SYS_RENEGADE) == SYS_RENEGADE) msgTextAltered = renegadeAttrsToSyncAttrs(msgTextAltered); if ((system.settings & SYS_WWIV) == SYS_WWIV) msgTextAltered = WWIVAttrsToSyncAttrs(msgTextAltered); if ((system.settings & SYS_CELERITY) == SYS_CELERITY) msgTextAltered = celerityAttrsToSyncAttrs(msgTextAltered); if ((system.settings & SYS_PCBOARD) == SYS_PCBOARD) msgTextAltered = PCBoardAttrsToSyncAttrs(msgTextAltered); if ((system.settings & SYS_WILDCAT) == SYS_WILDCAT) msgTextAltered = wildcatAttrsToSyncAttrs(msgTextAltered); // If the message contains ANSI codes, then if frame.js is available and // the user's terminal support ANSI, set up a Frame object for reading the // message with a scrollable interface. retObj.displayFrame = null; retObj.displayFrameScrollbar = null; var msgHasANSICodes = (typeof(pMsgHasANSICodes) == "boolean" ? pMsgHasANSICodes : textHasANSICodes(retObj.msgText)); if (msgHasANSICodes) { if (gFrameJSAvailable && console.term_supports(USER_ANSI)) { // Write the message to a file in a temporary directory, // have the frame object read it, then delete the temporary // directory. var ANSITempDirExists = false; var readerTmpOutputDir = backslash(system.node_dir + "DDMsgReaderANSIMsgTemp"); if (!file_exists(readerTmpOutputDir)) ANSITempDirExists = mkdir(readerTmpOutputDir); if (ANSITempDirExists) { var tmpANSIFileName = backslash(readerTmpOutputDir) + "tmpMsg.ans"; var tmpANSIFile = new File(tmpANSIFileName); if (tmpANSIFile.open("w")) { var tmpANSIFileWritten = tmpANSIFile.write(msgTextAltered); tmpANSIFile.close(); // If the file was successfully written, then create the // Frame object and have it read the file. if (tmpANSIFileWritten) { // Create the Frame object and // TODO: Should this use 79 or 80 columns? retObj.displayFrame = new Frame(1, // x: Horizontal coordinate of top left this.enhMsgHeaderLines.length+1, // y: Vertical coordinate of top left 80, // Width // Height: Allow for the message header // and enhanced reader help line console.screen_rows-this.enhMsgHeaderLines.length-1, BG_BLACK); retObj.displayFrame.v_scroll = true; retObj.displayFrame.h_scroll = false; retObj.displayFrame.scrollbars = true; // Load the message file into the Frame object retObj.displayFrame.load(tmpANSIFileName); // If scrollbar.js is available, then set up a vertical // scrollbar for the Frame object if (gScrollbarJSAvailable) retObj.displayFrameScrollbar = new ScrollBar(retObj.displayFrame, {bg: BG_BLACK, fg: LIGHTGRAY, orientation: "vertical", autohide: false}); } } // Cleanup: Remove the temporary directory deltree(readerTmpOutputDir); } } else { // frame.js is not available. Just convert the ANSI to // Synchronet attributes. It might not look the best, but // at least we can convert some of the ANSI codes. msgTextAltered = ANSIAttrsToSyncAttrs(msgTextAltered); } } var wordWrapTheMsgText = true; if (typeof(pWordWrap) == "boolean") wordWrapTheMsgText = pWordWrap; if (wordWrapTheMsgText) { // Wrap the text to fit into the available message area. // Note: In Synchronet 3.15 (and some beta builds of 3.16), there seemed to // be a bug in the word_wrap() function where the word wrap length in Linux // was one less than Windows, so if the BBS is running 3.15 or earlier of // Synchronet, add 1 to the word wrap length if running in Linux. var textWrapLen = this.msgAreaWidth; if (system.version_num <= 31500) textWrapLen = gRunningInWindows ? this.msgAreaWidth : this.msgAreaWidth + 1; var msgTextWrapped = word_wrap(msgTextAltered, textWrapLen); retObj.messageLines = lfexpand(msgTextWrapped).split("\r\n"); // Go through the message lines and trim them to ensure they'll easily fit // in the message display area without having to trim them later. (Note: // this is okay to do since we're only using messageLines to display the // message on the screen; messageLines isn't used for quoting/replying). for (var msgLnIdx = 0; msgLnIdx < retObj.messageLines.length; ++msgLnIdx) retObj.messageLines[msgLnIdx] = shortenStrWithAttrCodes(retObj.messageLines[msgLnIdx], this.msgAreaWidth); // Set up some variables for displaying the message retObj.topMsgLineIdxForLastPage = retObj.messageLines.length - this.msgAreaHeight; if (retObj.topMsgLineIdxForLastPage < 0) retObj.topMsgLineIdxForLastPage = 0; // Variables for the scrollbar to show the fraction of the message shown retObj.msgFractionShown = this.msgAreaHeight / retObj.messageLines.length; if (retObj.msgFractionShown > 1) retObj.msgFractionShown = 1.0; retObj.numSolidScrollBlocks = Math.floor(this.msgAreaHeight * retObj.msgFractionShown); if (retObj.numSolidScrollBlocks == 0) retObj.numSolidScrollBlocks = 1; } else { retObj.messageLines = []; retObj.messageLines.push(msgTextAltered); } retObj.numNonSolidScrollBlocks = this.msgAreaHeight - retObj.numSolidScrollBlocks; retObj.solidBlockStartRow = this.msgAreaTop; return retObj; } // For the DigDistMsgReader class: Returns the index of the last read message in // the current message area. If reading personal email, this will look at the // search results. Otherwise, this will use the sub-board's last_read pointer. // If there is no last read message or if there is a problem getting the last read // message index, this method will return -1. // // Parameters: // pMailStartFromFirst: Optional boolean - Whether or not to start from the // first message (rather than from the last message) if // reading personal email. Will stop looking at the first // unread message. Defaults to false. // // Return value: The index of the last read message in the current message area function DigDistMsgReader_GetLastReadMsgIdx(pMailStartFromFirst) { var msgIndex = -1; if (this.readingPersonalEmail) { if (this.SearchingAndResultObjsDefinedForCurSub()) { var startFromFirst = (typeof(pMailStartFromFirst) == "boolean" ? pMailStartFromFirst : false); if (startFromFirst) { for (var idx = 0; idx < this.msgSearchHdrs[this.subBoardCode].indexed.length; ++idx) { if ((this.msgSearchHdrs[this.subBoardCode].indexed[idx].attr & MSG_READ) == MSG_READ) msgIndex = idx; else break; } } else { for (var idx = this.msgSearchHdrs[this.subBoardCode].indexed.length-1; idx >= 0; --idx) { if ((this.msgSearchHdrs[this.subBoardCode].indexed[idx].attr & MSG_READ) == MSG_READ) { msgIndex = idx; break; } } } // Sanity checking for msgIndex (note: this function should return -1 if // there is no last read message). if (msgIndex >= this.msgSearchHdrs[this.subBoardCode].indexed.length) msgIndex = this.msgSearchHdrs[this.subBoardCode].indexed.length - 1; } } else { msgIndex = this.AbsMsgNumToIdx(msg_area.sub[this.subBoardCode].last_read); // Sanity checking for msgIndex (note: this function should return -1 if // there is no last read message). if ((this.msgbase != null) && this.msgbase.is_open) { //if (msgIndex >= this.msgbase.total_msgs) // msgIndex = this.msgbase.total_msgs - 1; // TODO: Is this code right? Modified 3/24/2015 to replace // the above 2 commented lines. if ((msgIndex < 0) || (msgIndex >= this.msgbase.total_msgs)) { // Look for the first message not marked as deleted var nonDeletedMsgIdx = this.FindNextNonDeletedMsgIdx(0, true); // If a non-deleted message was found, then set the last read // pointer to it. if (nonDeletedMsgIdx > -1) { var newLastRead = this.IdxToAbsMsgNum(nonDeletedMsgIdx); if (newLastRead > -1) msg_area.sub[this.subBoardCode].last_read = newLastRead; else msg_area.sub[this.subBoardCode].last_read = 0; } else msg_area.sub[this.subBoardCode].last_read = 0; } } } return msgIndex; } // For the DigDistMsgReader class: Returns the index of the message pointed to // by the scan pointer in the current sub-board. If reading personal email or // if the message base isn't open, this will return 0. If the scan pointer is // 0 or if the messagebase is open and the scan pointer is invalid, this will // return -1. function DigDistMsgReader_GetScanPtrMsgIdx() { if (this.readingPersonalEmail) return 0; if (msg_area.sub[this.subBoardCode].scan_ptr == 0) return -1; if ((this.msgbase == null) || (!this.msgbase.is_open)) return 0; var msgIdx = this.AbsMsgNumToIdx(msg_area.sub[this.subBoardCode].scan_ptr); // Sanity checking for msgIdx if ((msgIdx < 0) || (msgIdx >= this.msgbase.total_msgs)) { msgIdx = -1; // Look for the first message not marked as deleted var nonDeletedMsgIdx = this.FindNextNonDeletedMsgIdx(0, true); // If a non-deleted message was found, then set the scan pointer to it. if (nonDeletedMsgIdx > -1) { var newLastRead = this.IdxToAbsMsgNum(nonDeletedMsgIdx); if (newLastRead > -1) msg_area.sub[this.subBoardCode].scan_ptr = newLastRead; else msg_area.sub[this.subBoardCode].scan_ptr = 0; } else msg_area.sub[this.subBoardCode].scan_ptr = 0; } return msgIdx; } // For the DigDistMsgReader class: Returns whether there is a search specified // (according to this.searchType) and the search result objects are defined for // the current sub-board (as specified by this.subBoardCode). // // Return value: Boolean - Whether or not there is a search specified and the // search result objects are defined for the current sub-board // (as specified by this.subBoardCode). function DigDistMsgReader_SearchingAndResultObjsDefinedForCurSub() { return (this.SearchTypePopulatesSearchResults() && (this.msgSearchHdrs != null) && this.msgSearchHdrs.hasOwnProperty(this.subBoardCode) && (typeof(this.msgSearchHdrs[this.subBoardCode]) == "object") && (typeof(this.msgSearchHdrs[this.subBoardCode].indexed) != "undefined")); } // For the DigDistMsgReader class: Removes a message header from the search // results array for the current sub-board. // // Parameters: // pMsgIdx: The index of the message header to remove (in the indexed messages, // not necessarily the actual message offset in the messagebase) function DigDistMsgReader_RemoveFromSearchResults(pMsgIdx) { if (typeof(pMsgIdx) != "number") return; if ((typeof(this.msgSearchHdrs) == "object") && this.msgSearchHdrs.hasOwnProperty(this.subBoardCode) && (typeof(this.msgSearchHdrs[this.subBoardCode].indexed) != "undefined")) { if ((pMsgIdx >= 0) && (pMsgIdx < this.msgSearchHdrs[this.subBoardCode].indexed.length)) this.msgSearchHdrs[this.subBoardCode].indexed.splice(pMsgIdx, 1); } } // For the DigDistMsgReader class: Looks for the next message in the thread of // a given message (by its header). // // Paramters: // pMsgHdr: A message header object - The next message in the thread will be // searched starting from this message // pThreadType: The type of threading to use. Can be THREAD_BY_ID, THREAD_BY_TITLE, // THREAD_BY_AUTHOR, or THREAD_BY_TO_USER. // pPositionCursorForStatus: Optional boolean - Whether or not to move the cursor // to the bottom row before outputting status messages. // Defaults to false. // // Return value: The offset (index) of the next message thread, or -1 if none // was found. function DigDistMsgReader_FindThreadNextOffset(pMsgHdr, pThreadType, pPositionCursorForStatus) { if ((this.msgbase == null) || (!this.msgbase.is_open)) return -1; if ((pMsgHdr == null) || (typeof(pMsgHdr) != "object")) return -1; var newMsgOffset = -1; switch (pThreadType) { case THREAD_BY_ID: default: // The thread_id field was introduced in Synchronet 3.16. So, if // the Synchronet version is 3.16 or higher and the message header // has a thread_id field, then look for the next message with the // same thread ID. If the Synchronet version is below 3.16 or there // is no thread ID, then fall back to using the header's thread_next, // if it exists. if ((system.version_num >= 31600) && (typeof(pMsgHdr.thread_id) == "number")) { // Look for the next message with the same thread ID. // Write "Searching.." in case searching takes a while. console.print("\1n"); if (pPositionCursorForStatus) { console.gotoxy(1, console.screen_rows); console.cleartoeol(); console.gotoxy(this.msgAreaLeft, console.screen_rows); } console.print("\1h\1ySearching\1i...\1n"); // Look for the next message in the thread var nextMsgOffset = -1; var numOfMessages = this.NumMessages(); if (pMsgHdr.offset < numOfMessages - 1) { var nextMsgHdr; for (var messageIdx = pMsgHdr.offset+1; (messageIdx < numOfMessages) && (nextMsgOffset == -1); ++messageIdx) { nextMsgHdr = this.GetMsgHdrByIdx(messageIdx); if (((nextMsgHdr.attr & MSG_DELETE) == 0) && (typeof(nextMsgHdr.thread_id) == "number") && (nextMsgHdr.thread_id == pMsgHdr.thread_id)) nextMsgOffset = nextMsgHdr.offset; } } if (nextMsgOffset > -1) newMsgOffset = nextMsgOffset; } // Fall back to thread_next if the Synchronet version is below 3.16 or there is // no thread_id field in the header else if ((typeof(pMsgHdr.thread_next) == "number") && (pMsgHdr.thread_next > 0)) newMsgOffset = this.AbsMsgNumToIdx(pMsgHdr.thread_next); break; case THREAD_BY_TITLE: case THREAD_BY_AUTHOR: case THREAD_BY_TO_USER: // Title (subject) searching will look for the subject anywhere in the // other messages' subjects (not a fully exact subject match), so if // the message subject is blank, we won't want to do the search. var doSearch = true; if ((pThreadType == THREAD_BY_TITLE) && (pMsgHdr.subject.length == 0)) doSearch = false; if (doSearch) { var subjUppercase = ""; var fromNameUppercase = ""; var toNameUppercase = ""; // Set up a message header matching function, depending on // which field of the header we want to match var msgHdrMatch; if (pThreadType == THREAD_BY_TITLE) { subjUppercase = pMsgHdr.subject.toUpperCase(); // Remove any leading instances of "RE:" from the subject while (/^RE:/.test(subjUppercase)) subjUppercase = subjUppercase.substr(3); while (/^RE: /.test(subjUppercase)) subjUppercase = subjUppercase.substr(4); // Remove any leading & trailing whitespace from the subject subjUppercase = trimSpaces(subjUppercase, true, true, true); msgHdrMatch = function(pMsgHdr) { return (((pMsgHdr.attr & MSG_DELETE) == 0) && (pMsgHdr.subject.toUpperCase().indexOf(subjUppercase, 0) > -1)); }; } else if (pThreadType == THREAD_BY_AUTHOR) { fromNameUppercase = pMsgHdr.from.toUpperCase(); msgHdrMatch = function(pMsgHdr) { return (((pMsgHdr.attr & MSG_DELETE) == 0) && (pMsgHdr.from.toUpperCase() == fromNameUppercase)); }; } else if (pThreadType == THREAD_BY_TO_USER) { toNameUppercase = pMsgHdr.to.toUpperCase(); msgHdrMatch = function(pMsgHdr) { return (((pMsgHdr.attr & MSG_DELETE) == 0) && (pMsgHdr.to.toUpperCase() == toNameUppercase)); }; } // Perform the search // Write "Searching.." in case searching takes a while. console.print("\1n"); if (pPositionCursorForStatus) { console.gotoxy(1, console.screen_rows); console.cleartoeol(); console.gotoxy(this.msgAreaLeft, console.screen_rows); } console.print("\1h\1ySearching\1i...\1n"); // Look for the next message that contains the given message's subject var nextMsgOffset = -1; var numOfMessages = this.NumMessages(); if (pMsgHdr.offset < numOfMessages - 1) { var nextMsgHdr; for (var messageIdx = pMsgHdr.offset+1; (messageIdx < numOfMessages) && (nextMsgOffset == -1); ++messageIdx) { nextMsgHdr = this.GetMsgHdrByIdx(messageIdx); if (msgHdrMatch(nextMsgHdr)) nextMsgOffset = nextMsgHdr.offset; } } if (nextMsgOffset > -1) newMsgOffset = nextMsgOffset; } break; } // If no messages were found, then output a message to say so with a momentary pause. if (newMsgOffset == -1) { if (pPositionCursorForStatus) { console.gotoxy(1, console.screen_rows); console.cleartoeol(); console.gotoxy(this.msgAreaLeft, console.screen_rows); } else console.crlf(); console.print("\1n\1h\1yNo messages found.\1n"); mswait(ERROR_PAUSE_WAIT_MS); } return newMsgOffset; } // For the DigDistMsgReader class: Looks for the previous message in the thread of // a given message (by its header). // // Paramters: // pMsgHdr: A message header object - The previous message in the thread will be // searched starting from this message // pThreadType: The type of threading to use. Can be THREAD_BY_ID, THREAD_BY_TITLE, // THREAD_BY_AUTHOR, or THREAD_BY_TO_USER. // pPositionCursorForStatus: Optional boolean - Whether or not to move the cursor // to the bottom row before outputting status messages. // Defaults to false. // // Return value: The offset (index) of the previous message thread, or -1 if // none was found. function DigDistMsgReader_FindThreadPrevOffset(pMsgHdr, pThreadType, pPositionCursorForStatus) { if ((this.msgbase == null) || (!this.msgbase.is_open)) return -1; if ((pMsgHdr == null) || (typeof(pMsgHdr) != "object")) return -1; var newMsgOffset = -1; switch (pThreadType) { case THREAD_BY_ID: default: // The thread_id field was introduced in Synchronet 3.16. So, if // the Synchronet version is 3.16 or higher and the message header // has a thread_id field, then look for the previous message with the // same thread ID. If the Synchronet version is below 3.16 or there // is no thread ID, then fall back to using the header's thread_next, // if it exists. if ((system.version_num >= 31600) && (typeof(pMsgHdr.thread_id) == "number")) { // Look for the previous message with the same thread ID. // Write "Searching.." in case searching takes a while. console.print("\1n"); if (pPositionCursorForStatus) { console.gotoxy(1, console.screen_rows); console.cleartoeol(); console.gotoxy(this.msgAreaLeft, console.screen_rows); } console.print("\1h\1ySearching\1i...\1n"); // Look for the previous message in the thread var nextMsgOffset = -1; if (pMsgHdr.offset > 0) { var prevMsgHdr; for (var messageIdx = pMsgHdr.offset-1; (messageIdx >= 0) && (nextMsgOffset == -1); --messageIdx) { prevMsgHdr = this.GetMsgHdrByIdx(messageIdx); if (((prevMsgHdr.attr & MSG_DELETE) == 0) && (typeof(prevMsgHdr.thread_id) == "number") && (prevMsgHdr.thread_id == pMsgHdr.thread_id)) nextMsgOffset = prevMsgHdr.offset; } } if (nextMsgOffset > -1) newMsgOffset = nextMsgOffset; } // Fall back to thread_next if the Synchronet version is below 3.16 or there is // no thread_id field in the header else if ((typeof(pMsgHdr.thread_back) == "number") && (pMsgHdr.thread_back > 0)) newMsgOffset = this.AbsMsgNumToIdx(pMsgHdr.thread_back); /* // If thread_back is valid for the message header, then use that. if ((typeof(pMsgHdr.thread_back) == "number") && (pMsgHdr.thread_back > 0)) newMsgOffset = this.AbsMsgNumToIdx(pMsgHdr.thread_back); else { // If thread_id is defined and the index of the first message // in the thread is before the current message, then search // backwards for messages with a matching thread_id. var firstThreadMsgIdx = this.AbsMsgNumToIdx(pMsgHdr.thread_first); if ((typeof(pMsgHdr.thread_id) == "number") && (firstThreadMsgIdx < pMsgHdr.offset)) { // Note (2014-10-11): Digital Man said thread_id was // introduced in Synchronet version 3.16 and was still // a work in progress and isn't 100% accurate for // networked sub-boards. // Look for the previous message with the same thread ID. // Note: I'm not sure when thread_id was introduced in // Synchronet, so I'm not sure of the minimum version where // this will work. // Write "Searching.." in case searching takes a while. console.print("\1n"); if (pPositionCursorForStatus) { console.gotoxy(1, console.screen_rows); console.cleartoeol(); console.gotoxy(this.msgAreaLeft, console.screen_rows); } console.print("\1h\1ySearching\1i...\1n"); // Look for the previous message in the thread var nextMsgOffset = -1; if (pMsgHdr.offset > 0) { for (var messageIdx = pMsgHdr.offset-1; (messageIdx >= 0) && (nextMsgOffset == -1); --messageIdx) { var prevMsgHdr = this.GetMsgHdrByIdx(messageIdx); if (((prevMsgHdr.attr & MSG_DELETE) == 0) && (typeof(prevMsgHdr.thread_id) == "number") && (prevMsgHdr.thread_id == pMsgHdr.thread_id)) nextMsgOffset = prevMsgHdr.offset; } } if (nextMsgOffset > -1) newMsgOffset = nextMsgOffset; } } */ break; case THREAD_BY_TITLE: case THREAD_BY_AUTHOR: case THREAD_BY_TO_USER: // Title (subject) searching will look for the subject anywhere in the // other messages' subjects (not a fully exact subject match), so if // the message subject is blank, we won't want to do the search. var doSearch = true; if ((pThreadType == THREAD_BY_TITLE) && (pMsgHdr.subject.length == 0)) doSearch = false; if (doSearch) { var subjUppercase = ""; var fromNameUppercase = ""; var toNameUppercase = ""; // Set up a message header matching function, depending on // which field of the header we want to match var msgHdrMatch; if (pThreadType == THREAD_BY_TITLE) { subjUppercase = pMsgHdr.subject.toUpperCase(); // Remove any leading instances of "RE:" from the subject while (/^RE:/.test(subjUppercase)) subjUppercase = subjUppercase.substr(3); while (/^RE: /.test(subjUppercase)) subjUppercase = subjUppercase.substr(4); // Remove any leading & trailing whitespace from the subject subjUppercase = trimSpaces(subjUppercase, true, true, true); msgHdrMatch = function(pMsgHdr) { return (((pMsgHdr.attr & MSG_DELETE) == 0) && (pMsgHdr.subject.toUpperCase().indexOf(subjUppercase, 0) > -1)); }; } else if (pThreadType == THREAD_BY_AUTHOR) { fromNameUppercase = pMsgHdr.from.toUpperCase(); msgHdrMatch = function(pMsgHdr) { return (((pMsgHdr.attr & MSG_DELETE) == 0) && (pMsgHdr.from.toUpperCase() == fromNameUppercase)); }; } else if (pThreadType == THREAD_BY_TO_USER) { toNameUppercase = pMsgHdr.to.toUpperCase(); msgHdrMatch = function(pMsgHdr) { return (((pMsgHdr.attr & MSG_DELETE) == 0) && (pMsgHdr.to.toUpperCase() == toNameUppercase)); }; } // Perform the search // Write "Searching.." in case searching takes a while. console.print("\1n"); if (pPositionCursorForStatus) { console.gotoxy(1, console.screen_rows); console.cleartoeol(); console.gotoxy(this.msgAreaLeft, console.screen_rows); } console.print("\1h\1ySearching\1i...\1n"); // Look for the next message that contains the given message's subject var nextMsgOffset = -1; if (pMsgHdr.offset > 0) { var nextMsgHdr; for (var messageIdx = pMsgHdr.offset-1; (messageIdx >= 0) && (nextMsgOffset == -1); --messageIdx) { nextMsgHdr = this.GetMsgHdrByIdx(messageIdx); if (msgHdrMatch(nextMsgHdr)) nextMsgOffset = nextMsgHdr.offset; } } if (nextMsgOffset > -1) newMsgOffset = nextMsgOffset; } break; } // If no messages were found, then output a message to say so with a momentary pause. if (newMsgOffset == -1) { if (pPositionCursorForStatus) { console.gotoxy(1, console.screen_rows); console.cleartoeol(); console.gotoxy(this.msgAreaLeft, console.screen_rows); } else console.crlf(); console.print("\1n\1h\1yNo messages found.\1n"); mswait(ERROR_PAUSE_WAIT_MS); } return newMsgOffset; } // For the DigDistMsgReader class: Calculates the top message index for a page, // for the traditional-style message list. // // Parameters: // pPageNum: A page number (1-based) function DigDistMsgReader_CalcTraditionalMsgListTopIdx(pPageNum) { if (this.reverseListOrder) this.tradListTopMsgIdx = this.NumMessages() - (this.tradMsgListNumLines * (pPageNum-1)) - 1; else this.tradListTopMsgIdx = (this.tradMsgListNumLines * (pPageNum-1)); } // For the DigDistMsgReader class: Calculates the top message index for a page, // for the lightbar message list. // // Parameters: // pPageNum: A page number (1-based) function DigDistMsgReader_CalcLightbarMsgListTopIdx(pPageNum) { if (this.reverseListOrder) this.lightbarListTopMsgIdx = this.NumMessages() - (this.lightbarMsgListNumLines * (pPageNum-1)) - 1; else this.lightbarListTopMsgIdx = (this.lightbarMsgListNumLines * (pPageNum-1)); } // For the DigDistMsgReader class: Given a message number (1-based), this calculates // the screen index veriables (stored in the object) for the message list. This is // used for the enhanced reader mode when we want the message list to be in the // correct place for the message being read. // // Parameters: // pMsgNum: The message number (1-based) function DigDistMsgReader_CalcMsgListScreenIdxVarsFromMsgNum(pMsgNum) { // Calculate the message list variables var numItemsPerPage = this.tradMsgListNumLines; if (this.msgListUseLightbarListInterface && canDoHighASCIIAndANSI()) numItemsPerPage = this.lightbarMsgListNumLines; var newPageNum = findPageNumOfItemNum(pMsgNum, numItemsPerPage, this.NumMessages(), this.reverseListOrder); this.CalcTraditionalMsgListTopIdx(newPageNum); this.CalcLightbarMsgListTopIdx(newPageNum); this.lightbarListSelectedMsgIdx = pMsgNum - 1; if (this.lightbarListCurPos == null) this.lightbarListCurPos = new Object(); this.lightbarListCurPos.x = 1; this.lightbarListCurPos.y = this.lightbarMsgListStartScreenRow + ((pMsgNum-1) - this.lightbarListTopMsgIdx); } // For the DigDistMsgReader class: Validates a user's choice in message area. // Returns a status/error message for the caller to display if there's an // error. This function outputs intermediate status messages (i.e., // "Searching.."). // // Parameters: // pGrpIdx: The message group index (i.e., bbs.curgrp) // pSubIdx: The message sub-board index (i.e., bbs.cursub) // pCurPos: Optional - An object containing x and y properties representing // the cursor position. Used for outputting intermediate status // messages, but not for outputting the error message. // // Return value: An object containing the following properties: // msgAreaGood: A boolean to indicate whether the message area // can be selected // errorMsg: If the message area can't be selected, this string // will contain an eror message. Otherwise, this will // be an empty string. function DigDistMsgReader_ValidateMsgAreaChoice(pGrpIdx, pSubIdx, pCurPos) { var retObj = new Object(); retObj.msgAreaGood = true; retObj.errorMsg = ""; // Get the internal code of the sub-board from the given group & sub-board // indexes var subCode = msg_area.grp_list[pGrpIdx].sub_list[pSubIdx].code; // If a search is specified that would populate the search results, then do // a search in the given sub-board. if (this.SearchTypePopulatesSearchResults()) { // See if we can use pCurPos to move the cursor before displaying messages var useCurPos = (console.term_supports(USER_ANSI) && (typeof(pCurPos) == "object") && (typeof(pCurPos.x) == "number") && (typeof(pCurPos.y) == "number")); // TODO: In case new messages were posted in this sub-board, it might help // to check the current number of messages vs. the previous number of messages // and search the new messages if there are more. // Determine whether or not to search - If there are no search results for // the given sub-board already, then do a search in the sub-board. var doSearch = true; if (this.msgSearchHdrs.hasOwnProperty(subCode) && (typeof(this.msgSearchHdrs[subCode]) == "object") && (typeof(this.msgSearchHdrs[subCode].indexed) != "undefined")) { doSearch = (this.msgSearchHdrs[subCode].indexed.length == 0); } if (doSearch) { if (useCurPos) { console.gotoxy(pCurPos); console.cleartoeol("\1n"); console.gotoxy(pCurPos); } console.print("\1n\1h\1wSearching\1i...\1n"); var msgBase = new MsgBase(subCode); if (msgBase.open()) { this.msgSearchHdrs[subCode] = searchMsgbase(subCode, msgBase, this.searchType, this.searchString, this.readingPersonalEmailFromUser); msgBase.close(); // If there are no messages, then set the return object variables to indicate so. if (this.msgSearchHdrs[subCode].indexed.length == 0) { retObj.msgAreaGood = false; retObj.errorMsg = "No search results found"; } } else { retObj.msgAreaGood = false; retObj.errorMsg = "Unable to open message base (for searching)!"; } } } else { // No search is specified. Just check to see if there are any messages // to read in the given sub-board. var msgBase = new MsgBase(subCode); if (msgBase.open()) { if (msgBase.total_msgs == 0) { retObj.msgAreaGood = false; retObj.errorMsg = "No messages in that message area"; } msgBase.close(); } } return retObj; } // For the DigDistMsgReader class: Writes message lines to a file on the BBS // machine. // // Parameters: // pMsgHdr: The header object for the message // pFilename: The name of the file to write the message to // pStripCtrl: Boolean - Whether or not to remove Synchronet control // codes from the message lines // pMsgLines: An array containing the message lines // pAttachments: An array containing attachment information (as returned by determineMsgAttachments()) // // Return value: An object containing the following properties: // succeeded: Boolean - Whether or not the file was successfully written // errorMsg: String - On failure, will contain the reason it failed function DigDistMsgReader_SaveMsgToFile(pMsgHdr, pFilename, pStripCtrl, pMsgLines, pAttachments) { // Sanity checking if (typeof(pFilename) != "string") return({ succeeded: false, errorMsg: "Filename parameter not a string"}); if (pFilename.length == 0) return({ succeeded: false, errorMsg: "Empty filename given"}); // If no message lines are passed in, then get the message lines now. var msgLines = pMsgLines; var attachments = pAttachments; if ((pMsgLines == null) || (typeof(pMsgLines) != "object")) { if (typeof(pMsgHdr) == "object") { // Get the message text, interpret any @-codes in it, replace tabs with spaces // to prevent weirdness when displaying the message lines, and word-wrap the // text so that it looks good on the screen, //GetMsgInfoForEnhancedReader(pMsgHdr, pWordWrap, pDetermineAttachments, pGetB64Data) //var msgInfo = this.GetMsgInfoForEnhancedReader(pMsgHdr, false, false, false); var msgInfo = this.GetMsgInfoForEnhancedReader(pMsgHdr, true, true, true); msgLines = msgInfo.messageLines; if (msgInfo.hasOwnProperty("attachments")) attachments = msgInfo.attachments; } else return({ succeeded: false, errorMsg: "No message lines and null header object"}); } var retObj = new Object(); retObj.succeeded = true; retObj.errorMsg = ""; // If there are message attachments, then treat pFilename as a directory and // create the directory for saving both the message text & attachments. // Then, save the attachments to that directory. var msgTextFilename = pFilename; if ((attachments != null) && (attachments.length > 0)) { if (file_isdir(pFilename)) { if (file_exists(pFilename)) return({ succeeded: false, errorMsg: "Can't make directory: File with that name exists"}); } else { if (!mkdir(pFilename)) return({ succeeded: false, errorMsg: "Failed to create directory"}); } // The name of the file to save the message text will be called "messageText.txt" // in the save directory. var savePathWithTrailingSlash = backslash(pFilename); msgTextFilename = savePathWithTrailingSlash + "messageText.txt"; // Save the attachments to the directory var saveFileError = ""; for (var attachIdx = 0; (attachIdx < attachments.length) && (saveFileError.length == 0); ++attachIdx) { var destFilename = savePathWithTrailingSlash + attachments[attachIdx].filename; // If the file info has base64 data, then decode & save it to the directory. // Otherwise, the file was probably uploaded to the user's mailbox in Synchronet, // so copy the file to the save directory. if (attachments[attachIdx].hasOwnProperty("B64Data")) { var attachedFile = new File(destFilename); if (attachedFile.open("wb")) { attachedFile.base64 = true; if (!attachedFile.write(attachments[attachIdx].B64Data)) saveFileError = "\1n\1cFailed to save " + attachments[attachIdx].filename; attachedFile.close(); } } else { // There is no base64 data for the file, so it's probably in the // user's mailbox in Synchronet, so copy it to the save directory. if (file_exists(attachments[attachIdx].fullyPathedFilename)) { if (!file_copy(attachments[attachIdx].fullyPathedFilename, destFilename)) saveFileError = "Failed to copy " + attachments[attachIdx].filename; } else saveFileError = "File " + attachments[attachIdx].fullyPathedAttachmentFilename + " doesn't exist"; } } if (saveFileError.length > 0) return({ succeeded: false, errorMsg: saveFileError }); } var messageSaveFile = new File(msgTextFilename); if (messageSaveFile.open("w")) { // Write some header information to the file if (typeof(pMsgHdr) == "object") { if (pMsgHdr.hasOwnProperty("from")) messageSaveFile.writeln("From: " + pMsgHdr.from); if (pMsgHdr.hasOwnProperty("to")) messageSaveFile.writeln(" To: " + pMsgHdr.to); if (pMsgHdr.hasOwnProperty("subject")) messageSaveFile.writeln("Subj: " + pMsgHdr.subject); /* if (pMsgHdr.hasOwnProperty("when_written_time")) messageSaveFile.writeln(strftime("Date: %Y-%m-%d %H:%M:%S", msgHeader.when_written_time)); */ if (pMsgHdr.hasOwnProperty("date")) messageSaveFile.writeln("Date: " + pMsgHdr.date); if (pMsgHdr.hasOwnProperty("from_net_addr")) messageSaveFile.writeln("From net address: " + pMsgHdr.from_net_addr); if (pMsgHdr.hasOwnProperty("to_net_addr")) messageSaveFile.writeln("To net address: " + pMsgHdr.to_net_addr); if (pMsgHdr.hasOwnProperty("id")) messageSaveFile.writeln("ID: " + pMsgHdr.id); if (pMsgHdr.hasOwnProperty("reply_id")) messageSaveFile.writeln("Reply ID: " + pMsgHdr.reply_id); messageSaveFile.writeln("==============================="); } // Write the message body to the file if (pStripCtrl) { for (var msgLineIdx = 0; msgLineIdx < msgLines.length; ++msgLineIdx) messageSaveFile.writeln(strip_ctrl(msgLines[msgLineIdx])); } else { for (var msgLineIdx = 0; msgLineIdx < msgLines.length; ++msgLineIdx) messageSaveFile.writeln(msgLines[msgLineIdx]); } messageSaveFile.close(); } else { retObj.succeeded = false; retObj.errorMsg = "Unable to open the message file for writing"; } return retObj; } // For the DigDistMsgReader class: Toggles whether a message has been 'selected' // (i.e., for things like batch delete, etc.) // // Parameters: // pSubCode: The internal sub-board code of the message sub-board where the // message resides // pMsgIdx: The index of the message to toggle // pSelected: Optional boolean to explictly specify whether the message should // be selected. If this is not provided (or is null), then this // message will simply toggle the selection state of the message. function DigDistMsgReader_ToggleSelectedMessage(pSubCode, pMsgIdx, pSelected) { // Sanity checking if (typeof(pSubCode) != "string") return; if (typeof(pMsgIdx) != "number") return; // If the 'selected message' object doesn't have the sub code index, // then add it. if (!this.selectedMessages.hasOwnProperty(pSubCode)) this.selectedMessages[pSubCode] = new Object(); // If pSelected is a boolean, then it specifies the specific selection // state of the message (true = selected, false = not selected). if (typeof(pSelected) == "boolean") { if (pSelected) { if (!this.selectedMessages[pSubCode].hasOwnProperty(pMsgIdx)) this.selectedMessages[pSubCode][pMsgIdx] = true; } else { if (this.selectedMessages[pSubCode].hasOwnProperty(pMsgIdx)) delete this.selectedMessages[pSubCode][pMsgIdx]; } } else { // pSelected is not a boolean, so simply toggle the selection state of // the message. // If the object for the given sub-board code contains the message // index, then remove it. Otherwise, add it. if (this.selectedMessages[pSubCode].hasOwnProperty(pMsgIdx)) delete this.selectedMessages[pSubCode][pMsgIdx]; else this.selectedMessages[pSubCode][pMsgIdx] = true; } } // For the DigDistMsgReader class: Returns whether a message (by sub-board code & index) // is selected (i.e., for batch delete, etc.). // // Parameters: // pSubCode: The internal sub-board code of the message sub-board where the // message resides // pMsgIdx: The index of the message to toggle // // Return value: Boolean - Whether or not the given message has been selected function DigDistMsgReader_MessageIsSelected(pSubCode, pMsgIdx) { return (this.selectedMessages.hasOwnProperty(pSubCode) && this.selectedMessages[pSubCode].hasOwnProperty(pMsgIdx)); } // For the DigDistMsgReader class: Checks to see if all selected messages can // be deleted (i.e., whether the user has permission to delete all of them). function DigDistMsgReader_AllSelectedMessagesCanBeDeleted() { // If the user has sysop access, then they should be able to delete messages. if (gIsSysop) return true; var userCanDeleteAllSelectedMessages = true; var msgBase = null; for (var subBoardCode in this.selectedMessages) { // If the current sub-board is personal mail, then the user can delete // those messages. Otherwise, check the sub-board configuration to see // if the user can delete messages in the sub-board. if (subBoardCode != "mail") { msgBase = new MsgBase(subBoardCode); if (msgBase.open()) { userCanDeleteAllSelectedMessages = userCanDeleteAllSelectedMessages && ((msgBase.cfg.settings & SUB_DEL) == SUB_DEL); msgBase.close(); } } if (!userCanDeleteAllSelectedMessages) break; } return userCanDeleteAllSelectedMessages; } // For the DigDistMsgReader class: Marks the 'selected messages' (in // this.selecteMessages) as deleted. Returns an object with the following // properties: // deletedAll: Boolean - Whether or not all messages were successfully marked // for deletion // failureList: An object containing indexes of messages that failed to get // marked for deletion, indexed by internal sub-board code, then // containing messages indexes as properties. Reasons for failing // to mark messages deleted can include the user not having permission // to delete in a sub-board, failure to open the sub-board, etc. function DigDistMsgReader_DeleteSelectedMessages() { var retObj = new Object(); retObj.deletedAll = true; retObj.failureList = new Object(); var msgBase = null; var msgHdr = null; var msgWasDeleted = false; for (var subBoardCode in this.selectedMessages) { msgBase = new MsgBase(subBoardCode); if (msgBase.open()) { // Allow the user to delete the messages if they're the sysop, they're // reading their personal mail, or the sub-board allows deleting messages. if (gIsSysop || (subBoardCode == "mail") || ((msgBase.cfg.settings & SUB_DEL) == SUB_DEL)) { for (var msgIdx in this.selectedMessages[subBoardCode]) { // It seems that msgIdx is a string, so make sure we have a // numeric version. var msgIdxNumber = +msgIdx; // If doing a search (this.msgSearchHdrs has the sub-board code), // then get the message header by index from there. Otherwise, // use the message base object to get the message by index. if (this.msgSearchHdrs.hasOwnProperty(subBoardCode) && (this.msgSearchHdrs[subBoardCode].indexed.length > 0)) { if ((msgIdxNumber >= 0) && (msgIdxNumber < this.msgSearchHdrs[subBoardCode].indexed.length)) msgHdr = this.msgSearchHdrs[subBoardCode].indexed[msgIdxNumber]; } else { if ((msgIdxNumber >= 0) && (msgIdxNumber < msgBase.total_msgs)) msgHdr = msgBase.get_msg_header(true, msgIdxNumber, true); } // If we got the message header, then mark it for deletion. // If the message header wasn't marked as deleted, then add // the message index to the return object. if (msgHdr != null) msgWasDeleted = msgBase.remove_msg(true, msgHdr.offset); else msgWasDeleted = false; if (msgWasDeleted) { // Refresh the message header in the search results (if it // exists there) and remove the message index from the // selectedMessages object this.RefreshSearchResultMsgHdr(msgIdxNumber, MSG_DELETE, subBoardCode); delete this.selectedMessages[subBoardCode][msgIdx]; } else { retObj.deletedAll = false; if (!retObj.failureList.hasOwnProperty(subBoardCode)) retObj.failureList[subBoardCode] = new Object(); retObj.failureList[subBoardCode].push(msgIdxNumber); } } // If the sub-board index array no longer has any properties (i.e., // all messages in the sub-board were marked as deleted), then remove // the sub-board property from this.selectedMessages if (Object.keys(this.selectedMessages[subBoardCode]).length == 0) delete this.selectedMessages[subBoardCode]; } else { // The user doesn't have permission to delete messages // in this sub-board. // Create an entry in retObj.failureList indexed by the // sub-board code to indicate failure to delete all // messages in the sub-board. retObj.deletedAll = false; retObj.failureList[subBoardCode] = new Object(); } msgBase.close(); } else { // Failure to open the sub-board. // Create an entry in retObj.failureList indexed by the // sub-board code to indicate failure to delete all messages // in the sub-board. retObj.deletedAll = false; retObj.failureList[subBoardCode] = new Object(); } } return retObj; } // For the DigDistMsgReader class: Returns the number of selected messages function DigDistMsgReader_NumSelectedMessages() { var numSelectedMsgs = 0; for (var subBoardCode in this.selectedMessages) numSelectedMsgs += Object.keys(this.selectedMessages[subBoardCode]).length; return numSelectedMsgs; } /////////////////////////////////////////////////////////////////////////////////// // Helper functions // Displays the program information. function DisplayProgramInfo() { displayTextWithLineBelow("Digital Distortion Message Reader", true, "\1n\1c\1h", "\1k\1h") console.center("\1n\1cVersion \1g" + READER_VERSION + " \1w\1h(\1b" + READER_DATE + "\1w)"); console.crlf(); } // This function returns an array of default colors used in the // DigDistMessageReader class. function getDefaultColors() { var colorArray = new Array(); // Header line: "Current msg group:" colorArray["msgListHeaderMsgGroupTextColor"] = "\1n\1" + "4\1c"; // Normal cyan on blue background //colorArray["msgListHeaderMsgGroupTextColor"] = "\1n\1" + "4\1w"; // Normal white on blue background // Header line: Message group name colorArray["msgListHeaderMsgGroupNameColor"] = "\1h\1c"; // High cyan //colorArray["msgListHeaderMsgGroupNameColor"] = "\1h\1w"; // High white // Header line: "Current sub-board:" colorArray["msgListHeaderSubBoardTextColor"] = "\1n\1" + "4\1c"; // Normal cyan on blue background //colorArray["msgListHeaderSubBoardTextColor"] = "\1n\1" + "4\1w"; // Normal white on blue background // Header line: Message sub-board name colorArray["msgListHeaderMsgSubBoardName"] = "\1h\1c"; // High cyan //colorArray["msgListHeaderMsgSubBoardName"] = "\1h\1w"; // High white // Line with column headers //colorArray["msgListColHeader"] = "\1h\1w"; // High white (keep blue background) colorArray["msgListColHeader"] = "\1n\1h\1w"; // High white on black background //colorArray["msgListColHeader"] = "\1h\1c"; // High cyan (keep blue background) //colorArray["msgListColHeader"] = "\1" + "4\1h\1y"; // High yellow (keep blue background) // Message list information colorArray["msgListMsgNumColor"] = "\1n\1h\1y"; colorArray["msgListFromColor"] = "\1n\1c"; colorArray["msgListToColor"] = "\1n\1c"; colorArray["msgListSubjectColor"] = "\1n\1c"; colorArray["msgListDateColor"] = "\1h\1b"; colorArray["msgListTimeColor"] = "\1h\1b"; // Message information for messages written to the user colorArray["msgListToUserMsgNumColor"] = "\1n\1h\1y"; colorArray["msgListToUserFromColor"] = "\1h\1g"; colorArray["msgListToUserToColor"] = "\1h\1g"; colorArray["msgListToUserSubjectColor"] = "\1h\1g"; colorArray["msgListToUserDateColor"] = "\1h\1b"; colorArray["msgListToUserTimeColor"] = "\1h\1b"; // Message information for messages from the user colorArray["msgListFromUserMsgNumColor"] = "\1n\1h\1y"; colorArray["msgListFromUserFromColor"] = "\1n\1c"; colorArray["msgListFromUserToColor"] = "\1n\1c"; colorArray["msgListFromUserSubjectColor"] = "\1n\1c"; colorArray["msgListFromUserDateColor"] = "\1h\1b"; colorArray["msgListFromUserTimeColor"] = "\1h\1b"; // Message list highlight colors colorArray["msgListHighlightBkgColor"] = "\1" + "4"; // Background colorArray["msgListMsgNumHighlightColor"] = "\1h\1y"; colorArray["msgListFromHighlightColor"] = "\1h\1c"; colorArray["msgListToHighlightColor"] = "\1h\1c"; colorArray["msgListSubjHighlightColor"] = "\1h\1c"; colorArray["msgListDateHighlightColor"] = "\1h\1w"; colorArray["msgListTimeHighlightColor"] = "\1h\1w"; // Lightbar message list help line colors colorArray["lightbarMsgListHelpLineBkgColor"] = "\1" + "7"; // Background colorArray["lightbarMsgListHelpLineGeneralColor"] = "\1b"; colorArray["lightbarMsgListHelpLineHotkeyColor"] = "\1r"; colorArray["lightbarMsgListHelpLineParenColor"] = "\1m"; // Continue prompt colors colorArray["tradInterfaceContPromptMainColor"] = "\1n\1g"; // Main text color colorArray["tradInterfaceContPromptHotkeyColor"] = "\1h\1c"; // Hotkey color colorArray["tradInterfaceContPromptUserInputColor"] = "\1h\1g"; // User input color // Message body color colorArray["msgBodyColor"] = "\1n\1w"; // Read message confirmation colors colorArray["readMsgConfirmColor"] = "\1n\1c"; colorArray["readMsgConfirmNumberColor"] = "\1h\1c"; // Prompt for continuing to list messages after reading a message colorArray["afterReadMsg_ListMorePromptColor"] = "\1n\1c"; // Help screen text color colorArray["tradInterfaceHelpScreenColor"] = "\1n\1h\1w"; // Colors for choosing a message group & sub-board colorArray["areaChooserMsgAreaNumColor"] = "\1n\1w\1h"; colorArray["areaChooserMsgAreaDescColor"] = "\1n\1c"; colorArray["areaChooserMsgAreaNumItemsColor"] = "\1b\1h"; colorArray["areaChooserMsgAreaHeaderColor"] = "\1n\1y\1h"; colorArray["areaChooserSubBoardHeaderColor"] = "\1n\1g"; colorArray["areaChooserMsgAreaMarkColor"] = "\1g\1h"; colorArray["areaChooserMsgAreaLatestDateColor"] = "\1n\1g"; colorArray["areaChooserMsgAreaLatestTimeColor"] = "\1n\1m"; // Highlighted colors (for lightbar mode) colorArray["areaChooserMsgAreaBkgHighlightColor"] = "\1" + "4"; // Blue background colorArray["areaChooserMsgAreaNumHighlightColor"] = "\1w\1h"; colorArray["areaChooserMsgAreaDescHighlightColor"] = "\1c"; colorArray["areaChooserMsgAreaDateHighlightColor"] = "\1w\1h"; colorArray["areaChooserMsgAreaTimeHighlightColor"] = "\1w\1h"; colorArray["areaChooserMsgAreaNumItemsHighlightColor"] = "\1w\1h"; // Lightbar area chooser help line colorArray["lightbarAreaChooserHelpLineBkgColor"] = "\1" + "7"; // Background colorArray["lightbarAreaChooserHelpLineGeneralColor"] = "\1b"; colorArray["lightbarAreaChooserHelpLineHotkeyColor"] = "\1r"; colorArray["lightbarAreaChooserHelpLineParenColor"] = "\1m"; // Scrollbar background and scroll block colors (for the enhanced // message reader interface) colorArray["scrollbarBGColor"] = "\1n\1h\1k"; colorArray["scrollbarScrollBlockColor"] = "\1n\1h\1w"; // Color for the line drawn in the 2nd to last line of the message // area in the enhanced reader mode before a prompt colorArray["enhReaderPromptSepLineColor"] = "\1n\1h\1g"; // Colors for the enhanced reader help line colorArray["enhReaderHelpLineBkgColor"] = "\1" + "7"; colorArray["enhReaderHelpLineGeneralColor"] = "\1b"; colorArray["enhReaderHelpLineHotkeyColor"] = "\1r"; colorArray["enhReaderHelpLineParenColor"] = "\1m"; // Message header line colors colorArray["hdrLineLabelColor"] = "\1n\1c"; colorArray["hdrLineValueColor"] = "\1n\1b\1h"; // Selected message marker color colorArray["selectedMsgMarkColor"] = "\1n\1w\1h"; return colorArray; } // This function returns the month number (1-based) from a capitalized // month name. // // Parameters: // pMonthName: The name of the month // // Return value: The number of the month (1-12). function getMonthNum(pMonthName) { var monthNum = 1; if (pMonthName.substr(0, 3) == "Jan") monthNum = 1; else if (pMonthName.substr(0, 3) == "Feb") monthNum = 2; else if (pMonthName.substr(0, 3) == "Mar") monthNum = 3; else if (pMonthName.substr(0, 3) == "Apr") monthNum = 4; else if (pMonthName.substr(0, 3) == "May") monthNum = 5; else if (pMonthName.substr(0, 3) == "Jun") monthNum = 6; else if (pMonthName.substr(0, 3) == "Jul") monthNum = 7; else if (pMonthName.substr(0, 3) == "Aug") monthNum = 8; else if (pMonthName.substr(0, 3) == "Sep") monthNum = 9; else if (pMonthName.substr(0, 3) == "Oct") monthNum = 10; else if (pMonthName.substr(0, 3) == "Nov") monthNum = 11; else if (pMonthName.substr(0, 3) == "Dec") monthNum = 12; return monthNum; } // Clears each line from a given line to the end of the screen. // // Parameters: // pStartLineNum: The line number to start at (1-based) function clearToEOS(pStartLineNum) { if (typeof(pStartLineNum) == "undefined") return; if (pStartLineNum == null) return; for (var lineNum = pStartLineNum; lineNum <= console.screen_rows; ++lineNum) { console.gotoxy(1, lineNum); console.clearline(); } } // Returns the number of messages in a sub-board. // // Parameters: // pSubBoardCode: The sub-board code (i.e., from bbs.cursub_code) // // Return value: The number of messages in the sub-board, or 0 // if the sub-board could not be opened. function numMessages(pSubBoardCode) { var messageCount = 0; var myMsgbase = new MsgBase(pSubBoardCode); if (myMsgbase.open()) messageCount = myMsgbase.total_msgs; myMsgbase.close(); myMsgbase = null; return messageCount; } // Removes multiple, leading, and/or trailing spaces // The search & replace regular expressions used in this // function came from the following URL: // http://qodo.co.uk/blog/javascript-trim-leading-and-trailing-spaces // // Parameters: // pString: The string to trim // pLeading: Whether or not to trim leading spaces (optional, defaults to true) // pMultiple: Whether or not to trim multiple spaces (optional, defaults to true) // pTrailing: Whether or not to trim trailing spaces (optional, defaults to true) // // Return value: The string with whitespace trimmed function trimSpaces(pString, pLeading, pMultiple, pTrailing) { var leading = true; var multiple = true; var trailing = true; if(typeof(pLeading) != "undefined") leading = pLeading; if(typeof(pMultiple) != "undefined") multiple = pMultiple; if(typeof(pTrailing) != "undefined") trailing = pTrailing; // To remove both leading & trailing spaces: //pString = pString.replace(/(^\s*)|(\s*$)/gi,""); if (leading) pString = pString.replace(/(^\s*)/gi,""); if (multiple) pString = pString.replace(/[ ]{2,}/gi," "); if (trailing) pString = pString.replace(/(\s*$)/gi,""); return pString; } // Returns whether an internal sub-board code is valid. // // Parameters: // pSubBoardCode: The internal sub-board code to test // // Return value: Boolean - Whether or not the given internal code is a valid // sub-board code function subBoardCodeIsValid(pSubBoardCode) { return ((pSubBoardCode == "mail") || (typeof(msg_area.sub[pSubBoardCode]) == "object")) } // Displays some text with a solid horizontal line on the next line. // // Parameters: // pText: The text to display // pCenter: Whether or not to center the text. Optional; defaults // to false. // pTextColor: The color to use for the text. Optional; by default, // normal white will be used. // pLineColor: The color to use for the line underneath the text. // Optional; by default, bright black will be used. function displayTextWithLineBelow(pText, pCenter, pTextColor, pLineColor) { var centerText = (typeof(pCenter) == "boolean" ? pCenter : false); var textColor = (typeof(pTextColor) == "string" ? pTextColor : "\1n\1w"); var lineColor = (typeof(pLineColor) == "string" ? pLineColor : "\1n\1k\1h"); // Output the text and a solid line on the next line. if (centerText) { console.center(textColor + pText); var solidLine = ""; var textLength = console.strlen(pText); for (var i = 0; i < textLength; ++i) solidLine += "�"; console.center(lineColor + solidLine); } else { console.print(textColor + pText); console.crlf(); console.print(lineColor); var textLength = console.strlen(pText); for (var i = 0; i < textLength; ++i) console.print("�"); console.crlf(); } } // Returns whether the Synchronet compile date is at least May 12, 2013. That // was when Digital Man's change to make bbs.msg_number work when a script is // running first went into the Synchronet daily builds. function compileDateAtLeast2013_05_12() { // system.compiled_when is in the following format: // May 12 2013 05:02 var compileDateParts = system.compiled_when.split(" "); if (compileDateParts.length < 4) return false; // Convert the month to a 1-based number var compileMonth = 0; if (/^Jan/.test(compileDateParts[0])) compileMonth = 1; else if (/^Feb/.test(compileDateParts[0])) compileMonth = 2; else if (/^Mar/.test(compileDateParts[0])) compileMonth = 3; else if (/^Apr/.test(compileDateParts[0])) compileMonth = 4; else if (/^May/.test(compileDateParts[0])) compileMonth = 5; else if (/^Jun/.test(compileDateParts[0])) compileMonth = 6; else if (/^Jul/.test(compileDateParts[0])) compileMonth = 7; else if (/^Aug/.test(compileDateParts[0])) compileMonth = 8; else if (/^Sep/.test(compileDateParts[0])) compileMonth = 9; else if (/^Oct/.test(compileDateParts[0])) compileMonth = 10; else if (/^Nov/.test(compileDateParts[0])) compileMonth = 11; else if (/^Dec/.test(compileDateParts[0])) compileMonth = 12; // Get the compileDay and compileYear as numeric variables var compileDay = +compileDateParts[1]; var compileYear = +compileDateParts[2]; // Determine if the compile date is at least 2013-05-12 var compileDateIsAtLeastMin = true; if (compileYear > 2013) compileDateIsAtLeastMin = true; else if (compileYear < 2013) compileDateIsAtLeastMin = false; else // compileYear is 2013 { if (compileMonth > 5) compileDateIsAtLeastMin = true else if (compileMonth < 5) compileDateIsAtLeastMin = false; else // compileMonth is 5 compileDateIsAtLeastMin = (compileDay >= 12); } return compileDateIsAtLeastMin; } // Removes multiple, leading, and/or trailing spaces. // The search & replace regular expressions used in this // function came from the following URL: // http://qodo.co.uk/blog/javascript-trim-leading-and-trailing-spaces // // Parameters: // pString: The string to trim // pLeading: Whether or not to trim leading spaces (optional, defaults to true) // pMultiple: Whether or not to trim multiple spaces (optional, defaults to true) // pTrailing: Whether or not to trim trailing spaces (optional, defaults to true) function trimSpaces(pString, pLeading, pMultiple, pTrailing) { var leading = true; var multiple = true; var trailing = true; if(typeof(pLeading) != "undefined") leading = pLeading; if(typeof(pMultiple) != "undefined") multiple = pMultiple; if(typeof(pTrailing) != "undefined") trailing = pTrailing; // To remove both leading & trailing spaces: //pString = pString.replace(/(^\s*)|(\s*$)/gi,""); if (leading) pString = pString.replace(/(^\s*)/gi,""); if (multiple) pString = pString.replace(/[ ]{2,}/gi," "); if (trailing) pString = pString.replace(/(\s*$)/gi,""); return pString; } // Calculates & returns a page number. // // Parameters: // pTopIndex: The index (0-based) of the topmost item on the page // pNumPerPage: The number of items per page // // Return value: The page number function calcPageNum(pTopIndex, pNumPerPage) { return ((pTopIndex / pNumPerPage) + 1); } // Returns the greatest number of messages of all sub-boards within // a message group. // // Parameters: // pGrpIndex: The index of the message group // // Returns: The greatest number of messages of all sub-boards within // the message group function getGreatestNumMsgs(pGrpIndex) { // Sanity checking if (typeof(pGrpIndex) != "number") return 0; if (typeof(msg_area.grp_list[pGrpIndex]) == "undefined") return 0; var greatestNumMsgs = 0; var msgBase = null; for (var subIndex = 0; subIndex < msg_area.grp_list[pGrpIndex].sub_list.length; ++subIndex) { msgBase = new MsgBase(msg_area.grp_list[pGrpIndex].sub_list[subIndex].code); if (msgBase == null) continue; if (msgBase.open()) { if (msgBase.total_msgs > greatestNumMsgs) greatestNumMsgs = msgBase.total_msgs; msgBase.close(); } } return greatestNumMsgs; } // Inputs a keypress from the user and handles some ESC-based // characters such as PageUp, PageDown, and ESC. If PageUp // or PageDown are pressed, this function will return the // string "\1PgUp" (KEY_PAGE_UP) or "\1Pgdn" (KEY_PAGE_DOWN), // respectively. Also, F1-F5 will be returned as "\1F1" // through "\1F5", respectively. // Thanks goes to Psi-Jack for the original impementation // of this function. // // Parameters: // pGetKeyMode: Optional - The mode bits for console.getkey(). // If not specified, K_NONE will be used. // // Return value: The user's keypress function getKeyWithESCChars(pGetKeyMode) { var getKeyMode = K_NONE; if (typeof(pGetKeyMode) == "number") getKeyMode = pGetKeyMode; var userInput = console.getkey(getKeyMode); if (userInput == KEY_ESC) { switch (console.inkey(K_NOECHO|K_NOSPIN, 2)) { case '[': switch (console.inkey(K_NOECHO|K_NOSPIN, 2)) { case 'V': userInput = KEY_PAGE_UP; break; case 'U': userInput = KEY_PAGE_DOWN; break; } break; case 'O': switch (console.inkey(K_NOECHO|K_NOSPIN, 2)) { case 'P': userInput = "\1F1"; break; case 'Q': userInput = "\1F2"; break; case 'R': userInput = "\1F3"; break; case 'S': userInput = "\1F4"; break; case 't': userInput = "\1F5"; break; } default: break; } } return userInput; } // Finds the next or previous non-empty message sub-board. Returns an // object containing the message group & sub-board indexes. If all of // the next/previous sub-boards are empty, then the given current indexes // will be returned. // // Parameters: // pStartGrpIdx: The index of the message group to start from // pStartSubIdx: The index of the sub-board in the message group to start from // pForward: Boolean - Whether or not to search forward (true) or backward (false). // Optional; defaults to true, to search forward. // // Return value: An object with the following properties: // foundSubBoard: Boolean - Whether or not a different sub-board was found // grpIdx: The message group index of the found sub-board // subIdx: The sub-board index in the group of the found sub-board // subCode: The internal code of the sub-board // subChanged: Boolean - Whether or not the found sub-board is // different from the one that was passed in // paramsValid: Boolean - Whether or not all the passed-in parameters // were valid. function findNextOrPrevNonEmptySubBoard(pStartGrpIdx, pStartSubIdx, pForward) { var retObj = new Object(); retObj.grpIdx = pStartGrpIdx; retObj.subIdx = pStartSubIdx; retObj.subCode = msg_area.grp_list[pStartGrpIdx].sub_list[pStartSubIdx].code; retObj.foundSubBoard = false; // Sanity checking retObj.paramsValid = ((pStartGrpIdx >= 0) && (pStartGrpIdx < msg_area.grp_list.length) && (pStartSubIdx >= 0) && (pStartSubIdx < msg_area.grp_list[pStartGrpIdx].sub_list.length)); if (!retObj.paramsValid) return retObj; var grpIdx = pStartGrpIdx; var subIdx = pStartSubIdx; var searchForward = (typeof(pForward) == "boolean" ? pForward : true); if (searchForward) { // Advance the sub-board (and group) index, and determine whether or not // to do the search (i.e., we might not want to if the starting sub-board // is the last sub-board in the last group). var searchForSubBoard = true; if (subIdx < msg_area.grp_list[grpIdx].sub_list.length - 1) ++subIdx; else { if ((grpIdx < msg_area.grp_list.length - 1) && (msg_area.grp_list[grpIdx+1].sub_list.length > 0)) { subIdx = 0; ++grpIdx; } else searchForSubBoard = false; } // If we can search, then do it. if (searchForSubBoard) { while (numMsgsInSubBoard(msg_area.grp_list[grpIdx].sub_list[subIdx].code) == 0) { if (subIdx < msg_area.grp_list[grpIdx].sub_list.length - 1) ++subIdx; else { if ((grpIdx < msg_area.grp_list.length - 1) && (msg_area.grp_list[grpIdx+1].sub_list.length > 0)) { subIdx = 0; ++grpIdx; } else break; // Stop searching } } } } else { // Search the sub-boards in reverse // Decrement the sub-board (and group) index, and determine whether or not // to do the search (i.e., we might not want to if the starting sub-board // is the first sub-board in the first group). var searchForSubBoard = true; if (subIdx > 0) --subIdx; else { if ((grpIdx > 0) && (msg_area.grp_list[grpIdx-1].sub_list.length > 0)) { --grpIdx; subIdx = msg_area.grp_list[grpIdx].sub_list.length - 1; } else searchForSubBoard = false; } // If we can search, then do it. if (searchForSubBoard) { while (numMsgsInSubBoard(msg_area.grp_list[grpIdx].sub_list[subIdx].code) == 0) { if (subIdx > 0) --subIdx; else { if ((grpIdx > 0) && (msg_area.grp_list[grpIdx-1].sub_list.length > 0)) { --grpIdx; subIdx = msg_area.grp_list[grpIdx].sub_list.length - 1; } else break; // Stop searching } } } } // If we found a sub-board with messages in it, then set the variables // in the return object if (numMsgsInSubBoard(msg_area.grp_list[grpIdx].sub_list[subIdx].code) > 0) { retObj.grpIdx = grpIdx; retObj.subIdx = subIdx; retObj.subCode = msg_area.grp_list[grpIdx].sub_list[subIdx].code; retObj.foundSubBoard = true; retObj.subChanged = ((grpIdx != pStartGrpIdx) || (subIdx != pStartSubIdx)); } return retObj; } // Returns the number of messages in a sub-board. // // Parameters: // pSubBoardCode: The internal code of the sub-board to check // pIncludeDeleted: Optional boolean - Whether or not to include deleted // messages in the count. Defaults to false. // // Return value: The number of messages in the sub-board function numMsgsInSubBoard(pSubBoardCode, pIncludeDeleted) { var numMessages = 0; var msgbase = new MsgBase(pSubBoardCode); if (msgbase.open()) { var includeDeleted = (typeof(pIncludeDeleted) == "boolean" ? pIncludeDeleted : false); if (includeDeleted) numMessages = msgbase.total_msgs; else { // Don't include deleted messages. Go through each message // in the sub-board and count the ones that aren't marked // as deleted. for (var msgIdx = 0; msgIdx < msgbase.total_msgs; ++msgIdx) { var msgHdr = msgbase.get_msg_header(true, msgIdx, false); if ((msgHdr != null) && ((msgHdr.attr & MSG_DELETE) == 0)) ++numMessages; } } msgbase.close(); } return numMessages; } // Replaces @-codes in a string and returns the new string. // // Parameters: // pStr: A string in which to replace @-codes // // Return value: A version of the string with @-codes interpreted function replaceAtCodesInStr(pStr) { if (typeof(pStr) != "string") return ""; // This code was originally written by Deuce. I updated it to check whether // the string returned by bbs.atcode() is null, and if so, just return // the original string. return pStr.replace(/@([^@]+)@/g, function(m, code) { var decoded = bbs.atcode(code); return (decoded != null ? decoded : "@" + code + "@"); }); } // Shortens a string, accounting for control/attribute codes. Returns a new // (shortened) copy of the string. // // Parameters: // pStr: The string to shorten // pNewLength: The new (shorter) length of the string // // Return value: The shortened version of the string function shortenStrWithAttrCodes(pStr, pNewLength) { if (typeof(pStr) != "string") return ""; if (typeof(pNewLength) != "number") return pStr; if (pNewLength >= console.strlen(pStr)) return pStr; var strCopy = ""; var tmpStr = ""; var strIdx = 0; var lengthGood = true; while (lengthGood && (strIdx < pStr.length)) { tmpStr = strCopy + pStr.charAt(strIdx++); if (console.strlen(tmpStr) <= pNewLength) strCopy = tmpStr; else lengthGood = false; } return strCopy; } // Returns whether a given name matches the logged-in user's handle, alias, or // name. // // Parameters: // pName: A name to match against the logged-in user // // Return value: Boolean - Whether or not the given name matches the logged-in // user's handle, alias, or name function userHandleAliasNameMatch(pName) { if (typeof(pName) != "string") return false; var userMatch = false; var nameUpper = pName.toUpperCase(); if (user.handle.length > 0) userMatch = (nameUpper.indexOf(user.handle.toUpperCase()) > -1); if (!userMatch && (user.alias.length > 0)) userMatch = (nameUpper.indexOf(user.alias.toUpperCase()) > -1); if (!userMatch && (user.name.length > 0)) userMatch = (nameUpper.indexOf(user.name.toUpperCase()) > -1); return userMatch; } // Displays a range of text lines on the screen and allows scrolling through them // with the up & down arrow keys, PageUp, PageDown, HOME, and END. It is assumed // that the array of text lines are already truncated to fit in the width of the // text area, as a speed optimization. // // Parameters: // pTxtLines: The array of text lines to allow scrolling for // pTopLineIdx: The index of the text line to display at the top // pTxtAttrib: The attribute(s) to apply to the text lines // pWriteTxtLines: Boolean - Whether or not to write the text lines (in addition // to doing the message loop). If false, this will only do the // the message loop. This parameter is intended as a screen // refresh optimization. // pTopLeftX: The upper-left corner column for the text area // pTopLeftY: The upper-left corner row for the text area // pWidth: The width of the text area // pHeight: The height of the text area // pPostWriteCurX: The X location for the cursor after writing the message // lines // pPostWriteCurY: The Y location for the cursor after writing the message // lines // pScrollUpdateFn: A function that the caller can provide for updating the // scroll position. This function has one parameter: // - fractionToLastPage: The fraction of the top index divided // by the top index for the last page (basically, the progress // to the last page). // // Return value: An object with the following properties: // lastKeypress: The last key pressed by the user (a string) // topLineIdx: The new top line index of the text lines, in case of scrolling function scrollTextLines(pTxtLines, pTopLineIdx, pTxtAttrib, pWriteTxtLines, pTopLeftX, pTopLeftY, pWidth, pHeight, pPostWriteCurX, pPostWriteCurY, pScrollUpdateFn) { // Variables for the top line index for the last page, scrolling, etc. var topLineIdxForLastPage = pTxtLines.length - pHeight; if (topLineIdxForLastPage < 0) topLineIdxForLastPage = 0; var msgFractionShown = pHeight / pTxtLines.length; if (msgFractionShown > 1) msgFractionShown = 1.0; var fractionToLastPage = 0; var lastTxtRow = pTopLeftY + pHeight - 1; var txtLineFormatStr = "%-" + pWidth + "s"; var retObj = new Object(); retObj.lastKeypress = ""; retObj.topLineIdx = pTopLineIdx; var writeTxtLines = pWriteTxtLines; var continueOn = true; while (continueOn) { // If we are to write the text lines, then write each of them and also // clear out the rest of the row on the screen if (writeTxtLines) { // If the scroll update function parameter is a function, then calculate // the fraction to the last page and call the scroll update function. if (typeof(pScrollUpdateFn) == "function") { if (topLineIdxForLastPage != 0) fractionToLastPage = retObj.topLineIdx / topLineIdxForLastPage; pScrollUpdateFn(fractionToLastPage); } var screenY = pTopLeftY; for (var lineIdx = retObj.topLineIdx; (lineIdx < pTxtLines.length) && (screenY <= lastTxtRow); ++lineIdx) { console.gotoxy(pTopLeftX, screenY++); // Print the text line, then clear the rest of the line console.print(pTxtAttrib + pTxtLines[lineIdx]); printf("\1n%" + +(pWidth - console.strlen(pTxtLines[lineIdx])) + "s", ""); } // If there are still some lines left in the message reading area, then // clear the lines. console.print("\1n" + pTxtAttrib); while (screenY <= lastTxtRow) { console.gotoxy(pTopLeftX, screenY++); printf(txtLineFormatStr, ""); } } writeTxtLines = false; // Get a keypress from the user and take action based on it console.gotoxy(pPostWriteCurX, pPostWriteCurY); retObj.lastKeypress = getKeyWithESCChars(K_UPPER|K_NOCRLF|K_NOECHO|K_NOSPIN); switch (retObj.lastKeypress) { case KEY_UP: if (retObj.topLineIdx > 0) { --retObj.topLineIdx; writeTxtLines = true; } break; case KEY_DOWN: if (retObj.topLineIdx < topLineIdxForLastPage) { ++retObj.topLineIdx; writeTxtLines = true; } break; case KEY_PAGE_DOWN: // Next page if (retObj.topLineIdx < topLineIdxForLastPage) { retObj.topLineIdx += pHeight; if (retObj.topLineIdx > topLineIdxForLastPage) retObj.topLineIdx = topLineIdxForLastPage; writeTxtLines = true; } break; case KEY_PAGE_UP: // Previous page if (retObj.topLineIdx > 0) { retObj.topLineIdx -= pHeight; if (retObj.topLineIdx < 0) retObj.topLineIdx = 0; writeTxtLines = true; } break; case KEY_HOME: // First page if (retObj.topLineIdx > 0) { retObj.topLineIdx = 0; writeTxtLines = true; } break; case KEY_END: // Last page if (retObj.topLineIdx < topLineIdxForLastPage) { retObj.topLineIdx = topLineIdxForLastPage; writeTxtLines = true; } break; default: continueOn = false; break; } } return retObj; } // Displays a Frame on the screen and allows scrolling through it with the up & // down arrow keys, PageUp, PageDown, HOME, and END. // // Parameters: // pFrame: A Frame object to display & scroll through // pScrollbar: A ScrollBar object associated with the Frame object // pTopLineIdx: The index of the text line to display at the top // pTxtAttrib: The attribute(s) to apply to the text lines // pWriteTxtLines: Boolean - Whether or not to write the text lines (in addition // to doing the message loop). If false, this will only do the // the message loop. This parameter is intended as a screen // refresh optimization. // pPostWriteCurX: The X location for the cursor after writing the message // lines // pPostWriteCurY: The Y location for the cursor after writing the message // lines // pScrollUpdateFn: A function that the caller can provide for updating the // scroll position. This function has one parameter: // - fractionToLastPage: The fraction of the top index divided // by the top index for the last page (basically, the progress // to the last page). // // Return value: An object with the following properties: // lastKeypress: The last key pressed by the user (a string) // topLineIdx: The new top line index of the text lines, in case of scrolling function scrollFrame(pFrame, pScrollbar, pTopLineIdx, pTxtAttrib, pWriteTxtLines, pPostWriteCurX, pPostWriteCurY, pScrollUpdateFn) { // Variables for the top line index for the last page, scrolling, etc. var topLineIdxForLastPage = pFrame.data_height - pFrame.height; if (topLineIdxForLastPage < 0) topLineIdxForLastPage = 0; var retObj = new Object(); retObj.lastKeypress = ""; retObj.topLineIdx = pTopLineIdx; if (pTopLineIdx > 0) pFrame.scrollTo(0, pTopLineIdx); var writeTxtLines = pWriteTxtLines; if (writeTxtLines) { pFrame.invalidate(); // Force drawing on the next call to draw() or cycle() pFrame.cycle(); //pFrame.draw(); } var cycleFrame = true; var continueOn = true; while (continueOn) { // If we are to write the text lines, then draw the frame. // TODO: Do we really need this? Will this be different from // scrollTextLines()? //if (writeTxtLines) // pFrame.draw(); if (cycleFrame) { // Invalidate the frame to force it to redraw everything, as a // workaround to clear the background before writing again // TODO: I might want to remove this invalidate() later when // Frame is fixed to redraw better on scrolling. pFrame.invalidate(); // Cycle the scrollbar & frame to get them to scroll if (pScrollbar != null) pScrollbar.cycle(); pFrame.cycle(); } writeTxtLines = false; cycleFrame = false; // Get a keypress from the user and take action based on it console.gotoxy(pPostWriteCurX, pPostWriteCurY); retObj.lastKeypress = getKeyWithESCChars(K_UPPER|K_NOCRLF|K_NOECHO|K_NOSPIN); switch (retObj.lastKeypress) { case KEY_UP: if (retObj.topLineIdx > 0) { pFrame.scroll(0, -1); --retObj.topLineIdx; cycleFrame = true; writeTxtLines = true; } break; case KEY_DOWN: if (retObj.topLineIdx < topLineIdxForLastPage) { pFrame.scroll(0, 1); cycleFrame = true; ++retObj.topLineIdx; writeTxtLines = true; } break; case KEY_PAGE_DOWN: // Next page if (retObj.topLineIdx < topLineIdxForLastPage) { //pFrame.scroll(0, pFrame.height); retObj.topLineIdx += pFrame.height; if (retObj.topLineIdx > topLineIdxForLastPage) retObj.topLineIdx = topLineIdxForLastPage; pFrame.scrollTo(1, retObj.topLineIdx+1); cycleFrame = true; writeTxtLines = true; } break; case KEY_PAGE_UP: // Previous page if (retObj.topLineIdx > 0) { //pFrame.scroll(0, -(pFrame.height)); retObj.topLineIdx -= pFrame.height; if (retObj.topLineIdx < 0) retObj.topLineIdx = 0; pFrame.scrollTo(1, retObj.topLineIdx+1); cycleFrame = true; writeTxtLines = true; } break; case KEY_HOME: // First page //pFrame.home(); pFrame.scrollTo(1, 1); cycleFrame = true; retObj.topLineIdx = 0; break; case KEY_END: // Last page //pFrame.end(); pFrame.scrollTo(1, topLineIdxForLastPage+1); cycleFrame = true; retObj.topLineIdx = topLineIdxForLastPage; break; default: continueOn = false; break; } } return retObj; } // Finds the (1-based) page number of an item by number (1-based). If no page // is found, then the return value will be 0. // // Parameters: // pItemNum: The item number (1-based) // pNumPerPage: The number of items per page // pTotoalNum: The total number of items in the list // pReverseOrder: Boolean - Whether or not the list is in reverse order. If not specified, // this will default to false. // // Return value: The page number (1-based) of the item number. If no page is found, // the return value will be 0. function findPageNumOfItemNum(pItemNum, pNumPerPage, pTotalNum, pReverseOrder) { if ((typeof(pItemNum) != "number") || (typeof(pNumPerPage) != "number") || (typeof(pTotalNum) != "number")) return 0; if ((pItemNum < 1) || (pItemNum > pTotalNum)) return 0; var reverseOrder = (typeof(pReverseOrder) == "boolean" ? pReverseOrder : false); var itemPageNum = 0; if (reverseOrder) { var pageNum = 1; for (var topNum = pTotalNum; ((topNum > 0) && (itemPageNum == 0)); topNum -= pNumPerPage) { if ((pItemNum <= topNum) && (pItemNum >= topNum-pNumPerPage+1)) itemPageNum = pageNum; ++pageNum; } } else // Forward order itemPageNum = Math.ceil(pItemNum / pNumPerPage); return itemPageNum; } // This function converts a search mode string to one of the defined search value // constants. If the passed-in mode string is unknown, then the return value will // be SEARCH_NONE (-1). // // Parameters: // pSearchTypeStr: A string describing a search mode ("keyword_search", "from_name_search", // "to_name_search", "to_user_search", "new_msg_scan", "new_msg_scan_cur_sub", // "new_msg_scan_cur_grp", "new_msg_scan_all", "to_user_new_scan", // "to_user_all_scan") // // Return value: An integer representing the search value (SEARCH_KEYWORD, // SEARCH_FROM_NAME, SEARCH_TO_NAME_CUR_MSG_AREA, // SEARCH_TO_USER_CUR_MSG_AREA), or SEARCH_NONE (-1) if the passed-in // search type string is unknown. function searchTypeStrToVal(pSearchTypeStr) { if (typeof(pSearchTypeStr) != "string") return SEARCH_NONE; var searchTypeInt = SEARCH_NONE; var modeStr = pSearchTypeStr.toLowerCase(); if (modeStr == "keyword_search") searchTypeInt = SEARCH_KEYWORD; else if (modeStr == "from_name_search") searchTypeInt = SEARCH_FROM_NAME; else if (modeStr == "to_name_search") searchTypeInt = SEARCH_TO_NAME_CUR_MSG_AREA; else if (modeStr == "to_user_search") searchTypeInt = SEARCH_TO_USER_CUR_MSG_AREA; else if (modeStr == "new_msg_scan") searchTypeInt = SEARCH_MSG_NEWSCAN; else if (modeStr == "new_msg_scan_cur_sub") searchTypeInt = SEARCH_MSG_NEWSCAN_CUR_SUB; else if (modeStr == "new_msg_scan_cur_grp") searchTypeInt = SEARCH_MSG_NEWSCAN_CUR_GRP; else if (modeStr == "new_msg_scan_all") searchTypeInt = SEARCH_MSG_NEWSCAN_ALL; else if (modeStr == "to_user_new_scan") searchTypeInt = SEARCH_TO_USER_NEW_SCAN; else if (modeStr == "to_user_new_scan_cur_sub") searchTypeInt = SEARCH_TO_USER_NEW_SCAN_CUR_SUB; else if (modeStr == "to_user_new_scan_cur_grp") searchTypeInt = SEARCH_TO_USER_NEW_SCAN_CUR_GRP; else if (modeStr == "to_user_new_scan_all") searchTypeInt = SEARCH_TO_USER_NEW_SCAN_ALL; else if (modeStr == "to_user_all_scan") searchTypeInt = SEARCH_ALL_TO_USER_SCAN; return searchTypeInt; } // This function converts a search type value to a string description. // // Parameters: // pSearchType: The search type value to convert // // Return value: A string describing the search type value function searchTypeValToStr(pSearchType) { if (typeof(pSearchType) != "number") return "Unknown (not a number)"; var searchTypeStr = ""; switch (pSearchType) { case SEARCH_NONE: searchTypeStr = "None (SEARCH_NONE)"; break; case SEARCH_KEYWORD: searchTypeStr = "Keyword (SEARCH_KEYWORD)"; break; case SEARCH_FROM_NAME: searchTypeStr = "'From' name (SEARCH_FROM_NAME)"; break; case SEARCH_TO_NAME_CUR_MSG_AREA: searchTypeStr = "'To' name (SEARCH_TO_NAME_CUR_MSG_AREA)"; break; case SEARCH_TO_USER_CUR_MSG_AREA: searchTypeStr = "To you (SEARCH_TO_USER_CUR_MSG_AREA)"; break; case SEARCH_MSG_NEWSCAN: searchTypeStr = "New message scan (SEARCH_MSG_NEWSCAN)"; break; case SEARCH_MSG_NEWSCAN_CUR_SUB: searchTypeStr = "New in current message area (SEARCH_MSG_NEWSCAN_CUR_SUB)"; break; case SEARCH_MSG_NEWSCAN_CUR_GRP: searchTypeStr = "New in current message group (SEARCH_MSG_NEWSCAN_CUR_GRP)"; break; case SEARCH_MSG_NEWSCAN_ALL: searchTypeStr = "Newscan - All (SEARCH_MSG_NEWSCAN_ALL)"; break; case SEARCH_TO_USER_NEW_SCAN: searchTypeStr = "To You new scan (SEARCH_TO_USER_NEW_SCAN)"; break; case SEARCH_TO_USER_NEW_SCAN_CUR_SUB: searchTypeStr = "To You new scan, current sub-board (SEARCH_TO_USER_NEW_SCAN_CUR_SUB)"; break; case SEARCH_TO_USER_NEW_SCAN_CUR_GRP: searchTypeStr = "To You new scan, current group (SEARCH_TO_USER_NEW_SCAN_CUR_GRP)"; break; case SEARCH_TO_USER_NEW_SCAN_ALL: searchTypeStr = "To You new scan, all sub-boards (SEARCH_TO_USER_NEW_SCAN_ALL)"; break; case SEARCH_ALL_TO_USER_SCAN: searchTypeStr = "All To You scan (SEARCH_ALL_TO_USER_SCAN)"; break; default: searchTypeStr = "Unknown (" + pSearchType + ")"; break; } return searchTypeStr; } // This function converts a reader mode string to one of the defined reader mode // value constants. If the passed-in mode string is unknown, then the return value // will be -1. // // Parameters: // pModeStr: A string describing a reader mode ("read", "reader", "list", "lister") // // Return value: An integer representing the reader mode value (READER_MODE_READ, // READER_MODE_LIST), or -1 if the passed-in mode string is unknown. function readerModeStrToVal(pModeStr) { if (typeof(pModeStr) != "string") return -1; var readerModeInt = -1; var modeStr = pModeStr.toLowerCase(); if ((modeStr == "read") || (modeStr == "reader")) readerModeInt = READER_MODE_READ; else if ((modeStr == "list") || (modeStr == "lister")) readerModeInt = READER_MODE_LIST; return readerModeInt; } // This function returns a boolean to signify whether or not the user's // terminal supports both high-ASCII characters and ANSI codes. function canDoHighASCIIAndANSI() { //return (console.term_supports(USER_ANSI) && (user.settings & USER_NO_EXASCII == 0)); return (console.term_supports(USER_ANSI)); } // Searches a given range in an open message base and returns an object with arrays // containing the message headers (0-based indexed and indexed by message number) // with the message headers of any found messages. // // Parameters: // pSubCode: The internal code of the message sub-board // pMsgbase: A message base object in which to search messages // pSearchType: The type of search to do (one of the SEARCH_ values) // pSearchString: The string to search for. // pListingPersonalEmailFromUser: Optional boolean - Whether or not we're listing // personal email sent by the user. This defaults // to false. // pStartIndex: The starting message index (0-based). Optional; defaults to 0. // pEndIndex: One past the last message index. Optional; defaults to the total number // of messages. // // Return value: An object with the following arrays: // indexed: A 0-based indexed array of message headers function searchMsgbase(pSubCode, pMsgbase, pSearchType, pSearchString, pListingPersonalEmailFromUser, pStartIndex, pEndIndex) { var msgHeaders = new Object(); msgHeaders.indexed = new Array(); if ((pSubCode != "mail") && ((typeof(pSearchString) != "string") || !searchTypePopulatesSearchResults(pSearchType))) return msgHeaders; var startMsgIndex = 0; var endMsgIndex = pMsgbase.total_msgs; if (typeof(pStartIndex) == "number") { if ((pStartIndex >= 0) && (pStartIndex < pMsgbase.total_msgs)) startMsgIndex = pStartIndex; } if (typeof(pEndIndex) == "number") { if ((pEndIndex >= 0) && (pEndIndex > startMsgIndex) && (pEndIndex <= pMsgbase.total_msgs)) endMsgIndex = pEndIndex; } // Define a search function for the message field we're going to search var readingPersonalEmailFromUser = (typeof(pListingPersonalEmailFromUser) == "boolean" ? pListingPersonalEmailFromUser : false); var matchFn = null; switch (pSearchType) { // It might seem odd to have SEARCH_NONE in here, but it's here because // when reading personal email, we need to search for messages only to // the current user. case SEARCH_NONE: if (pSubCode == "mail") { // Set up the match function slightly differently depending on whether // we're looking for mail from the current user or to the current user. if (readingPersonalEmailFromUser) { matchFn = function(pSearchStr, pMsgHdr, pMsgBase, pSubBoardCode) { var msgText = strip_ctrl(pMsgBase.get_msg_body(true, pMsgHdr.offset)); return gAllPersonalEmailOptSpecified || msgIsFromUser(pMsgHdr); } } else { matchFn = function(pSearchStr, pMsgHdr, pMsgBase, pSubBoardCode) { var msgText = strip_ctrl(pMsgBase.get_msg_body(true, pMsgHdr.offset)); return gAllPersonalEmailOptSpecified || msgIsToUserByNum(pMsgHdr); } } } break; case SEARCH_KEYWORD: matchFn = function(pSearchStr, pMsgHdr, pMsgBase, pSubBoardCode) { var msgText = strip_ctrl(pMsgBase.get_msg_body(true, pMsgHdr.offset)); var keywordFound = ((pMsgHdr.subject.toUpperCase().indexOf(pSearchStr) > -1) || (msgText.toUpperCase().indexOf(pSearchStr) > -1)); if (pSubBoardCode == "mail") return keywordFound && msgIsToUserByNum(pMsgHdr); else return keywordFound; } break; case SEARCH_FROM_NAME: matchFn = function(pSearchStr, pMsgHdr, pMsgBase, pSubBoardCode) { var fromNameFound = (pMsgHdr.from.toUpperCase() == pSearchStr.toUpperCase()); if (pSubBoardCode == "mail") return fromNameFound && (gAllPersonalEmailOptSpecified || msgIsToUserByNum(pMsgHdr)); else return fromNameFound; } break; case SEARCH_TO_NAME_CUR_MSG_AREA: matchFn = function(pSearchStr, pMsgHdr, pMsgBase, pSubBoardCode) { return (pMsgHdr.to.toUpperCase() == pSearchStr); } break; case SEARCH_TO_USER_CUR_MSG_AREA: case SEARCH_ALL_TO_USER_SCAN: matchFn = function(pSearchStr, pMsgHdr, pMsgBase, pSubBoardCode) { // See if the message is not marked as deleted and the 'To' name // matches the user's handle, alias, and/or username. return (((pMsgHdr.attr & MSG_DELETE) == 0) && userNameHandleAliasMatch(pMsgHdr.to)); } break; case SEARCH_TO_USER_NEW_SCAN: case SEARCH_TO_USER_NEW_SCAN_CUR_SUB: case SEARCH_TO_USER_NEW_SCAN_CUR_GRP: case SEARCH_TO_USER_NEW_SCAN_ALL: if (pSubCode != "mail") { // If pStartIndex or pEndIndex aren't specified, then set // startMsgIndex to the scan pointer and endMsgIndex to one // past the index of the last message in the sub-board if (typeof(pStartIndex) != "number") { // First, write some messages to the log if verbose logging is enabled if (gCmdLineArgVals.verboselogging) { writeToSysAndNodeLog("New-to-user scan for " + subBoardGrpAndName(pSubCode) + " -- Scan pointer: " + msg_area.sub[pSubCode].scan_ptr); } //startMsgIndex = absMsgNumToIdx(pMsgbase, msg_area.sub[pSubCode].last_read); startMsgIndex = absMsgNumToIdx(pMsgbase, msg_area.sub[pSubCode].scan_ptr); if (startMsgIndex == -1) { msg_area.sub[pSubCode].scan_ptr = 0; startMsgIndex = 0; } else { var startMsgHeader = pMsgbase.get_msg_header(true, startMsgIndex, true); if ((startMsgHeader.attr & MSG_READ) == MSG_READ) ++startMsgIndex; } } if (typeof(pEndIndex) != "number") endMsgIndex = (pMsgbase.total_msgs > 0 ? pMsgbase.total_msgs : 0); } matchFn = function(pSearchStr, pMsgHdr, pMsgBase, pSubBoardCode) { // Note: This assumes pSubBoardCode is not "mail" (personal mail). // See if the message 'To' name matches the user's handle, alias, // and/or username and is not marked as deleted and is unread. return (((pMsgHdr.attr & MSG_DELETE) == 0) && ((pMsgHdr.attr & MSG_READ) == 0) && userNameHandleAliasMatch(pMsgHdr.to)); } break; case SEARCH_MSG_NEWSCAN: case SEARCH_MSG_NEWSCAN_CUR_SUB: case SEARCH_MSG_NEWSCAN_CUR_GRP: case SEARCH_MSG_NEWSCAN_ALL: matchFn = function(pSearchStr, pMsgHdr, pMsgBase, pSubBoardCode) { // Note: This assumes pSubBoardCode is not "mail" (personal mail). // Get the offset of the last read message and compare it with the // offset of the given message header var lastReadMsgHdr = pMsgBase.get_msg_header(false, msg_area.sub[pSubBoardCode].last_read, true); var lastReadMsgOffset = (lastReadMsgHdr != null ? lastReadMsgHdr.offset : 0); return (pMsgHdr.offset > lastReadMsgOffset); } break; } // Search the messages if (matchFn != null) { for (var msgIdx = startMsgIndex; msgIdx < endMsgIndex; ++msgIdx) { var msgHeader = pMsgbase.get_msg_header(true, msgIdx, true); // I've seen situations where the message header object is null for // some reason, so check that before running the search function. if (msgHeader != null) { if (matchFn(pSearchString, msgHeader, pMsgbase, pSubCode)) msgHeaders.indexed.push(msgHeader); } } } return msgHeaders; } // Returns whether or not a message is to the current user (either the current // logged-in user or the user specified by the userNum command-line argument) // and is not deleted. // // Parameters: // pMsgHdr: A message header object // // Return value: Boolean - Whether or not the message is to the user and is not // deleted. function msgIsToUserByNum(pMsgHdr) { if (typeof(pMsgHdr) != "object") return false; // Return false if the message is marked as deleted if ((pMsgHdr.attr & MSG_DELETE) == MSG_DELETE) return false; var msgIsToUser = false; // If an alternate user number was specified on the command line, then use that // user information. Otherwise, use the current logged-in user. if (gCmdLineArgVals.hasOwnProperty("altUserNum")) msgIsToUser = (pMsgHdr.to_ext == gCmdLineArgVals.altUserNum); else msgIsToUser = (pMsgHdr.to_ext == user.number); return msgIsToUser; } // Returns whether or not a message is from the current user (either the current // logged-in user or the user specified by the userNum command-line argument) // and is not deleted. // // Parameters: // pMsgHdr: A message header object // // Return value: Boolean - Whether or not the message is from the logged-in user // and is not deleted. function msgIsFromUser(pMsgHdr) { if (typeof(pMsgHdr) != "object") return false; // Return false if the message is marked as deleted if ((pMsgHdr.attr & MSG_DELETE) == MSG_DELETE) return false; var isFromUser = false; // If an alternate user number was specified on the command line, then use that // user information. Otherwise, use the current logged-in user. if (pMsgHdr.hasOwnProperty("from_ext")) { if (gCmdLineArgVals.hasOwnProperty("altUserNum")) isFromUser = (pMsgHdr.from_ext == gCmdLineArgVals.altUserNum); else isFromUser = (pMsgHdr.from_ext == user.number); } else { var hdrFromUpper = pMsgHdr.from.toUpperCase(); if (gCmdLineArgVals.hasOwnProperty("altUserName") && gCmdLineArgVals.hasOwnProperty("altUserAlias")) isFromUser = ((hdrFromUpper == gCmdLineArgVals.altUserAlias.toUpperCase()) || (hdrFromUpper == gCmdLineArgVals.altUserName.toUpperCase())); else isFromUser = ((hdrFromUpper == user.alias.toUpperCase()) || (hdrFromUpper == user.name.toUpperCase())); } return isFromUser; } ///////////////////////////////////////////////////////////////////////// // Functions for converting other BBS color codes to Synchronet attribute codes // Converts WWIV attribute codes to Synchronet attribute codes. // // Parameters: // pText: A string containing the text to convert // // Return value: The text with the color codes converted function WWIVAttrsToSyncAttrs(pText) { // First, see if the text has any WWIV-style attribute codes at // all. We'll be performing a bunch of search & replace commands, // so we don't want to do all that work for nothing.. :) if (/\x03[0-9]/.test(pText)) { var text = pText.replace(/\x030/g, "\1n"); // Normal text = text.replace(/\x031/g, "\1n\1c\1h"); // Bright cyan text = text.replace(/\x032/g, "\1n\1y\1h"); // Bright yellow text = text.replace(/\x033/g, "\1n\1m"); // Magenta text = text.replace(/\x034/g, "\1n\1h\1w\1" + "4"); // Bright white on blue text = text.replace(/\x035/g, "\1n\1g"); // Green text = text.replace(/\x036/g, "\1h\1r\1i"); // Bright red, blinking text = text.replace(/\x037/g, "\1n\1h\1b"); // Bright blue text = text.replace(/\x038/g, "\1n\1b"); // Blue text = text.replace(/\x039/g, "\1n\1c"); // Cyan return text; } else return pText; // No WWIV-style color attribute found, so just return the text. } // Converts PCBoard attribute codes to Synchronet attribute codes. // // Parameters: // pText: A string containing the text to convert // // Return value: The text with the color codes converted function PCBoardAttrsToSyncAttrs(pText) { // First, see if the text has any PCBoard-style attribute codes at // all. We'll be performing a bunch of search & replace commands, // so we don't want to do all that work for nothing.. :) if (/@[xX][0-9A-Fa-f]{2}/.test(pText)) { // Black background var text = pText.replace(/@[xX]00/g, "\1n\1k\1" + "0"); // Black on black text = text.replace(/@[xX]01/g, "\1n\1b\1" + "0"); // Blue on black text = text.replace(/@[xX]02/g, "\1n\1g\1" + "0"); // Green on black text = text.replace(/@[xX]03/g, "\1n\1c\1" + "0"); // Cyan on black text = text.replace(/@[xX]04/g, "\1n\1r\1" + "0"); // Red on black text = text.replace(/@[xX]05/g, "\1n\1m\1" + "0"); // Magenta on black text = text.replace(/@[xX]06/g, "\1n\1y\1" + "0"); // Yellow/brown on black text = text.replace(/@[xX]07/g, "\1n\1w\1" + "0"); // White on black text = text.replace(/@[xX]08/g, "\1n\1w\1" + "0"); // White on black text = text.replace(/@[xX]09/g, "\1n\1w\1" + "0"); // White on black text = text.replace(/@[xX]08/g, "\1h\1k\1" + "0"); // Bright black on black text = text.replace(/@[xX]09/g, "\1h\1b\1" + "0"); // Bright blue on black text = text.replace(/@[xX]0[Aa]/g, "\1h\1g\1" + "0"); // Bright green on black text = text.replace(/@[xX]0[Bb]/g, "\1h\1c\1" + "0"); // Bright cyan on black text = text.replace(/@[xX]0[Cc]/g, "\1h\1r\1" + "0"); // Bright red on black text = text.replace(/@[xX]0[Dd]/g, "\1h\1m\1" + "0"); // Bright magenta on black text = text.replace(/@[xX]0[Ee]/g, "\1h\1y\1" + "0"); // Bright yellow on black text = text.replace(/@[xX]0[Ff]/g, "\1h\1w\1" + "0"); // Bright white on black // Blinking foreground // Blue background text = text.replace(/@[xX]10/g, "\1n\1k\1" + "4"); // Black on blue text = text.replace(/@[xX]11/g, "\1n\1b\1" + "4"); // Blue on blue text = text.replace(/@[xX]12/g, "\1n\1g\1" + "4"); // Green on blue text = text.replace(/@[xX]13/g, "\1n\1c\1" + "4"); // Cyan on blue text = text.replace(/@[xX]14/g, "\1n\1r\1" + "4"); // Red on blue text = text.replace(/@[xX]15/g, "\1n\1m\1" + "4"); // Magenta on blue text = text.replace(/@[xX]16/g, "\1n\1y\1" + "4"); // Yellow/brown on blue text = text.replace(/@[xX]17/g, "\1n\1w\1" + "4"); // White on blue text = text.replace(/@[xX]18/g, "\1h\1k\1" + "4"); // Bright black on blue text = text.replace(/@[xX]19/g, "\1h\1b\1" + "4"); // Bright blue on blue text = text.replace(/@[xX]1[Aa]/g, "\1h\1g\1" + "4"); // Bright green on blue text = text.replace(/@[xX]1[Bb]/g, "\1h\1c\1" + "4"); // Bright cyan on blue text = text.replace(/@[xX]1[Cc]/g, "\1h\1r\1" + "4"); // Bright red on blue text = text.replace(/@[xX]1[Dd]/g, "\1h\1m\1" + "4"); // Bright magenta on blue text = text.replace(/@[xX]1[Ee]/g, "\1h\1y\1" + "4"); // Bright yellow on blue text = text.replace(/@[xX]1[Ff]/g, "\1h\1w\1" + "4"); // Bright white on blue // Green background text = text.replace(/@[xX]20/g, "\1n\1k\1" + "2"); // Black on green text = text.replace(/@[xX]21/g, "\1n\1b\1" + "2"); // Blue on green text = text.replace(/@[xX]22/g, "\1n\1g\1" + "2"); // Green on green text = text.replace(/@[xX]23/g, "\1n\1c\1" + "2"); // Cyan on green text = text.replace(/@[xX]24/g, "\1n\1r\1" + "2"); // Red on green text = text.replace(/@[xX]25/g, "\1n\1m\1" + "2"); // Magenta on green text = text.replace(/@[xX]26/g, "\1n\1y\1" + "2"); // Yellow/brown on green text = text.replace(/@[xX]27/g, "\1n\1w\1" + "2"); // White on green text = text.replace(/@[xX]28/g, "\1h\1k\1" + "2"); // Bright black on green text = text.replace(/@[xX]29/g, "\1h\1b\1" + "2"); // Bright blue on green text = text.replace(/@[xX]2[Aa]/g, "\1h\1g\1" + "2"); // Bright green on green text = text.replace(/@[xX]2[Bb]/g, "\1h\1c\1" + "2"); // Bright cyan on green text = text.replace(/@[xX]2[Cc]/g, "\1h\1r\1" + "2"); // Bright red on green text = text.replace(/@[xX]2[Dd]/g, "\1h\1m\1" + "2"); // Bright magenta on green text = text.replace(/@[xX]2[Ee]/g, "\1h\1y\1" + "2"); // Bright yellow on green text = text.replace(/@[xX]2[Ff]/g, "\1h\1w\1" + "2"); // Bright white on green // Cyan background text = text.replace(/@[xX]30/g, "\1n\1k\1" + "6"); // Black on cyan text = text.replace(/@[xX]31/g, "\1n\1b\1" + "6"); // Blue on cyan text = text.replace(/@[xX]32/g, "\1n\1g\1" + "6"); // Green on cyan text = text.replace(/@[xX]33/g, "\1n\1c\1" + "6"); // Cyan on cyan text = text.replace(/@[xX]34/g, "\1n\1r\1" + "6"); // Red on cyan text = text.replace(/@[xX]35/g, "\1n\1m\1" + "6"); // Magenta on cyan text = text.replace(/@[xX]36/g, "\1n\1y\1" + "6"); // Yellow/brown on cyan text = text.replace(/@[xX]37/g, "\1n\1w\1" + "6"); // White on cyan text = text.replace(/@[xX]38/g, "\1h\1k\1" + "6"); // Bright black on cyan text = text.replace(/@[xX]39/g, "\1h\1b\1" + "6"); // Bright blue on cyan text = text.replace(/@[xX]3[Aa]/g, "\1h\1g\1" + "6"); // Bright green on cyan text = text.replace(/@[xX]3[Bb]/g, "\1h\1c\1" + "6"); // Bright cyan on cyan text = text.replace(/@[xX]3[Cc]/g, "\1h\1r\1" + "6"); // Bright red on cyan text = text.replace(/@[xX]3[Dd]/g, "\1h\1m\1" + "6"); // Bright magenta on cyan text = text.replace(/@[xX]3[Ee]/g, "\1h\1y\1" + "6"); // Bright yellow on cyan text = text.replace(/@[xX]3[Ff]/g, "\1h\1w\1" + "6"); // Bright white on cyan // Red background text = text.replace(/@[xX]40/g, "\1n\1k\1" + "1"); // Black on red text = text.replace(/@[xX]41/g, "\1n\1b\1" + "1"); // Blue on red text = text.replace(/@[xX]42/g, "\1n\1g\1" + "1"); // Green on red text = text.replace(/@[xX]43/g, "\1n\1c\1" + "1"); // Cyan on red text = text.replace(/@[xX]44/g, "\1n\1r\1" + "1"); // Red on red text = text.replace(/@[xX]45/g, "\1n\1m\1" + "1"); // Magenta on red text = text.replace(/@[xX]46/g, "\1n\1y\1" + "1"); // Yellow/brown on red text = text.replace(/@[xX]47/g, "\1n\1w\1" + "1"); // White on red text = text.replace(/@[xX]48/g, "\1h\1k\1" + "1"); // Bright black on red text = text.replace(/@[xX]49/g, "\1h\1b\1" + "1"); // Bright blue on red text = text.replace(/@[xX]4[Aa]/g, "\1h\1g\1" + "1"); // Bright green on red text = text.replace(/@[xX]4[Bb]/g, "\1h\1c\1" + "1"); // Bright cyan on red text = text.replace(/@[xX]4[Cc]/g, "\1h\1r\1" + "1"); // Bright red on red text = text.replace(/@[xX]4[Dd]/g, "\1h\1m\1" + "1"); // Bright magenta on red text = text.replace(/@[xX]4[Ee]/g, "\1h\1y\1" + "1"); // Bright yellow on red text = text.replace(/@[xX]4[Ff]/g, "\1h\1w\1" + "1"); // Bright white on red // Magenta background text = text.replace(/@[xX]50/g, "\1n\1k\1" + "5"); // Black on magenta text = text.replace(/@[xX]51/g, "\1n\1b\1" + "5"); // Blue on magenta text = text.replace(/@[xX]52/g, "\1n\1g\1" + "5"); // Green on magenta text = text.replace(/@[xX]53/g, "\1n\1c\1" + "5"); // Cyan on magenta text = text.replace(/@[xX]54/g, "\1n\1r\1" + "5"); // Red on magenta text = text.replace(/@[xX]55/g, "\1n\1m\1" + "5"); // Magenta on magenta text = text.replace(/@[xX]56/g, "\1n\1y\1" + "5"); // Yellow/brown on magenta text = text.replace(/@[xX]57/g, "\1n\1w\1" + "5"); // White on magenta text = text.replace(/@[xX]58/g, "\1h\1k\1" + "5"); // Bright black on magenta text = text.replace(/@[xX]59/g, "\1h\1b\1" + "5"); // Bright blue on magenta text = text.replace(/@[xX]5[Aa]/g, "\1h\1g\1" + "5"); // Bright green on magenta text = text.replace(/@[xX]5[Bb]/g, "\1h\1c\1" + "5"); // Bright cyan on magenta text = text.replace(/@[xX]5[Cc]/g, "\1h\1r\1" + "5"); // Bright red on magenta text = text.replace(/@[xX]5[Dd]/g, "\1h\1m\1" + "5"); // Bright magenta on magenta text = text.replace(/@[xX]5[Ee]/g, "\1h\1y\1" + "5"); // Bright yellow on magenta text = text.replace(/@[xX]5[Ff]/g, "\1h\1w\1" + "5"); // Bright white on magenta // Brown background text = text.replace(/@[xX]60/g, "\1n\1k\1" + "3"); // Black on brown text = text.replace(/@[xX]61/g, "\1n\1b\1" + "3"); // Blue on brown text = text.replace(/@[xX]62/g, "\1n\1g\1" + "3"); // Green on brown text = text.replace(/@[xX]63/g, "\1n\1c\1" + "3"); // Cyan on brown text = text.replace(/@[xX]64/g, "\1n\1r\1" + "3"); // Red on brown text = text.replace(/@[xX]65/g, "\1n\1m\1" + "3"); // Magenta on brown text = text.replace(/@[xX]66/g, "\1n\1y\1" + "3"); // Yellow/brown on brown text = text.replace(/@[xX]67/g, "\1n\1w\1" + "3"); // White on brown text = text.replace(/@[xX]68/g, "\1h\1k\1" + "3"); // Bright black on brown text = text.replace(/@[xX]69/g, "\1h\1b\1" + "3"); // Bright blue on brown text = text.replace(/@[xX]6[Aa]/g, "\1h\1g\1" + "3"); // Bright breen on brown text = text.replace(/@[xX]6[Bb]/g, "\1h\1c\1" + "3"); // Bright cyan on brown text = text.replace(/@[xX]6[Cc]/g, "\1h\1r\1" + "3"); // Bright red on brown text = text.replace(/@[xX]6[Dd]/g, "\1h\1m\1" + "3"); // Bright magenta on brown text = text.replace(/@[xX]6[Ee]/g, "\1h\1y\1" + "3"); // Bright yellow on brown text = text.replace(/@[xX]6[Ff]/g, "\1h\1w\1" + "3"); // Bright white on brown // White background text = text.replace(/@[xX]70/g, "\1n\1k\1" + "7"); // Black on white text = text.replace(/@[xX]71/g, "\1n\1b\1" + "7"); // Blue on white text = text.replace(/@[xX]72/g, "\1n\1g\1" + "7"); // Green on white text = text.replace(/@[xX]73/g, "\1n\1c\1" + "7"); // Cyan on white text = text.replace(/@[xX]74/g, "\1n\1r\1" + "7"); // Red on white text = text.replace(/@[xX]75/g, "\1n\1m\1" + "7"); // Magenta on white text = text.replace(/@[xX]76/g, "\1n\1y\1" + "7"); // Yellow/brown on white text = text.replace(/@[xX]77/g, "\1n\1w\1" + "7"); // White on white text = text.replace(/@[xX]78/g, "\1h\1k\1" + "7"); // Bright black on white text = text.replace(/@[xX]79/g, "\1h\1b\1" + "7"); // Bright blue on white text = text.replace(/@[xX]7[Aa]/g, "\1h\1g\1" + "7"); // Bright green on white text = text.replace(/@[xX]7[Bb]/g, "\1h\1c\1" + "7"); // Bright cyan on white text = text.replace(/@[xX]7[Cc]/g, "\1h\1r\1" + "7"); // Bright red on white text = text.replace(/@[xX]7[Dd]/g, "\1h\1m\1" + "7"); // Bright magenta on white text = text.replace(/@[xX]7[Ee]/g, "\1h\1y\1" + "7"); // Bright yellow on white text = text.replace(/@[xX]7[Ff]/g, "\1h\1w\1" + "7"); // Bright white on white // Black background, blinking foreground text = text.replace(/@[xX]80/g, "\1n\1k\1" + "0\1i"); // Blinking black on black text = text.replace(/@[xX]81/g, "\1n\1b\1" + "0\1i"); // Blinking blue on black text = text.replace(/@[xX]82/g, "\1n\1g\1" + "0\1i"); // Blinking green on black text = text.replace(/@[xX]83/g, "\1n\1c\1" + "0\1i"); // Blinking cyan on black text = text.replace(/@[xX]84/g, "\1n\1r\1" + "0\1i"); // Blinking red on black text = text.replace(/@[xX]85/g, "\1n\1m\1" + "0\1i"); // Blinking magenta on black text = text.replace(/@[xX]86/g, "\1n\1y\1" + "0\1i"); // Blinking yellow/brown on black text = text.replace(/@[xX]87/g, "\1n\1w\1" + "0\1i"); // Blinking white on black text = text.replace(/@[xX]88/g, "\1h\1k\1" + "0\1i"); // Blinking bright black on black text = text.replace(/@[xX]89/g, "\1h\1b\1" + "0\1i"); // Blinking bright blue on black text = text.replace(/@[xX]8[Aa]/g, "\1h\1g\1" + "0\1i"); // Blinking bright green on black text = text.replace(/@[xX]8[Bb]/g, "\1h\1c\1" + "0\1i"); // Blinking bright cyan on black text = text.replace(/@[xX]8[Cc]/g, "\1h\1r\1" + "0\1i"); // Blinking bright red on black text = text.replace(/@[xX]8[Dd]/g, "\1h\1m\1" + "0\1i"); // Blinking bright magenta on black text = text.replace(/@[xX]8[Ee]/g, "\1h\1y\1" + "0\1i"); // Blinking bright yellow on black text = text.replace(/@[xX]8[Ff]/g, "\1h\1w\1" + "0\1i"); // Blinking bright white on black // Blue background, blinking foreground text = text.replace(/@[xX]90/g, "\1n\1k\1" + "4\1i"); // Blinking black on blue text = text.replace(/@[xX]91/g, "\1n\1b\1" + "4\1i"); // Blinking blue on blue text = text.replace(/@[xX]92/g, "\1n\1g\1" + "4\1i"); // Blinking green on blue text = text.replace(/@[xX]93/g, "\1n\1c\1" + "4\1i"); // Blinking cyan on blue text = text.replace(/@[xX]94/g, "\1n\1r\1" + "4\1i"); // Blinking red on blue text = text.replace(/@[xX]95/g, "\1n\1m\1" + "4\1i"); // Blinking magenta on blue text = text.replace(/@[xX]96/g, "\1n\1y\1" + "4\1i"); // Blinking yellow/brown on blue text = text.replace(/@[xX]97/g, "\1n\1w\1" + "4\1i"); // Blinking white on blue text = text.replace(/@[xX]98/g, "\1h\1k\1" + "4\1i"); // Blinking bright black on blue text = text.replace(/@[xX]99/g, "\1h\1b\1" + "4\1i"); // Blinking bright blue on blue text = text.replace(/@[xX]9[Aa]/g, "\1h\1g\1" + "4\1i"); // Blinking bright green on blue text = text.replace(/@[xX]9[Bb]/g, "\1h\1c\1" + "4\1i"); // Blinking bright cyan on blue text = text.replace(/@[xX]9[Cc]/g, "\1h\1r\1" + "4\1i"); // Blinking bright red on blue text = text.replace(/@[xX]9[Dd]/g, "\1h\1m\1" + "4\1i"); // Blinking bright magenta on blue text = text.replace(/@[xX]9[Ee]/g, "\1h\1y\1" + "4\1i"); // Blinking bright yellow on blue text = text.replace(/@[xX]9[Ff]/g, "\1h\1w\1" + "4\1i"); // Blinking bright white on blue // Green background, blinking foreground text = text.replace(/@[xX][Aa]0/g, "\1n\1k\1" + "2\1i"); // Blinking black on green text = text.replace(/@[xX][Aa]1/g, "\1n\1b\1" + "2\1i"); // Blinking blue on green text = text.replace(/@[xX][Aa]2/g, "\1n\1g\1" + "2\1i"); // Blinking green on green text = text.replace(/@[xX][Aa]3/g, "\1n\1c\1" + "2\1i"); // Blinking cyan on green text = text.replace(/@[xX][Aa]4/g, "\1n\1r\1" + "2\1i"); // Blinking red on green text = text.replace(/@[xX][Aa]5/g, "\1n\1m\1" + "2\1i"); // Blinking magenta on green text = text.replace(/@[xX][Aa]6/g, "\1n\1y\1" + "2\1i"); // Blinking yellow/brown on green text = text.replace(/@[xX][Aa]7/g, "\1n\1w\1" + "2\1i"); // Blinking white on green text = text.replace(/@[xX][Aa]8/g, "\1h\1k\1" + "2\1i"); // Blinking bright black on green text = text.replace(/@[xX][Aa]9/g, "\1h\1b\1" + "2\1i"); // Blinking bright blue on green text = text.replace(/@[xX][Aa][Aa]/g, "\1h\1g\1" + "2\1i"); // Blinking bright green on green text = text.replace(/@[xX][Aa][Bb]/g, "\1h\1c\1" + "2\1i"); // Blinking bright cyan on green text = text.replace(/@[xX][Aa][Cc]/g, "\1h\1r\1" + "2\1i"); // Blinking bright red on green text = text.replace(/@[xX][Aa][Dd]/g, "\1h\1m\1" + "2\1i"); // Blinking bright magenta on green text = text.replace(/@[xX][Aa][Ee]/g, "\1h\1y\1" + "2\1i"); // Blinking bright yellow on green text = text.replace(/@[xX][Aa][Ff]/g, "\1h\1w\1" + "2\1i"); // Blinking bright white on green // Cyan background, blinking foreground text = text.replace(/@[xX][Bb]0/g, "\1n\1k\1" + "6\1i"); // Blinking black on cyan text = text.replace(/@[xX][Bb]1/g, "\1n\1b\1" + "6\1i"); // Blinking blue on cyan text = text.replace(/@[xX][Bb]2/g, "\1n\1g\1" + "6\1i"); // Blinking green on cyan text = text.replace(/@[xX][Bb]3/g, "\1n\1c\1" + "6\1i"); // Blinking cyan on cyan text = text.replace(/@[xX][Bb]4/g, "\1n\1r\1" + "6\1i"); // Blinking red on cyan text = text.replace(/@[xX][Bb]5/g, "\1n\1m\1" + "6\1i"); // Blinking magenta on cyan text = text.replace(/@[xX][Bb]6/g, "\1n\1y\1" + "6\1i"); // Blinking yellow/brown on cyan text = text.replace(/@[xX][Bb]7/g, "\1n\1w\1" + "6\1i"); // Blinking white on cyan text = text.replace(/@[xX][Bb]8/g, "\1h\1k\1" + "6\1i"); // Blinking bright black on cyan text = text.replace(/@[xX][Bb]9/g, "\1h\1b\1" + "6\1i"); // Blinking bright blue on cyan text = text.replace(/@[xX][Bb][Aa]/g, "\1h\1g\1" + "6\1i"); // Blinking bright green on cyan text = text.replace(/@[xX][Bb][Bb]/g, "\1h\1c\1" + "6\1i"); // Blinking bright cyan on cyan text = text.replace(/@[xX][Bb][Cc]/g, "\1h\1r\1" + "6\1i"); // Blinking bright red on cyan text = text.replace(/@[xX][Bb][Dd]/g, "\1h\1m\1" + "6\1i"); // Blinking bright magenta on cyan text = text.replace(/@[xX][Bb][Ee]/g, "\1h\1y\1" + "6\1i"); // Blinking bright yellow on cyan text = text.replace(/@[xX][Bb][Ff]/g, "\1h\1w\1" + "6\1i"); // Blinking bright white on cyan // Red background, blinking foreground text = text.replace(/@[xX][Cc]0/g, "\1n\1k\1" + "1\1i"); // Blinking black on red text = text.replace(/@[xX][Cc]1/g, "\1n\1b\1" + "1\1i"); // Blinking blue on red text = text.replace(/@[xX][Cc]2/g, "\1n\1g\1" + "1\1i"); // Blinking green on red text = text.replace(/@[xX][Cc]3/g, "\1n\1c\1" + "1\1i"); // Blinking cyan on red text = text.replace(/@[xX][Cc]4/g, "\1n\1r\1" + "1\1i"); // Blinking red on red text = text.replace(/@[xX][Cc]5/g, "\1n\1m\1" + "1\1i"); // Blinking magenta on red text = text.replace(/@[xX][Cc]6/g, "\1n\1y\1" + "1\1i"); // Blinking yellow/brown on red text = text.replace(/@[xX][Cc]7/g, "\1n\1w\1" + "1\1i"); // Blinking white on red text = text.replace(/@[xX][Cc]8/g, "\1h\1k\1" + "1\1i"); // Blinking bright black on red text = text.replace(/@[xX][Cc]9/g, "\1h\1b\1" + "1\1i"); // Blinking bright blue on red text = text.replace(/@[xX][Cc][Aa]/g, "\1h\1g\1" + "1\1i"); // Blinking bright green on red text = text.replace(/@[xX][Cc][Bb]/g, "\1h\1c\1" + "1\1i"); // Blinking bright cyan on red text = text.replace(/@[xX][Cc][Cc]/g, "\1h\1r\1" + "1\1i"); // Blinking bright red on red text = text.replace(/@[xX][Cc][Dd]/g, "\1h\1m\1" + "1\1i"); // Blinking bright magenta on red text = text.replace(/@[xX][Cc][Ee]/g, "\1h\1y\1" + "1\1i"); // Blinking bright yellow on red text = text.replace(/@[xX][Cc][Ff]/g, "\1h\1w\1" + "1\1i"); // Blinking bright white on red // Magenta background, blinking foreground text = text.replace(/@[xX][Dd]0/g, "\1n\1k\1" + "5\1i"); // Blinking black on magenta text = text.replace(/@[xX][Dd]1/g, "\1n\1b\1" + "5\1i"); // Blinking blue on magenta text = text.replace(/@[xX][Dd]2/g, "\1n\1g\1" + "5\1i"); // Blinking green on magenta text = text.replace(/@[xX][Dd]3/g, "\1n\1c\1" + "5\1i"); // Blinking cyan on magenta text = text.replace(/@[xX][Dd]4/g, "\1n\1r\1" + "5\1i"); // Blinking red on magenta text = text.replace(/@[xX][Dd]5/g, "\1n\1m\1" + "5\1i"); // Blinking magenta on magenta text = text.replace(/@[xX][Dd]6/g, "\1n\1y\1" + "5\1i"); // Blinking yellow/brown on magenta text = text.replace(/@[xX][Dd]7/g, "\1n\1w\1" + "5\1i"); // Blinking white on magenta text = text.replace(/@[xX][Dd]8/g, "\1h\1k\1" + "5\1i"); // Blinking bright black on magenta text = text.replace(/@[xX][Dd]9/g, "\1h\1b\1" + "5\1i"); // Blinking bright blue on magenta text = text.replace(/@[xX][Dd][Aa]/g, "\1h\1g\1" + "5\1i"); // Blinking bright green on magenta text = text.replace(/@[xX][Dd][Bb]/g, "\1h\1c\1" + "5\1i"); // Blinking bright cyan on magenta text = text.replace(/@[xX][Dd][Cc]/g, "\1h\1r\1" + "5\1i"); // Blinking bright red on magenta text = text.replace(/@[xX][Dd][Dd]/g, "\1h\1m\1" + "5\1i"); // Blinking bright magenta on magenta text = text.replace(/@[xX][Dd][Ee]/g, "\1h\1y\1" + "5\1i"); // Blinking bright yellow on magenta text = text.replace(/@[xX][Dd][Ff]/g, "\1h\1w\1" + "5\1i"); // Blinking bright white on magenta // Brown background, blinking foreground text = text.replace(/@[xX][Ee]0/g, "\1n\1k\1" + "3\1i"); // Blinking black on brown text = text.replace(/@[xX][Ee]1/g, "\1n\1b\1" + "3\1i"); // Blinking blue on brown text = text.replace(/@[xX][Ee]2/g, "\1n\1g\1" + "3\1i"); // Blinking green on brown text = text.replace(/@[xX][Ee]3/g, "\1n\1c\1" + "3\1i"); // Blinking cyan on brown text = text.replace(/@[xX][Ee]4/g, "\1n\1r\1" + "3\1i"); // Blinking red on brown text = text.replace(/@[xX][Ee]5/g, "\1n\1m\1" + "3\1i"); // Blinking magenta on brown text = text.replace(/@[xX][Ee]6/g, "\1n\1y\1" + "3\1i"); // Blinking yellow/brown on brown text = text.replace(/@[xX][Ee]7/g, "\1n\1w\1" + "3\1i"); // Blinking white on brown text = text.replace(/@[xX][Ee]8/g, "\1h\1k\1" + "3\1i"); // Blinking bright black on brown text = text.replace(/@[xX][Ee]9/g, "\1h\1b\1" + "3\1i"); // Blinking bright blue on brown text = text.replace(/@[xX][Ee][Aa]/g, "\1h\1g\1" + "3\1i"); // Blinking bright green on brown text = text.replace(/@[xX][Ee][Bb]/g, "\1h\1c\1" + "3\1i"); // Blinking bright cyan on brown text = text.replace(/@[xX][Ee][Cc]/g, "\1h\1r\1" + "3\1i"); // Blinking bright red on brown text = text.replace(/@[xX][Ee][Dd]/g, "\1h\1m\1" + "3\1i"); // Blinking bright magenta on brown text = text.replace(/@[xX][Ee][Ee]/g, "\1h\1y\1" + "3\1i"); // Blinking bright yellow on brown text = text.replace(/@[xX][Ee][Ff]/g, "\1h\1w\1" + "3\1i"); // Blinking bright white on brown // White background, blinking foreground text = text.replace(/@[xX][Ff]0/g, "\1n\1k\1" + "7\1i"); // Blinking black on white text = text.replace(/@[xX][Ff]1/g, "\1n\1b\1" + "7\1i"); // Blinking blue on white text = text.replace(/@[xX][Ff]2/g, "\1n\1g\1" + "7\1i"); // Blinking green on white text = text.replace(/@[xX][Ff]3/g, "\1n\1c\1" + "7\1i"); // Blinking cyan on white text = text.replace(/@[xX][Ff]4/g, "\1n\1r\1" + "7\1i"); // Blinking red on white text = text.replace(/@[xX][Ff]5/g, "\1n\1m\1" + "7\1i"); // Blinking magenta on white text = text.replace(/@[xX][Ff]6/g, "\1n\1y\1" + "7\1i"); // Blinking yellow/brown on white text = text.replace(/@[xX][Ff]7/g, "\1n\1w\1" + "7\1i"); // Blinking white on white text = text.replace(/@[xX][Ff]8/g, "\1h\1k\1" + "7\1i"); // Blinking bright black on white text = text.replace(/@[xX][Ff]9/g, "\1h\1b\1" + "7\1i"); // Blinking bright blue on white text = text.replace(/@[xX][Ff][Aa]/g, "\1h\1g\1" + "7\1i"); // Blinking bright green on white text = text.replace(/@[xX][Ff][Bb]/g, "\1h\1c\1" + "7\1i"); // Blinking bright cyan on white text = text.replace(/@[xX][Ff][Cc]/g, "\1h\1r\1" + "7\1i"); // Blinking bright red on white text = text.replace(/@[xX][Ff][Dd]/g, "\1h\1m\1" + "7\1i"); // Blinking bright magenta on white text = text.replace(/@[xX][Ff][Ee]/g, "\1h\1y\1" + "7\1i"); // Blinking bright yellow on white text = text.replace(/@[xX][Ff][Ff]/g, "\1h\1w\1" + "7\1i"); // Blinking bright white on white return text; } else return pText; // No PCBoard-style attribute codes found, so just return the text. } // Converts Wildcat attribute codes to Synchronet attribute codes. // // Parameters: // pText: A string containing the text to convert // // Return value: The text with the color codes converted function wildcatAttrsToSyncAttrs(pText) { // First, see if the text has any Wildcat-style attribute codes at // all. We'll be performing a bunch of search & replace commands, // so we don't want to do all that work for nothing.. :) if (/@[0-9A-Fa-f]{2}@/.test(pText)) { // Black background var text = pText.replace(/@00@/g, "\1n\1k\1" + "0"); // Black on black text = text.replace(/@01@/g, "\1n\1b\1" + "0"); // Blue on black text = text.replace(/@02@/g, "\1n\1g\1" + "0"); // Green on black text = text.replace(/@03@/g, "\1n\1c\1" + "0"); // Cyan on black text = text.replace(/@04@/g, "\1n\1r\1" + "0"); // Red on black text = text.replace(/@05@/g, "\1n\1m\1" + "0"); // Magenta on black text = text.replace(/@06@/g, "\1n\1y\1" + "0"); // Yellow/brown on black text = text.replace(/@07@/g, "\1n\1w\1" + "0"); // White on black text = text.replace(/@08@/g, "\1n\1w\1" + "0"); // White on black text = text.replace(/@09@/g, "\1n\1w\1" + "0"); // White on black text = text.replace(/@08@/g, "\1h\1k\1" + "0"); // Bright black on black text = text.replace(/@09@/g, "\1h\1b\1" + "0"); // Bright blue on black text = text.replace(/@0[Aa]@/g, "\1h\1g\1" + "0"); // Bright green on black text = text.replace(/@0[Bb]@/g, "\1h\1c\1" + "0"); // Bright cyan on black text = text.replace(/@0[Cc]@/g, "\1h\1r\1" + "0"); // Bright red on black text = text.replace(/@0[Dd]@/g, "\1h\1m\1" + "0"); // Bright magenta on black text = text.replace(/@0[Ee]@/g, "\1h\1y\1" + "0"); // Bright yellow on black text = text.replace(/@0[Ff]@/g, "\1h\1w\1" + "0"); // Bright white on black // Blinking foreground // Blue background text = text.replace(/@10@/g, "\1n\1k\1" + "4"); // Black on blue text = text.replace(/@11@/g, "\1n\1b\1" + "4"); // Blue on blue text = text.replace(/@12@/g, "\1n\1g\1" + "4"); // Green on blue text = text.replace(/@13@/g, "\1n\1c\1" + "4"); // Cyan on blue text = text.replace(/@14@/g, "\1n\1r\1" + "4"); // Red on blue text = text.replace(/@15@/g, "\1n\1m\1" + "4"); // Magenta on blue text = text.replace(/@16@/g, "\1n\1y\1" + "4"); // Yellow/brown on blue text = text.replace(/@17@/g, "\1n\1w\1" + "4"); // White on blue text = text.replace(/@18@/g, "\1h\1k\1" + "4"); // Bright black on blue text = text.replace(/@19@/g, "\1h\1b\1" + "4"); // Bright blue on blue text = text.replace(/@1[Aa]@/g, "\1h\1g\1" + "4"); // Bright green on blue text = text.replace(/@1[Bb]@/g, "\1h\1c\1" + "4"); // Bright cyan on blue text = text.replace(/@1[Cc]@/g, "\1h\1r\1" + "4"); // Bright red on blue text = text.replace(/@1[Dd]@/g, "\1h\1m\1" + "4"); // Bright magenta on blue text = text.replace(/@1[Ee]@/g, "\1h\1y\1" + "4"); // Bright yellow on blue text = text.replace(/@1[Ff]@/g, "\1h\1w\1" + "4"); // Bright white on blue // Green background text = text.replace(/@20@/g, "\1n\1k\1" + "2"); // Black on green text = text.replace(/@21@/g, "\1n\1b\1" + "2"); // Blue on green text = text.replace(/@22@/g, "\1n\1g\1" + "2"); // Green on green text = text.replace(/@23@/g, "\1n\1c\1" + "2"); // Cyan on green text = text.replace(/@24@/g, "\1n\1r\1" + "2"); // Red on green text = text.replace(/@25@/g, "\1n\1m\1" + "2"); // Magenta on green text = text.replace(/@26@/g, "\1n\1y\1" + "2"); // Yellow/brown on green text = text.replace(/@27@/g, "\1n\1w\1" + "2"); // White on green text = text.replace(/@28@/g, "\1h\1k\1" + "2"); // Bright black on green text = text.replace(/@29@/g, "\1h\1b\1" + "2"); // Bright blue on green text = text.replace(/@2[Aa]@/g, "\1h\1g\1" + "2"); // Bright green on green text = text.replace(/@2[Bb]@/g, "\1h\1c\1" + "2"); // Bright cyan on green text = text.replace(/@2[Cc]@/g, "\1h\1r\1" + "2"); // Bright red on green text = text.replace(/@2[Dd]@/g, "\1h\1m\1" + "2"); // Bright magenta on green text = text.replace(/@2[Ee]@/g, "\1h\1y\1" + "2"); // Bright yellow on green text = text.replace(/@2[Ff]@/g, "\1h\1w\1" + "2"); // Bright white on green // Cyan background text = text.replace(/@30@/g, "\1n\1k\1" + "6"); // Black on cyan text = text.replace(/@31@/g, "\1n\1b\1" + "6"); // Blue on cyan text = text.replace(/@32@/g, "\1n\1g\1" + "6"); // Green on cyan text = text.replace(/@33@/g, "\1n\1c\1" + "6"); // Cyan on cyan text = text.replace(/@34@/g, "\1n\1r\1" + "6"); // Red on cyan text = text.replace(/@35@/g, "\1n\1m\1" + "6"); // Magenta on cyan text = text.replace(/@36@/g, "\1n\1y\1" + "6"); // Yellow/brown on cyan text = text.replace(/@37@/g, "\1n\1w\1" + "6"); // White on cyan text = text.replace(/@38@/g, "\1h\1k\1" + "6"); // Bright black on cyan text = text.replace(/@39@/g, "\1h\1b\1" + "6"); // Bright blue on cyan text = text.replace(/@3[Aa]@/g, "\1h\1g\1" + "6"); // Bright green on cyan text = text.replace(/@3[Bb]@/g, "\1h\1c\1" + "6"); // Bright cyan on cyan text = text.replace(/@3[Cc]@/g, "\1h\1r\1" + "6"); // Bright red on cyan text = text.replace(/@3[Dd]@/g, "\1h\1m\1" + "6"); // Bright magenta on cyan text = text.replace(/@3[Ee]@/g, "\1h\1y\1" + "6"); // Bright yellow on cyan text = text.replace(/@3[Ff]@/g, "\1h\1w\1" + "6"); // Bright white on cyan // Red background text = text.replace(/@40@/g, "\1n\1k\1" + "1"); // Black on red text = text.replace(/@41@/g, "\1n\1b\1" + "1"); // Blue on red text = text.replace(/@42@/g, "\1n\1g\1" + "1"); // Green on red text = text.replace(/@43@/g, "\1n\1c\1" + "1"); // Cyan on red text = text.replace(/@44@/g, "\1n\1r\1" + "1"); // Red on red text = text.replace(/@45@/g, "\1n\1m\1" + "1"); // Magenta on red text = text.replace(/@46@/g, "\1n\1y\1" + "1"); // Yellow/brown on red text = text.replace(/@47@/g, "\1n\1w\1" + "1"); // White on red text = text.replace(/@48@/g, "\1h\1k\1" + "1"); // Bright black on red text = text.replace(/@49@/g, "\1h\1b\1" + "1"); // Bright blue on red text = text.replace(/@4[Aa]@/g, "\1h\1g\1" + "1"); // Bright green on red text = text.replace(/@4[Bb]@/g, "\1h\1c\1" + "1"); // Bright cyan on red text = text.replace(/@4[Cc]@/g, "\1h\1r\1" + "1"); // Bright red on red text = text.replace(/@4[Dd]@/g, "\1h\1m\1" + "1"); // Bright magenta on red text = text.replace(/@4[Ee]@/g, "\1h\1y\1" + "1"); // Bright yellow on red text = text.replace(/@4[Ff]@/g, "\1h\1w\1" + "1"); // Bright white on red // Magenta background text = text.replace(/@50@/g, "\1n\1k\1" + "5"); // Black on magenta text = text.replace(/@51@/g, "\1n\1b\1" + "5"); // Blue on magenta text = text.replace(/@52@/g, "\1n\1g\1" + "5"); // Green on magenta text = text.replace(/@53@/g, "\1n\1c\1" + "5"); // Cyan on magenta text = text.replace(/@54@/g, "\1n\1r\1" + "5"); // Red on magenta text = text.replace(/@55@/g, "\1n\1m\1" + "5"); // Magenta on magenta text = text.replace(/@56@/g, "\1n\1y\1" + "5"); // Yellow/brown on magenta text = text.replace(/@57@/g, "\1n\1w\1" + "5"); // White on magenta text = text.replace(/@58@/g, "\1h\1k\1" + "5"); // Bright black on magenta text = text.replace(/@59@/g, "\1h\1b\1" + "5"); // Bright blue on magenta text = text.replace(/@5[Aa]@/g, "\1h\1g\1" + "5"); // Bright green on magenta text = text.replace(/@5[Bb]@/g, "\1h\1c\1" + "5"); // Bright cyan on magenta text = text.replace(/@5[Cc]@/g, "\1h\1r\1" + "5"); // Bright red on magenta text = text.replace(/@5[Dd]@/g, "\1h\1m\1" + "5"); // Bright magenta on magenta text = text.replace(/@5[Ee]@/g, "\1h\1y\1" + "5"); // Bright yellow on magenta text = text.replace(/@5[Ff]@/g, "\1h\1w\1" + "5"); // Bright white on magenta // Brown background text = text.replace(/@60@/g, "\1n\1k\1" + "3"); // Black on brown text = text.replace(/@61@/g, "\1n\1b\1" + "3"); // Blue on brown text = text.replace(/@62@/g, "\1n\1g\1" + "3"); // Green on brown text = text.replace(/@63@/g, "\1n\1c\1" + "3"); // Cyan on brown text = text.replace(/@64@/g, "\1n\1r\1" + "3"); // Red on brown text = text.replace(/@65@/g, "\1n\1m\1" + "3"); // Magenta on brown text = text.replace(/@66@/g, "\1n\1y\1" + "3"); // Yellow/brown on brown text = text.replace(/@67@/g, "\1n\1w\1" + "3"); // White on brown text = text.replace(/@68@/g, "\1h\1k\1" + "3"); // Bright black on brown text = text.replace(/@69@/g, "\1h\1b\1" + "3"); // Bright blue on brown text = text.replace(/@6[Aa]@/g, "\1h\1g\1" + "3"); // Bright breen on brown text = text.replace(/@6[Bb]@/g, "\1h\1c\1" + "3"); // Bright cyan on brown text = text.replace(/@6[Cc]@/g, "\1h\1r\1" + "3"); // Bright red on brown text = text.replace(/@6[Dd]@/g, "\1h\1m\1" + "3"); // Bright magenta on brown text = text.replace(/@6[Ee]@/g, "\1h\1y\1" + "3"); // Bright yellow on brown text = text.replace(/@6[Ff]@/g, "\1h\1w\1" + "3"); // Bright white on brown // White background text = text.replace(/@70@/g, "\1n\1k\1" + "7"); // Black on white text = text.replace(/@71@/g, "\1n\1b\1" + "7"); // Blue on white text = text.replace(/@72@/g, "\1n\1g\1" + "7"); // Green on white text = text.replace(/@73@/g, "\1n\1c\1" + "7"); // Cyan on white text = text.replace(/@74@/g, "\1n\1r\1" + "7"); // Red on white text = text.replace(/@75@/g, "\1n\1m\1" + "7"); // Magenta on white text = text.replace(/@76@/g, "\1n\1y\1" + "7"); // Yellow/brown on white text = text.replace(/@77@/g, "\1n\1w\1" + "7"); // White on white text = text.replace(/@78@/g, "\1h\1k\1" + "7"); // Bright black on white text = text.replace(/@79@/g, "\1h\1b\1" + "7"); // Bright blue on white text = text.replace(/@7[Aa]@/g, "\1h\1g\1" + "7"); // Bright green on white text = text.replace(/@7[Bb]@/g, "\1h\1c\1" + "7"); // Bright cyan on white text = text.replace(/@7[Cc]@/g, "\1h\1r\1" + "7"); // Bright red on white text = text.replace(/@7[Dd]@/g, "\1h\1m\1" + "7"); // Bright magenta on white text = text.replace(/@7[Ee]@/g, "\1h\1y\1" + "7"); // Bright yellow on white text = text.replace(/@7[Ff]@/g, "\1h\1w\1" + "7"); // Bright white on white // Black background, blinking foreground text = text.replace(/@80@/g, "\1n\1k\1" + "0\1i"); // Blinking black on black text = text.replace(/@81@/g, "\1n\1b\1" + "0\1i"); // Blinking blue on black text = text.replace(/@82@/g, "\1n\1g\1" + "0\1i"); // Blinking green on black text = text.replace(/@83@/g, "\1n\1c\1" + "0\1i"); // Blinking cyan on black text = text.replace(/@84@/g, "\1n\1r\1" + "0\1i"); // Blinking red on black text = text.replace(/@85@/g, "\1n\1m\1" + "0\1i"); // Blinking magenta on black text = text.replace(/@86@/g, "\1n\1y\1" + "0\1i"); // Blinking yellow/brown on black text = text.replace(/@87@/g, "\1n\1w\1" + "0\1i"); // Blinking white on black text = text.replace(/@88@/g, "\1h\1k\1" + "0\1i"); // Blinking bright black on black text = text.replace(/@89@/g, "\1h\1b\1" + "0\1i"); // Blinking bright blue on black text = text.replace(/@8[Aa]@/g, "\1h\1g\1" + "0\1i"); // Blinking bright green on black text = text.replace(/@8[Bb]@/g, "\1h\1c\1" + "0\1i"); // Blinking bright cyan on black text = text.replace(/@8[Cc]@/g, "\1h\1r\1" + "0\1i"); // Blinking bright red on black text = text.replace(/@8[Dd]@/g, "\1h\1m\1" + "0\1i"); // Blinking bright magenta on black text = text.replace(/@8[Ee]@/g, "\1h\1y\1" + "0\1i"); // Blinking bright yellow on black text = text.replace(/@8[Ff]@/g, "\1h\1w\1" + "0\1i"); // Blinking bright white on black // Blue background, blinking foreground text = text.replace(/@90@/g, "\1n\1k\1" + "4\1i"); // Blinking black on blue text = text.replace(/@91@/g, "\1n\1b\1" + "4\1i"); // Blinking blue on blue text = text.replace(/@92@/g, "\1n\1g\1" + "4\1i"); // Blinking green on blue text = text.replace(/@93@/g, "\1n\1c\1" + "4\1i"); // Blinking cyan on blue text = text.replace(/@94@/g, "\1n\1r\1" + "4\1i"); // Blinking red on blue text = text.replace(/@95@/g, "\1n\1m\1" + "4\1i"); // Blinking magenta on blue text = text.replace(/@96@/g, "\1n\1y\1" + "4\1i"); // Blinking yellow/brown on blue text = text.replace(/@97@/g, "\1n\1w\1" + "4\1i"); // Blinking white on blue text = text.replace(/@98@/g, "\1h\1k\1" + "4\1i"); // Blinking bright black on blue text = text.replace(/@99@/g, "\1h\1b\1" + "4\1i"); // Blinking bright blue on blue text = text.replace(/@9[Aa]@/g, "\1h\1g\1" + "4\1i"); // Blinking bright green on blue text = text.replace(/@9[Bb]@/g, "\1h\1c\1" + "4\1i"); // Blinking bright cyan on blue text = text.replace(/@9[Cc]@/g, "\1h\1r\1" + "4\1i"); // Blinking bright red on blue text = text.replace(/@9[Dd]@/g, "\1h\1m\1" + "4\1i"); // Blinking bright magenta on blue text = text.replace(/@9[Ee]@/g, "\1h\1y\1" + "4\1i"); // Blinking bright yellow on blue text = text.replace(/@9[Ff]@/g, "\1h\1w\1" + "4\1i"); // Blinking bright white on blue // Green background, blinking foreground text = text.replace(/@[Aa]0@/g, "\1n\1k\1" + "2\1i"); // Blinking black on green text = text.replace(/@[Aa]1@/g, "\1n\1b\1" + "2\1i"); // Blinking blue on green text = text.replace(/@[Aa]2@/g, "\1n\1g\1" + "2\1i"); // Blinking green on green text = text.replace(/@[Aa]3@/g, "\1n\1c\1" + "2\1i"); // Blinking cyan on green text = text.replace(/@[Aa]4@/g, "\1n\1r\1" + "2\1i"); // Blinking red on green text = text.replace(/@[Aa]5@/g, "\1n\1m\1" + "2\1i"); // Blinking magenta on green text = text.replace(/@[Aa]6@/g, "\1n\1y\1" + "2\1i"); // Blinking yellow/brown on green text = text.replace(/@[Aa]7@/g, "\1n\1w\1" + "2\1i"); // Blinking white on green text = text.replace(/@[Aa]8@/g, "\1h\1k\1" + "2\1i"); // Blinking bright black on green text = text.replace(/@[Aa]9@/g, "\1h\1b\1" + "2\1i"); // Blinking bright blue on green text = text.replace(/@[Aa][Aa]@/g, "\1h\1g\1" + "2\1i"); // Blinking bright green on green text = text.replace(/@[Aa][Bb]@/g, "\1h\1c\1" + "2\1i"); // Blinking bright cyan on green text = text.replace(/@[Aa][Cc]@/g, "\1h\1r\1" + "2\1i"); // Blinking bright red on green text = text.replace(/@[Aa][Dd]@/g, "\1h\1m\1" + "2\1i"); // Blinking bright magenta on green text = text.replace(/@[Aa][Ee]@/g, "\1h\1y\1" + "2\1i"); // Blinking bright yellow on green text = text.replace(/@[Aa][Ff]@/g, "\1h\1w\1" + "2\1i"); // Blinking bright white on green // Cyan background, blinking foreground text = text.replace(/@[Bb]0@/g, "\1n\1k\1" + "6\1i"); // Blinking black on cyan text = text.replace(/@[Bb]1@/g, "\1n\1b\1" + "6\1i"); // Blinking blue on cyan text = text.replace(/@[Bb]2@/g, "\1n\1g\1" + "6\1i"); // Blinking green on cyan text = text.replace(/@[Bb]3@/g, "\1n\1c\1" + "6\1i"); // Blinking cyan on cyan text = text.replace(/@[Bb]4@/g, "\1n\1r\1" + "6\1i"); // Blinking red on cyan text = text.replace(/@[Bb]5@/g, "\1n\1m\1" + "6\1i"); // Blinking magenta on cyan text = text.replace(/@[Bb]6@/g, "\1n\1y\1" + "6\1i"); // Blinking yellow/brown on cyan text = text.replace(/@[Bb]7@/g, "\1n\1w\1" + "6\1i"); // Blinking white on cyan text = text.replace(/@[Bb]8@/g, "\1h\1k\1" + "6\1i"); // Blinking bright black on cyan text = text.replace(/@[Bb]9@/g, "\1h\1b\1" + "6\1i"); // Blinking bright blue on cyan text = text.replace(/@[Bb][Aa]@/g, "\1h\1g\1" + "6\1i"); // Blinking bright green on cyan text = text.replace(/@[Bb][Bb]@/g, "\1h\1c\1" + "6\1i"); // Blinking bright cyan on cyan text = text.replace(/@[Bb][Cc]@/g, "\1h\1r\1" + "6\1i"); // Blinking bright red on cyan text = text.replace(/@[Bb][Dd]@/g, "\1h\1m\1" + "6\1i"); // Blinking bright magenta on cyan text = text.replace(/@[Bb][Ee]@/g, "\1h\1y\1" + "6\1i"); // Blinking bright yellow on cyan text = text.replace(/@[Bb][Ff]@/g, "\1h\1w\1" + "6\1i"); // Blinking bright white on cyan // Red background, blinking foreground text = text.replace(/@[Cc]0@/g, "\1n\1k\1" + "1\1i"); // Blinking black on red text = text.replace(/@[Cc]1@/g, "\1n\1b\1" + "1\1i"); // Blinking blue on red text = text.replace(/@[Cc]2@/g, "\1n\1g\1" + "1\1i"); // Blinking green on red text = text.replace(/@[Cc]3@/g, "\1n\1c\1" + "1\1i"); // Blinking cyan on red text = text.replace(/@[Cc]4@/g, "\1n\1r\1" + "1\1i"); // Blinking red on red text = text.replace(/@[Cc]5@/g, "\1n\1m\1" + "1\1i"); // Blinking magenta on red text = text.replace(/@[Cc]6@/g, "\1n\1y\1" + "1\1i"); // Blinking yellow/brown on red text = text.replace(/@[Cc]7@/g, "\1n\1w\1" + "1\1i"); // Blinking white on red text = text.replace(/@[Cc]8@/g, "\1h\1k\1" + "1\1i"); // Blinking bright black on red text = text.replace(/@[Cc]9@/g, "\1h\1b\1" + "1\1i"); // Blinking bright blue on red text = text.replace(/@[Cc][Aa]@/g, "\1h\1g\1" + "1\1i"); // Blinking bright green on red text = text.replace(/@[Cc][Bb]@/g, "\1h\1c\1" + "1\1i"); // Blinking bright cyan on red text = text.replace(/@[Cc][Cc]@/g, "\1h\1r\1" + "1\1i"); // Blinking bright red on red text = text.replace(/@[Cc][Dd]@/g, "\1h\1m\1" + "1\1i"); // Blinking bright magenta on red text = text.replace(/@[Cc][Ee]@/g, "\1h\1y\1" + "1\1i"); // Blinking bright yellow on red text = text.replace(/@[Cc][Ff]@/g, "\1h\1w\1" + "1\1i"); // Blinking bright white on red // Magenta background, blinking foreground text = text.replace(/@[Dd]0@/g, "\1n\1k\1" + "5\1i"); // Blinking black on magenta text = text.replace(/@[Dd]1@/g, "\1n\1b\1" + "5\1i"); // Blinking blue on magenta text = text.replace(/@[Dd]2@/g, "\1n\1g\1" + "5\1i"); // Blinking green on magenta text = text.replace(/@[Dd]3@/g, "\1n\1c\1" + "5\1i"); // Blinking cyan on magenta text = text.replace(/@[Dd]4@/g, "\1n\1r\1" + "5\1i"); // Blinking red on magenta text = text.replace(/@[Dd]5@/g, "\1n\1m\1" + "5\1i"); // Blinking magenta on magenta text = text.replace(/@[Dd]6@/g, "\1n\1y\1" + "5\1i"); // Blinking yellow/brown on magenta text = text.replace(/@[Dd]7@/g, "\1n\1w\1" + "5\1i"); // Blinking white on magenta text = text.replace(/@[Dd]8@/g, "\1h\1k\1" + "5\1i"); // Blinking bright black on magenta text = text.replace(/@[Dd]9@/g, "\1h\1b\1" + "5\1i"); // Blinking bright blue on magenta text = text.replace(/@[Dd][Aa]@/g, "\1h\1g\1" + "5\1i"); // Blinking bright green on magenta text = text.replace(/@[Dd][Bb]@/g, "\1h\1c\1" + "5\1i"); // Blinking bright cyan on magenta text = text.replace(/@[Dd][Cc]@/g, "\1h\1r\1" + "5\1i"); // Blinking bright red on magenta text = text.replace(/@[Dd][Dd]@/g, "\1h\1m\1" + "5\1i"); // Blinking bright magenta on magenta text = text.replace(/@[Dd][Ee]@/g, "\1h\1y\1" + "5\1i"); // Blinking bright yellow on magenta text = text.replace(/@[Dd][Ff]@/g, "\1h\1w\1" + "5\1i"); // Blinking bright white on magenta // Brown background, blinking foreground text = text.replace(/@[Ee]0@/g, "\1n\1k\1" + "3\1i"); // Blinking black on brown text = text.replace(/@[Ee]1@/g, "\1n\1b\1" + "3\1i"); // Blinking blue on brown text = text.replace(/@[Ee]2@/g, "\1n\1g\1" + "3\1i"); // Blinking green on brown text = text.replace(/@[Ee]3@/g, "\1n\1c\1" + "3\1i"); // Blinking cyan on brown text = text.replace(/@[Ee]4@/g, "\1n\1r\1" + "3\1i"); // Blinking red on brown text = text.replace(/@[Ee]5@/g, "\1n\1m\1" + "3\1i"); // Blinking magenta on brown text = text.replace(/@[Ee]6@/g, "\1n\1y\1" + "3\1i"); // Blinking yellow/brown on brown text = text.replace(/@[Ee]7@/g, "\1n\1w\1" + "3\1i"); // Blinking white on brown text = text.replace(/@[Ee]8@/g, "\1h\1k\1" + "3\1i"); // Blinking bright black on brown text = text.replace(/@[Ee]9@/g, "\1h\1b\1" + "3\1i"); // Blinking bright blue on brown text = text.replace(/@[Ee][Aa]@/g, "\1h\1g\1" + "3\1i"); // Blinking bright green on brown text = text.replace(/@[Ee][Bb]@/g, "\1h\1c\1" + "3\1i"); // Blinking bright cyan on brown text = text.replace(/@[Ee][Cc]@/g, "\1h\1r\1" + "3\1i"); // Blinking bright red on brown text = text.replace(/@[Ee][Dd]@/g, "\1h\1m\1" + "3\1i"); // Blinking bright magenta on brown text = text.replace(/@[Ee][Ee]@/g, "\1h\1y\1" + "3\1i"); // Blinking bright yellow on brown text = text.replace(/@[Ee][Ff]@/g, "\1h\1w\1" + "3\1i"); // Blinking bright white on brown // White background, blinking foreground text = text.replace(/@[Ff]0@/g, "\1n\1k\1" + "7\1i"); // Blinking black on white text = text.replace(/@[Ff]1@/g, "\1n\1b\1" + "7\1i"); // Blinking blue on white text = text.replace(/@[Ff]2@/g, "\1n\1g\1" + "7\1i"); // Blinking green on white text = text.replace(/@[Ff]3@/g, "\1n\1c\1" + "7\1i"); // Blinking cyan on white text = text.replace(/@[Ff]4@/g, "\1n\1r\1" + "7\1i"); // Blinking red on white text = text.replace(/@[Ff]5@/g, "\1n\1m\1" + "7\1i"); // Blinking magenta on white text = text.replace(/@[Ff]6@/g, "\1n\1y\1" + "7\1i"); // Blinking yellow/brown on white text = text.replace(/@[Ff]7@/g, "\1n\1w\1" + "7\1i"); // Blinking white on white text = text.replace(/@[Ff]8@/g, "\1h\1k\1" + "7\1i"); // Blinking bright black on white text = text.replace(/@[Ff]9@/g, "\1h\1b\1" + "7\1i"); // Blinking bright blue on white text = text.replace(/@[Ff][Aa]@/g, "\1h\1g\1" + "7\1i"); // Blinking bright green on white text = text.replace(/@[Ff][Bb]@/g, "\1h\1c\1" + "7\1i"); // Blinking bright cyan on white text = text.replace(/@[Ff][Cc]@/g, "\1h\1r\1" + "7\1i"); // Blinking bright red on white text = text.replace(/@[Ff][Dd]@/g, "\1h\1m\1" + "7\1i"); // Blinking bright magenta on white text = text.replace(/@[Ff][Ee]@/g, "\1h\1y\1" + "7\1i"); // Blinking bright yellow on white text = text.replace(/@[Ff][Ff]@/g, "\1h\1w\1" + "7\1i"); // Blinking bright white on white return text; } else return pText; // No Wildcat-style attribute codes found, so just return the text. } // Converts Celerity attribute codes to Synchronet attribute codes. // // Parameters: // pText: A string containing the text to convert // // Return value: The text with the color codes converted function celerityAttrsToSyncAttrs(pText) { // First, see if the text has any Celerity-style attribute codes at // all. We'll be performing a bunch of search & replace commands, // so we don't want to do all that work for nothing.. :) if (/\|[kbgcrmywdBGCRMYWS]/.test(pText)) { // Using the \|S code (swap foreground & background) // Blue background var text = pText.replace(/\|b\|S\|k/g, "\1n\1k\1" + "4"); // Black on blue text = text.replace(/\|b\|S\|b/g, "\1n\1b\1" + "4"); // Blue on blue text = text.replace(/\|b\|S\|g/g, "\1n\1g\1" + "4"); // Green on blue text = text.replace(/\|b\|S\|c/g, "\1n\1c\1" + "4"); // Cyan on blue text = text.replace(/\|b\|S\|r/g, "\1n\1r\1" + "4"); // Red on blue text = text.replace(/\|b\|S\|m/g, "\1n\1m\1" + "4"); // Magenta on blue text = text.replace(/\|b\|S\|y/g, "\1n\1y\1" + "4"); // Yellow/brown on blue text = text.replace(/\|b\|S\|w/g, "\1n\1w\1" + "4"); // White on blue text = text.replace(/\|b\|S\|d/g, "\1h\1k\1" + "4"); // Bright black on blue text = text.replace(/\|b\|S\|B/g, "\1h\1b\1" + "4"); // Bright blue on blue text = text.replace(/\|b\|S\|G/g, "\1h\1g\1" + "4"); // Bright green on blue text = text.replace(/\|b\|S\|C/g, "\1h\1c\1" + "4"); // Bright cyan on blue text = text.replace(/\|b\|S\|R/g, "\1h\1r\1" + "4"); // Bright red on blue text = text.replace(/\|b\|S\|M/g, "\1h\1m\1" + "4"); // Bright magenta on blue text = text.replace(/\|b\|S\|Y/g, "\1h\1y\1" + "4"); // Yellow on blue text = text.replace(/\|b\|S\|W/g, "\1h\1w\1" + "4"); // Bright white on blue // Green background text = text.replace(/\|g\|S\|k/g, "\1n\1k\1" + "2"); // Black on green text = text.replace(/\|g\|S\|b/g, "\1n\1b\1" + "2"); // Blue on green text = text.replace(/\|g\|S\|g/g, "\1n\1g\1" + "2"); // Green on green text = text.replace(/\|g\|S\|c/g, "\1n\1c\1" + "2"); // Cyan on green text = text.replace(/\|g\|S\|r/g, "\1n\1r\1" + "2"); // Red on green text = text.replace(/\|g\|S\|m/g, "\1n\1m\1" + "2"); // Magenta on green text = text.replace(/\|g\|S\|y/g, "\1n\1y\1" + "2"); // Yellow/brown on green text = text.replace(/\|g\|S\|w/g, "\1n\1w\1" + "2"); // White on green text = text.replace(/\|g\|S\|d/g, "\1h\1k\1" + "2"); // Bright black on green text = text.replace(/\|g\|S\|B/g, "\1h\1b\1" + "2"); // Bright blue on green text = text.replace(/\|g\|S\|G/g, "\1h\1g\1" + "2"); // Bright green on green text = text.replace(/\|g\|S\|C/g, "\1h\1c\1" + "2"); // Bright cyan on green text = text.replace(/\|g\|S\|R/g, "\1h\1r\1" + "2"); // Bright red on green text = text.replace(/\|g\|S\|M/g, "\1h\1m\1" + "2"); // Bright magenta on green text = text.replace(/\|g\|S\|Y/g, "\1h\1y\1" + "2"); // Yellow on green text = text.replace(/\|g\|S\|W/g, "\1h\1w\1" + "2"); // Bright white on green // Cyan background text = text.replace(/\|c\|S\|k/g, "\1n\1k\1" + "6"); // Black on cyan text = text.replace(/\|c\|S\|b/g, "\1n\1b\1" + "6"); // Blue on cyan text = text.replace(/\|c\|S\|g/g, "\1n\1g\1" + "6"); // Green on cyan text = text.replace(/\|c\|S\|c/g, "\1n\1c\1" + "6"); // Cyan on cyan text = text.replace(/\|c\|S\|r/g, "\1n\1r\1" + "6"); // Red on cyan text = text.replace(/\|c\|S\|m/g, "\1n\1m\1" + "6"); // Magenta on cyan text = text.replace(/\|c\|S\|y/g, "\1n\1y\1" + "6"); // Yellow/brown on cyan text = text.replace(/\|c\|S\|w/g, "\1n\1w\1" + "6"); // White on cyan text = text.replace(/\|c\|S\|d/g, "\1h\1k\1" + "6"); // Bright black on cyan text = text.replace(/\|c\|S\|B/g, "\1h\1b\1" + "6"); // Bright blue on cyan text = text.replace(/\|c\|S\|G/g, "\1h\1g\1" + "6"); // Bright green on cyan text = text.replace(/\|c\|S\|C/g, "\1h\1c\1" + "6"); // Bright cyan on cyan text = text.replace(/\|c\|S\|R/g, "\1h\1r\1" + "6"); // Bright red on cyan text = text.replace(/\|c\|S\|M/g, "\1h\1m\1" + "6"); // Bright magenta on cyan text = text.replace(/\|c\|S\|Y/g, "\1h\1y\1" + "6"); // Yellow on cyan text = text.replace(/\|c\|S\|W/g, "\1h\1w\1" + "6"); // Bright white on cyan // Red background text = text.replace(/\|r\|S\|k/g, "\1n\1k\1" + "1"); // Black on red text = text.replace(/\|r\|S\|b/g, "\1n\1b\1" + "1"); // Blue on red text = text.replace(/\|r\|S\|g/g, "\1n\1g\1" + "1"); // Green on red text = text.replace(/\|r\|S\|c/g, "\1n\1c\1" + "1"); // Cyan on red text = text.replace(/\|r\|S\|r/g, "\1n\1r\1" + "1"); // Red on red text = text.replace(/\|r\|S\|m/g, "\1n\1m\1" + "1"); // Magenta on red text = text.replace(/\|r\|S\|y/g, "\1n\1y\1" + "1"); // Yellow/brown on red text = text.replace(/\|r\|S\|w/g, "\1n\1w\1" + "1"); // White on red text = text.replace(/\|r\|S\|d/g, "\1h\1k\1" + "1"); // Bright black on red text = text.replace(/\|r\|S\|B/g, "\1h\1b\1" + "1"); // Bright blue on red text = text.replace(/\|r\|S\|G/g, "\1h\1g\1" + "1"); // Bright green on red text = text.replace(/\|r\|S\|C/g, "\1h\1c\1" + "1"); // Bright cyan on red text = text.replace(/\|r\|S\|R/g, "\1h\1r\1" + "1"); // Bright red on red text = text.replace(/\|r\|S\|M/g, "\1h\1m\1" + "1"); // Bright magenta on red text = text.replace(/\|r\|S\|Y/g, "\1h\1y\1" + "1"); // Yellow on red text = text.replace(/\|r\|S\|W/g, "\1h\1w\1" + "1"); // Bright white on red // Magenta background text = text.replace(/\|m\|S\|k/g, "\1n\1k\1" + "5"); // Black on magenta text = text.replace(/\|m\|S\|b/g, "\1n\1b\1" + "5"); // Blue on magenta text = text.replace(/\|m\|S\|g/g, "\1n\1g\1" + "5"); // Green on magenta text = text.replace(/\|m\|S\|c/g, "\1n\1c\1" + "5"); // Cyan on magenta text = text.replace(/\|m\|S\|r/g, "\1n\1r\1" + "5"); // Red on magenta text = text.replace(/\|m\|S\|m/g, "\1n\1m\1" + "5"); // Magenta on magenta text = text.replace(/\|m\|S\|y/g, "\1n\1y\1" + "5"); // Yellow/brown on magenta text = text.replace(/\|m\|S\|w/g, "\1n\1w\1" + "5"); // White on magenta text = text.replace(/\|m\|S\|d/g, "\1h\1k\1" + "5"); // Bright black on magenta text = text.replace(/\|m\|S\|B/g, "\1h\1b\1" + "5"); // Bright blue on magenta text = text.replace(/\|m\|S\|G/g, "\1h\1g\1" + "5"); // Bright green on magenta text = text.replace(/\|m\|S\|C/g, "\1h\1c\1" + "5"); // Bright cyan on magenta text = text.replace(/\|m\|S\|R/g, "\1h\1r\1" + "5"); // Bright red on magenta text = text.replace(/\|m\|S\|M/g, "\1h\1m\1" + "5"); // Bright magenta on magenta text = text.replace(/\|m\|S\|Y/g, "\1h\1y\1" + "5"); // Yellow on magenta text = text.replace(/\|m\|S\|W/g, "\1h\1w\1" + "5"); // Bright white on magenta // Brown background text = text.replace(/\|y\|S\|k/g, "\1n\1k\1" + "3"); // Black on brown text = text.replace(/\|y\|S\|b/g, "\1n\1b\1" + "3"); // Blue on brown text = text.replace(/\|y\|S\|g/g, "\1n\1g\1" + "3"); // Green on brown text = text.replace(/\|y\|S\|c/g, "\1n\1c\1" + "3"); // Cyan on brown text = text.replace(/\|y\|S\|r/g, "\1n\1r\1" + "3"); // Red on brown text = text.replace(/\|y\|S\|m/g, "\1n\1m\1" + "3"); // Magenta on brown text = text.replace(/\|y\|S\|y/g, "\1n\1y\1" + "3"); // Yellow/brown on brown text = text.replace(/\|y\|S\|w/g, "\1n\1w\1" + "3"); // White on brown text = text.replace(/\|y\|S\|d/g, "\1h\1k\1" + "3"); // Bright black on brown text = text.replace(/\|y\|S\|B/g, "\1h\1b\1" + "3"); // Bright blue on brown text = text.replace(/\|y\|S\|G/g, "\1h\1g\1" + "3"); // Bright green on brown text = text.replace(/\|y\|S\|C/g, "\1h\1c\1" + "3"); // Bright cyan on brown text = text.replace(/\|y\|S\|R/g, "\1h\1r\1" + "3"); // Bright red on brown text = text.replace(/\|y\|S\|M/g, "\1h\1m\1" + "3"); // Bright magenta on brown text = text.replace(/\|y\|S\|Y/g, "\1h\1y\1" + "3"); // Yellow on brown text = text.replace(/\|y\|S\|W/g, "\1h\1w\1" + "3"); // Bright white on brown // White background text = text.replace(/\|w\|S\|k/g, "\1n\1k\1" + "7"); // Black on white text = text.replace(/\|w\|S\|b/g, "\1n\1b\1" + "7"); // Blue on white text = text.replace(/\|w\|S\|g/g, "\1n\1g\1" + "7"); // Green on white text = text.replace(/\|w\|S\|c/g, "\1n\1c\1" + "7"); // Cyan on white text = text.replace(/\|w\|S\|r/g, "\1n\1r\1" + "7"); // Red on white text = text.replace(/\|w\|S\|m/g, "\1n\1m\1" + "7"); // Magenta on white text = text.replace(/\|w\|S\|y/g, "\1n\1y\1" + "7"); // Yellow/brown on white text = text.replace(/\|w\|S\|w/g, "\1n\1w\1" + "7"); // White on white text = text.replace(/\|w\|S\|d/g, "\1h\1k\1" + "7"); // Bright black on white text = text.replace(/\|w\|S\|B/g, "\1h\1b\1" + "7"); // Bright blue on white text = text.replace(/\|w\|S\|G/g, "\1h\1g\1" + "7"); // Bright green on white text = text.replace(/\|w\|S\|C/g, "\1h\1c\1" + "7"); // Bright cyan on white text = text.replace(/\|w\|S\|R/g, "\1h\1r\1" + "7"); // Bright red on white text = text.replace(/\|w\|S\|M/g, "\1h\1m\1" + "7"); // Bright magenta on white text = text.replace(/\|w\|S\|Y/g, "\1h\1y\1" + "7"); // Yellow on white text = text.replace(/\|w\|S\|W/g, "\1h\1w\1" + "7"); // Bright white on white // Colors on black background text = text.replace(/\|k/g, "\1n\1k\1" + "0"); // Black on black text = text.replace(/\|k\|S\|k/g, "\1n\1k\1" + "0"); // Black on black text = text.replace(/\|b/g, "\1n\1b\1" + "0"); // Blue on black text = text.replace(/\|k\|S\|b/g, "\1n\1b\1" + "0"); // Blue on black text = text.replace(/\|g/g, "\1n\1g\1" + "0"); // Green on black text = text.replace(/\|k\|S\|g/g, "\1n\1g\1" + "0"); // Green on black text = text.replace(/\|c/g, "\1n\1c\1" + "0"); // Cyan on black text = text.replace(/\|k\|S\|c/g, "\1n\1c\1" + "0"); // Cyan on black text = text.replace(/\|r/g, "\1n\1r\1" + "0"); // Red on black text = text.replace(/\|k\|S\|r/g, "\1n\1r\1" + "0"); // Red on black text = text.replace(/\|m/g, "\1n\1m\1" + "0"); // Magenta on black text = text.replace(/\|k\|S\|m/g, "\1n\1m\1" + "0"); // Magenta on black text = text.replace(/\|y/g, "\1n\1y\1" + "0"); // Yellow/brown on black text = text.replace(/\|k\|S\|y/g, "\1n\1y\1" + "0"); // Yellow/brown on black text = text.replace(/\|w/g, "\1n\1w\1" + "0"); // White on black text = text.replace(/\|k\|S\|w/g, "\1n\1w\1" + "0"); // White on black text = text.replace(/\|d/g, "\1h\1k\1" + "0"); // Bright black on black text = text.replace(/\|k\|S\|d/g, "\1h\1k\1" + "0"); // Bright black on black text = text.replace(/\|B/g, "\1h\1b\1" + "0"); // Bright blue on black text = text.replace(/\|k\|S\|B/g, "\1h\1b\1" + "0"); // Bright blue on black text = text.replace(/\|G/g, "\1h\1g\1" + "0"); // Bright green on black text = text.replace(/\|k\|S\|G/g, "\1h\1g\1" + "0"); // Bright green on black text = text.replace(/\|C/g, "\1h\1c\1" + "0"); // Bright cyan on black text = text.replace(/\|k\|S\|C/g, "\1h\1c\1" + "0"); // Bright cyan on black text = text.replace(/\|R/g, "\1h\1r\1" + "0"); // Bright red on black text = text.replace(/\|k\|S\|R/g, "\1h\1r\1" + "0"); // Bright red on black text = text.replace(/\|M/g, "\1h\1m\1" + "0"); // Bright magenta on black text = text.replace(/\|k\|S\|M/g, "\1h\1m\1" + "0"); // Bright magenta on black text = text.replace(/\|Y/g, "\1h\1y\1" + "0"); // Yellow on black text = text.replace(/\|k\|S\|Y/g, "\1h\1y\1" + "0"); // Yellow on black text = text.replace(/\|W/g, "\1h\1w\1" + "0"); // Bright white on black text = text.replace(/\|k\|S\|W/g, "\1h\1w\1" + "0"); // Bright white on black return text; } else return pText; // No Celerity-style attribute codes found, so just return the text. } // Converts Renegade attribute (color) codes to Synchronet attribute codes. // // Parameters: // pText: A string containing the text to convert // // Return value: The text with the color codes converted function renegadeAttrsToSyncAttrs(pText) { // First, see if the text has any Renegade-style attribute codes at // all. We'll be performing a bunch of search & replace commands, // so we don't want to do all that work for nothing.. :) if (/\|[0-3][0-9]/.test(pText)) { var text = pText.replace(/\|00/g, "\1n\1k"); // Normal black text = text.replace(/\|01/g, "\1n\1b"); // Normal blue text = text.replace(/\|02/g, "\1n\1g"); // Normal green text = text.replace(/\|03/g, "\1n\1c"); // Normal cyan text = text.replace(/\|04/g, "\1n\1r"); // Normal red text = text.replace(/\|05/g, "\1n\1m"); // Normal magenta text = text.replace(/\|06/g, "\1n\1y"); // Normal brown text = text.replace(/\|07/g, "\1n\1w"); // Normal white text = text.replace(/\|08/g, "\1n\1k\1h"); // High intensity black text = text.replace(/\|09/g, "\1n\1b\1h"); // High intensity blue text = text.replace(/\|10/g, "\1n\1g\1h"); // High intensity green text = text.replace(/\|11/g, "\1n\1c\1h"); // High intensity cyan text = text.replace(/\|12/g, "\1n\1r\1h"); // High intensity red text = text.replace(/\|13/g, "\1n\1m\1h"); // High intensity magenta text = text.replace(/\|14/g, "\1n\1y\1h"); // Yellow (high intensity brown) text = text.replace(/\|15/g, "\1n\1w\1h"); // High intensity white text = text.replace(/\|16/g, "\1" + "0"); // Background black text = text.replace(/\|17/g, "\1" + "4"); // Background blue text = text.replace(/\|18/g, "\1" + "2"); // Background green text = text.replace(/\|19/g, "\1" + "6"); // Background cyan text = text.replace(/\|20/g, "\1" + "1"); // Background red text = text.replace(/\|21/g, "\1" + "5"); // Background magenta text = text.replace(/\|22/g, "\1" + "3"); // Background brown text = text.replace(/\|23/g, "\1" + "7"); // Background white text = text.replace(/\|24/g, "\1i\1w\1" + "0"); // Blinking white on black text = text.replace(/\|25/g, "\1i\1w\1" + "4"); // Blinking white on blue text = text.replace(/\|26/g, "\1i\1w\1" + "2"); // Blinking white on green text = text.replace(/\|27/g, "\1i\1w\1" + "6"); // Blinking white on cyan text = text.replace(/\|28/g, "\1i\1w\1" + "1"); // Blinking white on red text = text.replace(/\|29/g, "\1i\1w\1" + "5"); // Blinking white on magenta text = text.replace(/\|30/g, "\1i\1w\1" + "3"); // Blinking white on yellow/brown text = text.replace(/\|31/g, "\1i\1w\1" + "7"); // Blinking white on white return text; } else return pText; // No Renegade-style attribute codes found, so just return the text. } // Converts ANSI attribute codes to Synchronet attribute codes. // // Parameters: // pText: A string containing the text to convert // // Return value: The text with the color codes converted function ANSIAttrsToSyncAttrs(pText) { // TODO: Test & update this some more.. Not sure if this is working 100% right. // Web pages with ANSI code information: // http://pueblo.sourceforge.net/doc/manual/ansi_color_codes.html // http://ascii-table.com/ansi-escape-sequences.php // http://stackoverflow.com/questions/4842424/list-of-ansi-color-escape-sequences // First, see if the text has any ANSI attribute codes at all. We'll be // performing a bunch of search & replace commands, so we don't want to do // all that work for nothing. if (textHasANSICodes(pText)) { var text = ""; var tempDirExists = true; // Temporary (to get it to run the old way for now) tempDirExists = false; /* var readerTmpDir = backslash(system.node_dir + "DDMsgReaderTemp"); if (!file_exists(readerTmpDir)) tempDirExists = mkdir(readerTmpDir); */ if (tempDirExists) { var wroteTempFile = true; var tmpFileName = readerTmpDir + "tmpMessage.ans"; var msgTmpFile = new File(tmpFileName); if (msgTmpFile.open("w")) { wroteTempFile = msgTmpFile.write(pText); msgTmpFile.close(); } // If the temp file was written, then convert it to Synchronet // attributes using ans2asc. if (wroteTempFile) { var convertedTempFileName = readerTmpDir + "tmpMessage.asc"; var cmdLine = system.exec_dir + "ans2asc \"" + tmpFileName + "\" \"" + convertedTempFileName + "\""; // Note: Both system.exec(cmdLine) and // bbs.exec(cmdLine, EX_NATIVE, gStartupPath) could be used to // execute the command, but system.exec() seems noticeably faster. system.exec(cmdLine); var convertedTmpFile = new File(convertedTempFileName); if (convertedTmpFile.open("r")) { text = convertedTmpFile.read(); convertedTmpFile.close(); } } deltree(readerTmpDir); } else { // Attributes var text = pText.replace(/\[0[mM]/g, "\1n"); // All attributes off text = text.replace(/\[1[mM]/g, "\1h"); // Bold on (use high intensity) text = text.replace(/\[5[mM]/g, "\1i"); // Blink on // Foreground colors text = text.replace(/\[30[mM]/g, "\1k"); // Black foreground text = text.replace(/\[31[mM]/g, "\1r"); // Red foreground text = text.replace(/\[32[mM]/g, "\1g"); // Green foreground text = text.replace(/\[33[mM]/g, "\1y"); // Yellow foreground text = text.replace(/\[34[mM]/g, "\1b"); // Blue foreground text = text.replace(/\[35[mM]/g, "\1m"); // Magenta foreground text = text.replace(/\[36[mM]/g, "\1c"); // Cyan foreground text = text.replace(/\[37[mM]/g, "\1w"); // White foreground // Background colors text = text.replace(/\[40[mM]/g, "\1" + "0"); // Black background text = text.replace(/\[41[mM]/g, "\1" + "1"); // Red background text = text.replace(/\[42[mM]/g, "\1" + "2"); // Green background text = text.replace(/\[43[mM]/g, "\1" + "3"); // Yellow background text = text.replace(/\[44[mM]/g, "\1" + "4"); // Blue background text = text.replace(/\[45[mM]/g, "\1" + "5"); // Magenta background text = text.replace(/\[46[mM]/g, "\1" + "6"); // Cyan background text = text.replace(/\[47[mM]/g, "\1" + "7"); // White background // Convert ;-delimited modes (such as [Value;...;Valuem) text = ANSIMultiConvertToSyncCodes(text); // Remove ANSI codes that are not wanted (such as moving the cursor, etc.) text = text.replace(/\[[0-9]+[aA]/g, ""); // Cursor up text = text.replace(/\[[0-9]+[bB]/g, ""); // Cursor down text = text.replace(/\[[0-9]+[cC]/g, ""); // Cursor forward text = text.replace(/\[[0-9]+[dD]/g, ""); // Cursor backward text = text.replace(/\[[0-9]+;[0-9]+[hH]/g, ""); // Cursor position text = text.replace(/\[[0-9]+;[0-9]+[fF]/g, ""); // Cursor position text = text.replace(/\[[sS]/g, ""); // Restore cursor position text = text.replace(/\[2[jJ]/g, ""); // Erase display text = text.replace(/\[[kK]/g, ""); // Erase line text = text.replace(/\[=[0-9]+[hH]/g, ""); // Set various screen modes text = text.replace(/\[=[0-9]+[lL]/g, ""); // Reset various screen modes } return text; } else return pText; // No ANSI codes found, so just return the text. } // Returns whether or not some text has any ANSI codes in it. // // Parameters: // pText: The text to test // // Return value: Boolean - Whether or not the text has ANSI codes in it function textHasANSICodes(pText) { return(/\[[0-9]+[mM]/.test(pText) || /\[[0-9]+(;[0-9]+)+[mM]/.test(pText) || /\[[0-9]+[aAbBcCdD]/.test(pText) || /\[[0-9]+;[0-9]+[hHfF]/.test(pText) || /\[[sSuUkK]/.test(pText) || /\[2[jJ]/.test(pText)); } // Converts ANSI ;-delimited modes (such as [Value;...;Valuem) to Synchronet // attribute codes // // Parameters: // pText: The text with ANSI ;-delimited modes to convert // // Return value: The text with ANSI ;-delimited modes converted to Synchronet attributes function ANSIMultiConvertToSyncCodes(pText) { var multiMatches = pText.match(/\[[0-9]+(;[0-9]+)+m/g); if (multiMatches == null) return pText; var updatedText = pText; for (var i = 0; i < multiMatches.length; ++i) { // Copy the string, with the [ removed from the beginning and the // trailing 'm' removed var text = multiMatches[i].substr(2); text = text.substr(0, text.length-1); var codes = text.split(";"); var syncCodes = ""; for (var idx = 0; idx < codes.length; ++idx) { if (codes[idx] == "0") // All attributes off syncCodes += "\1n"; else if (codes[idx] == "1") // Bold on (high intensity) syncCodes += "\1h"; else if (codes[idx] == "5") // Blink on syncCodes += "\1i"; else if (codes[idx] == "30") // Black foreground syncCodes += "\1k"; else if (codes[idx] == "31") // Red foreground syncCodes += "\1r"; else if (codes[idx] == "32") // Green foreground syncCodes += "\1g"; else if (codes[idx] == "33") // Yellow foreground syncCodes += "\1y"; else if (codes[idx] == "34") // Blue foreground syncCodes += "\1b"; else if (codes[idx] == "35") // Magenta foreground syncCodes += "\1m"; else if (codes[idx] == "36") // Cyan foreground syncCodes += "\1c"; else if (codes[idx] == "37") // White foreground syncCodes += "\1w"; else if (codes[idx] == "40") // Black background syncCodes += "\1" + "0"; else if (codes[idx] == "41") // Red background syncCodes += "\1" + "1"; else if (codes[idx] == "42") // Green background syncCodes += "\1" + "2"; else if (codes[idx] == "43") // Yellow background syncCodes += "\1" + "3"; else if (codes[idx] == "44") // Blue background syncCodes += "\1" + "4"; else if (codes[idx] == "45") // Magenta background syncCodes += "\1" + "5"; else if (codes[idx] == "46") // Cyan background syncCodes += "\1" + "6"; else if (codes[idx] == "47") // White background syncCodes += "\1" + "7"; } updatedText = updatedText.replace(multiMatches[i], syncCodes); } return updatedText; } // Returns whether a given message group index & sub-board index (or the current ones, // based on bbs.curgrp and bbs.cursub) are for the last message sub-board on the system. // // Parameters: // pGrpIdx: Optional - The index of the message group. If not specified, this will // default to bbs.curgrp. If bbs.curgrp is not defined in that case, // then this method will return false. // pSubIdx: Optional - The index of the message sub-board. If not specified, this will // default to bbs.cursub. If bbs.cursub is not defined in that case, // then this method will return false. // // Return value: Boolean - Whether or not the current/given message group index & sub-board // index are for the last message sub-board on the system. If there // are any issues with any of the values (including bbs.curgrp or // bbs.cursub), this method will return false. function curMsgSubBoardIsLast(pGrpIdx, pSubIdx) { var curGrp = 0; if (typeof(pGrpIdx) == "number") curGrp = pGrpIdx; else if (typeof(bbs.curgrp) == "number") curGrp = bbs.curgrp; else return false; var curSub = 0; if (typeof(pSubIdx) == "number") curSub = pSubIdx; else if (typeof(bbs.cursub) == "number") curSub = bbs.cursub; else return false; return (curGrp == msg_area.grp_list.length-1) && (curSub == msg_area.grp_list[msg_area.grp_list.length-1].sub_list.length-1); } // Parses arguments, where each argument in the given array is in the format // -arg=val. If the value is the string "true" or "false", then the value will // be a boolean. Otherwise, the value will be a string. // // Parameters: // pArgArr: An array of strings containing values in the format -arg=val // // Return value: An object containing the argument values. The index will be // the argument names, converted to lowercase. The values will // be either the string argument values or boolean values, depending // on the formats of the arguments passed in. function parseArgs(pArgArr) { var argVals = new Object(); // Set default values for parameters that are just true/false values argVals.chooseareafirst = false; argVals.personalemail = false; argVals.personalemailsent = false; argVals.verboselogging = false; argVals.suppresssearchtypetext = false; // Sanity checking for pArgArr - Make sure it's an array if ((typeof(pArgArr) != "object") || (typeof(pArgArr.length) != "number")) return argVals; // Go through pArgArr looking for strings in the format -arg=val and parse them // into objects in the argVals array. var equalsIdx = 0; var argName = ""; var argVal = ""; var argValLower = ""; // For case-insensitive "true"/"false" matching var argValIsTrue = false; for (var i = 0; i < pArgArr.length; ++i) { // We're looking for strings that start with "-", except strings that are // only "-". if ((typeof(pArgArr[i]) != "string") || (pArgArr[i].length == 0) || (pArgArr[i].charAt(0) != "-") || (pArgArr[i] == "-")) { continue; } // Look for an = and if found, split the string on the = equalsIdx = pArgArr[i].indexOf("="); // If a = is found, then split on it and add the argument name & value // to the array. Otherwise (if the = is not found), then treat the // argument as a boolean and set it to true (to enable an option). if (equalsIdx > -1) { argName = pArgArr[i].substring(1, equalsIdx).toLowerCase(); argVal = pArgArr[i].substr(equalsIdx+1); argValLower = argVal.toLowerCase(); // If the argument value is the word "true" or "false", then add it as a // boolean. Otherwise, add it as a string. argValIsTrue = (argValLower == "true"); if (argValIsTrue || (argValLower == "false")) argVals[argName] = argValIsTrue; else argVals[argName] = argVal; } else // An equals sign (=) was not found. Add as a boolean set to true to enable the option. { argName = pArgArr[i].substr(1).toLowerCase(); if ((argName == "chooseareafirst") || (argName == "personalemail") || (argName == "personalemailsent") || (argName == "allpersonalemail") || (argName == "verboselogging") || (argName == "suppresssearchtypetext")) { argVals[argName] = true; } } } // Sanity checking // If the arguments include personalEmail and personalEmail is enabled, // then check to see if a search type was specified - If so, only allow // keyword search and from name search. if (argVals.hasOwnProperty("personalemail") && argVals.personalemail) { // If a search type is specified, only allow keyword search & from name // search if (argVals.hasOwnProperty("search")) { var searchValLower = argVals.search.toLowerCase(); if ((searchValLower != "keyword_search") && (searchValLower != "from_name_search")) delete argVals.search; } } // If the arguments include userNum, make sure the value is all digits. If so, // add altUserNum to the arguments as a number type for user matching when looking // for personal email to the user. if (argVals.hasOwnProperty("usernum")) { if (/^[0-9]+$/.test(argVals.usernum)) { var specifiedUserNum = Number(argVals.usernum); // If the specified number is different than the current logged-in // user, then load the other user account and read their name and // alias and also store their user number in the arg vals as a // number. if (specifiedUserNum != user.number) { var theUser = new User(specifiedUserNum); argVals.altUserNum = theUser.number; argVals.altUserName = theUser.name; argVals.altUserAlias = theUser.alias; } else delete argVals.usernum; } else delete argVals.usernum; } return argVals; } // Returns a string describing all message attributes (main, auxiliary, and net). // // Parameters: // pMsgHdr: A message header object. // // Return value: A string describing all of the message attributes function makeAllMsgAttrStr(pMsgHdr) { if ((pMsgHdr == null) || (typeof(pMsgHdr) != "object")) return ""; var msgAttrStr = makeMainMsgAttrStr(pMsgHdr.attr); var auxAttrStr = makeAuxMsgAttrStr(pMsgHdr.auxattr); if (auxAttrStr.length > 0) { if (msgAttrStr.length > 0) msgAttrStr += ", "; msgAttrStr += auxAttrStr; } var netAttrStr = makeNetMsgAttrStr(pMsgHdr.netattr); if (netAttrStr.length > 0) { if (msgAttrStr.length > 0) msgAttrStr += ", "; msgAttrStr += netAttrStr; } return msgAttrStr; } // Returns a string describing the main message attributes. Makes use of the // gMainMsgAttrStrs object for the main message attributes and description // strings. // // Parameters: // pMainMsgAttrs: The bit field for the main message attributes // (normally, the 'attr' property of a header object) // pIfEmptyString: Optional - A string to use if there are no attributes set // // Return value: A string describing the main message attributes function makeMainMsgAttrStr(pMainMsgAttrs, pIfEmptyString) { var msgAttrStr = ""; if (typeof(pMainMsgAttrs) == "number") { for (var prop in gMainMsgAttrStrs) { if ((pMainMsgAttrs & prop) == prop) { if (msgAttrStr.length > 0) msgAttrStr += ", "; msgAttrStr += gMainMsgAttrStrs[prop]; } } } if ((msgAttrStr.length == 0) && (typeof(pIfEmptyString) == "string")) msgAttrStr = pIfEmptyString; return msgAttrStr; } // Returns a string describing auxiliary message attributes. Makes use of the // gAuxMsgAttrStrs object for the auxiliary message attributes and description // strings. // // Parameters: // pAuxMsgAttrs: The bit field for the auxiliary message attributes // (normally, the 'auxattr' property of a header object) // pIfEmptyString: Optional - A string to use if there are no attributes set // // Return value: A string describing the auxiliary message attributes function makeAuxMsgAttrStr(pAuxMsgAttrs, pIfEmptyString) { var msgAttrStr = ""; if (typeof(pAuxMsgAttrs) == "number") { for (var prop in gAuxMsgAttrStrs) { if ((pAuxMsgAttrs & prop) == prop) { if (msgAttrStr.length > 0) msgAttrStr += ", "; msgAttrStr += gAuxMsgAttrStrs[prop]; } } } if ((msgAttrStr.length == 0) && (typeof(pIfEmptyString) == "string")) msgAttrStr = pIfEmptyString; return msgAttrStr; } // Returns a string describing network message attributes. Makes use of the // gNetMsgAttrStrs object for the network message attributes and description // strings. // // Parameters: // pNetMsgAttrs: The bit field for the network message attributes // (normally, the 'netattr' property of a header object) // pIfEmptyString: Optional - A string to use if there are no attributes set // // Return value: A string describing the network message attributes function makeNetMsgAttrStr(pNetMsgAttrs, pIfEmptyString) { var msgAttrStr = ""; if (typeof(pNetMsgAttrs) == "number") { for (var prop in gNetMsgAttrStrs) { if ((pNetMsgAttrs & prop) == prop) { if (msgAttrStr.length > 0) msgAttrStr += ", "; msgAttrStr += gNetMsgAttrStrs[prop]; } } } if ((msgAttrStr.length == 0) && (typeof(pIfEmptyString) == "string")) msgAttrStr = pIfEmptyString; return msgAttrStr; } // Given a sub-board code, this function returns a sub-board's group and name. // If the given sub-board code is "mail", then this will return "Personal mail". // // Parameters: // pSubBoardCode: An internal sub-board code // // Return value: A string containing the sub-board code group & name, or // "Personal email" if it's the personal email sub-board function subBoardGrpAndName(pSubBoardCode) { if (typeof(pSubBoardCode) != "string") return ""; var subBoardGrpAndName = ""; if (pSubBoardCode == "mail") subBoardGrpAndName = "Personal mail"; else { subBoardGrpAndName = msg_area.sub[pSubBoardCode].grp_name + " - " + msg_area.sub[pSubBoardCode].name; } return subBoardGrpAndName; } // Returns whether a given string matches the current user's name, handle, or alias. // Does a case-insensitive match. // // Parameters: // pStr: The string to match against the user's name/handle/alias // // Return value: Boolean - Whether or not the string matches the current user's name, // handle, or alias function userNameHandleAliasMatch(pStr) { if (typeof(pStr) != "string") return false; var strUpper = pStr.toUpperCase(); return ((strUpper == user.name.toUpperCase()) || (strUpper == user.handle.toUpperCase()) || (strUpper == user.alias.toUpperCase())); } // Writes a log message to the system log (using LOG_INFO log level) and to the // node log. This will prepend the text "Digital Distortion Message Reader (" // + user.alias + "): " to the log message. // // // Parameters: // pMessage: The message to log function writeToSysAndNodeLog(pMessage) { if (typeof(pMessage) != "string") return; var logMessage = "Digital Distortion Message Reader (" + user.alias + "): " + pMessage; log(LOG_INFO, logMessage); bbs.log_str(logMessage); } // This function looks up and returns a sub-board code from the sub-board number. // If no matching sub-board is found, this will return an empty string. // // Parameters: // pSubBoardNum: A sub-board number // // Return value: The sub-board code. If no matching sub-board is found, an empty // string will be returned. function getSubBoardCodeFromNum(pSubBoardNum) { // Ensure we're using a numeric type for the sub-board number // (in case pSubBoardNum is a string rather than a number) var subNum = Number(pSubBoardNum); var subBoardCode = ""; for (var subCode in msg_area.sub) { if (msg_area.sub[subCode].number == subNum) { subBoardCode = subCode; break; } } return subBoardCode; } // Separates message text and any attachment data. // // Parameters: // pMsgHdr: The message header object // pMsgText: The text of a message // pGetB64Data: Optional boolean - Whether or not to get the Base64-encoded // data for base64-encoded attachments (i.e., in multi-part MIME // emails). Defaults to true. // // Return value: An object containing the following properties: // msgText: The text of the message, without any of the // attachment base64-encoded data, etc. If // the message doesn't have any attachments, then // this will likely be the same as pMsgText. // attachments: An array of objects containing the following properties // for each attachment: // B64Data: Base64-encoded file data - Only for attachments // that were attached as base64 in the message (i.e., // in a multi-part MIME message). If the attachment // was uploaded to the user's Synchronet mailbox, // then the object won't have the B64Data property. // filename: The name of the attached file // fullyPathedFilename: The full path & filename of the // attached file saved on the BBS machine // errorMsg: An error message if anything went wrong. If // nothing went wrong, this will be an empty string. function determineMsgAttachments(pMsgHdr, pMsgText, pGetB64Data) { var retObj = new Object(); retObj.msgText = ""; retObj.attachments = []; retObj.errorMsg = ""; // Keep track of the user's inbox directory: sbbs/data/file/<userNum>.in var userInboxDir = backslash(backslash(system.data_dir + "file") + format("%04d.in", user.number)); // If the message subject is a filename that exists in the user's // inbox directory, then add its filename to the list of attached // filenames that will be returned var fullyPathedAttachmentFilename = userInboxDir + pMsgHdr.subject; if (file_exists(fullyPathedAttachmentFilename)) { retObj.attachments.push({ filename: pMsgHdr.subject, fullyPathedFilename: fullyPathedAttachmentFilename }); } // The message to prepend onto the message text if the message has attachments var msgHasAttachmentsTxt = "\1n\1g\1h- This message contains one or more attachments. Press CTRL-A to download.\1n\r\n" + "\1n\1g\1h--------------------------------------------------------------------------\1n\r\n"; // Sanity checking if (typeof(pMsgText) != "string") { // If there are any attachments, prepend the message text with a message // saying that the message contains attachments. if (retObj.attachments.length > 0) retObj.msgText = msgHasAttachmentsTxt + retObj.msgText; return retObj; } // If the message text doesn't include a line starting with -- and a // line starting with "Content-type:", then then just return the // the same text in retObj. //var hasMultiParts = /--\S+\s*Content-Type:/.test(pMsgText); //var hasMultiParts = ((dashDashIdx > -1) && (/Content-Type/.test(pMsgText))); var dashDashIdx = pMsgText.indexOf("--"); var hasMultiParts = ((dashDashIdx > -1) && (pMsgText.indexOf("Content-Type", dashDashIdx+1) > dashDashIdx)); if (!hasMultiParts) { //retObj.msgText = pMsgText; // If there are any attachments, prepend the message text with a message // saying that the message contains attachments. if (retObj.attachments.length > 0) retObj.msgText = msgHasAttachmentsTxt + pMsgText; else retObj.msgText = pMsgText; return retObj; } var getB64Data = true; if (typeof(pGetB64Data) == "boolean") getB64Data = pGetB64Data; // Look in the message text for a line starting with -- followed by some characters, // then whitespace var sepMatches = /--\S+\s/.exec(pMsgText); var msgSeparator = sepMatches[0]; // If the last character in msgSeparator is a whitepsace character, then // remove it. if (/\s/.test(msgSeparator.substr(msgSeparator.length-1, 1))) msgSeparator = msgSeparator.substr(0, msgSeparator.length-1); var contentType = ""; // The content type of the current section var lastContentType = ""; // The content type of the last section var contentEncodingType = ""; var sepIdx = 0; var lastSepIdx = -1; var lastContentTypeIdx = -1; var lastContentEncodingTypeIdx = -1; var startIdx = 0; var gotMessageText = false; // In case the message has both text/plain & text/html while ((sepIdx = pMsgText.indexOf(msgSeparator, startIdx)) >= 0) { var contentEncodingTypeIdx = -1; // Look for a "Content-Type:" from the starting index var contentTypeIdx = pMsgText.indexOf("Content-Type: ", startIdx+msgSeparator.length); if (contentTypeIdx > -1) { // Extract the content-type string up to a newline or 15 characters // if there's no newline var newlineIdx = pMsgText.indexOf("\n", contentTypeIdx+14); contentType = pMsgText.substring(contentTypeIdx+14, newlineIdx > -1 ? newlineIdx : contentTypeIdx+29); // If the last character is whitespace (i.e., a newline), then remove it. if (/\s/.test(contentType.substr(contentType.length-1, 1))) contentType = contentType.substr(0, contentType.length-1); // Update the start index for looking for the next message separator string // - This should be after the "Content-type:" value. startIdx = contentTypeIdx + contentType.length; } else { // No "Content-Type:" string was found // Update the start index for looking for the next message separator string startIdx = sepIdx + msgSeparator.length; } if ((lastSepIdx > -1) && (lastContentTypeIdx > -1)) { // msgTextSearchStartIdx stores the index of where to start looking // for the message text. It could be lastContentTypeIdx, or it could // be the content encoding type index if the "Content encoding type" // text is found for the current message part. var msgTextSearchStartIdx = lastContentTypeIdx; // Look for "Content-Transfer-Encoding:" right after the content type // and extract the content encoding type string contentEncodingTypeIdx = pMsgText.indexOf("Content-Transfer-Encoding:", lastContentTypeIdx); // If "Content-Transfer-Encoding:" wasn't found after the content type, // then look just before the content type, but after the last separator // string. if (contentEncodingTypeIdx == -1) contentEncodingTypeIdx = pMsgText.indexOf("Content-Transfer-Encoding:", lastSepIdx); // If the next "Content-Encoding-Type" is after the current section, // then this section doesn't have a content type, so blank it out. if (contentEncodingTypeIdx > sepIdx) { contentEncodingTypeIdx = -1; contentEncodingType = ""; } else { msgTextSearchStartIdx = contentEncodingTypeIdx; // Extract the content encoding type var newlineIdx = pMsgText.indexOf("\n", contentEncodingTypeIdx+26); contentEncodingType = pMsgText.substring(contentEncodingTypeIdx, newlineIdx); // If the last character is whitespace (i.e., a newline), then remove it. if (/\s/.test(contentEncodingType.substr(contentEncodingType.length-1, 1))) contentEncodingType = contentEncodingType.substr(0, contentEncodingType.length-1); // Update startIdx based on the length of the "content encoding type" string startIdx += contentEncodingType.length; // Now, store just the content type in contentEncodingType (i.e., "base64"). contentEncodingType = contentEncodingType.substr(27).toLowerCase(); } // Look for the message text var contentTypeSearchIdx = -1; //if ((contentTypeSearchIdx = lastContentType.indexOf("text/plain")) > -1) if ((contentTypeSearchIdx = lastContentType.indexOf("text/")) > -1) { if (!gotMessageText) { var newlineIdx = pMsgText.indexOf("\n", msgTextSearchStartIdx); // Used to be lastContentTypeIdx if (newlineIdx > -1) retObj.msgText = pMsgText.substring(newlineIdx+1, sepIdx); else retObj.msgText = pMsgText.substring(lastSepIdx, sepIdx); gotMessageText = true; } } else { // Look for a filename in the content-type specification // If it doesn't contain the filename, then we'll have to look on the // next line for the filename. var attachmentFilename = ""; var matches = /name="(.*)"/.exec(lastContentType); if (matches != null) { if (matches.length >= 2) attachmentFilename = matches[1]; } if (attachmentFilename.length == 0) { // Look for the filename on the next line var newlineIdx = pMsgText.indexOf("\n", lastContentTypeIdx); if (newlineIdx > -1) { // 1000 chars should be enough var nextLine = pMsgText.substr(newlineIdx+1, 1000); var matches = /name="(.*)"/.exec(nextLine); if (matches != null) { if (matches.length >= 2) attachmentFilename = matches[1]; } } } // If we got a filename, then extract the base64-encoded file data. if (attachmentFilename.length > 0) { var fileInfo = { filename: attachmentFilename, fullyPathedFilename: gFileAttachDir + attachmentFilename }; // Only extract the base64-encoded data if getB64Data is true // and the current section's encoding type was actually specified // as base64. if (getB64Data && (contentEncodingType == "base64")) { // There should be 2 newlines before the base64 data // TODO: There's a bug here where sometimes it isn't getting // the correct section for base64 data. The code later that // looks for an existing filename in the attachments is sort // of a way around that though. var lineSeparator = ascii(13) + ascii(10); var twoNLIdx = pMsgText.indexOf(lineSeparator + lineSeparator, lastContentTypeIdx); if (twoNLIdx > -1) { // Get the base64-encoded data for the current file from the message, // and remove the newline & carriage return characters and whitespace // from it. fileInfo.B64Data = pMsgText.substring(twoNLIdx+2, sepIdx); fileInfo.B64Data = fileInfo.B64Data.replace(new RegExp(ascii(13) + "|" + ascii(10), "g"), "").trim(); // Update the start index for looking for the next message separator // string startIdx = twoNLIdx; } } // Add the file attachment information to the return object. // If there is already an entry with the filename, then replace // that one; otherwise, append it. var fileExists = false; for (var fileIdx = 0; (fileIdx < retObj.attachments.length) && !fileExists; ++fileIdx) { if (retObj.attachments[fileIdx].filename == fileInfo.filename) { fileExists = true; if (getB64Data && fileInfo.hasOwnProperty("B64Data")) retObj.attachments[fileIdx].B64Data = fileInfo.B64Data; } } if (!fileExists) retObj.attachments.push(fileInfo); } } } lastContentType = contentType; lastSepIdx = sepIdx; lastContentTypeIdx = contentTypeIdx; lastContentEncodingTypeIdx = contentEncodingTypeIdx; // The end of the message will have the message separator string with // "--" appended to it. If we've reached that point, then we know we // can stop. if (pMsgText.substr(sepIdx, msgSeparator.length+2) == msgSeparator + "--") break; } // If there are any attachments, prepend the message text with a message // saying that the message contains attachments. if (retObj.attachments.length > 0) retObj.msgText = msgHasAttachmentsTxt + retObj.msgText; // If there are attachments and the message text is more than will fit on the // screen (75% of the console height to account for the ), then append text at // the end to say there are attachments. var maxNumCharsOnScreen = 79 * Math.floor(console.screen_rows * 0.75); if ((retObj.attachments.length > 0) && (retObj.msgText.length > maxNumCharsOnScreen)) { retObj.msgText += "\1n\r\n\1g\1h--------------------------------------------------------------------------\1n\r\n"; retObj.msgText += "\1g\1h- This message contains one or more attachments. Press CTRL-A to download.\1n"; } return retObj; } // Allows the user to download files that were attached to a message. Takes an // array of file information given by determineMsgAttachments(). // // Parameters: // pAttachments: An array of file attachment information returned by // determineMsgAttachments() // for each attachment: // B64Data: Base64-encoded file data - Only for attachments // that were attached as base64 in the message (i.e., // in a multi-part MIME message). If the attachment // was uploaded to the user's Synchronet mailbox, // then the object won't have the B64Data property. // filename: The name of the attached file // fullyPathedFilename: The full path & filename of the // attached file saved on the BBS machine function sendAttachedFiles(pAttachments) { if (Object.prototype.toString.call(pAttachments) !== "[object Array]") return; // Synchronet doesn't allow batch downloading of files that aren't in the // file database, so we have to send each one at a time. :( // Get the file download confirmation text from text.dat // 662: "\r\nDownload attached file: \1w%s\1b (%s bytes)" var DLPromptTextOrig = bbs.text(DownloadAttachedFileQ); var anyErrors = false; // For each item in the array, allow the user to download the attachment. var fileNum = 1; pAttachments.forEach(function(fileInfo) { console.print("\1n"); console.crlf(); // If the file doesn't exist and base64 data is available for the file, // then save it to the temporary attachments directory. // Note that we need to save the file first in order to get the file's size // to display in the confirmation prompt to download the file. // errorMsg will contain an error if something went wrong creating the // temporary attachments directory, etc. var errorMsg = ""; var savedFileToBBS = false; // If we base64-decoded the file, we'll want to delete it after it's sent. if (!file_exists(fileInfo.fullyPathedFilename)) { if (fileInfo.hasOwnProperty("B64Data")) { // If the temporary attachments directory doesn't exist, // then create it. var attachmentDirExists = true; // Will be false if it couldn't be created if (!file_isdir(gFileAttachDir)) { // If it's a file rather than a directory, then remove it // before creating it as a directory. if (file_exists(gFileAttachDir)) file_remove(gFileAttachDir); attachmentDirExists = mkdir(gFileAttachDir); } // Write the file to the BBS machine if (attachmentDirExists) { var attachedFile = new File(fileInfo.fullyPathedFilename); if (attachedFile.open("wb")) { attachedFile.base64 = true; if (!attachedFile.write(fileInfo.B64Data)) errorMsg = "\1h\1g* \1n\1cCan't send " + quoteStrWithSpaces(fileInfo.filename) + " - Failed to save it to the BBS!"; attachedFile.close(); // Saved the file to the temporary attachments directory (even if it failed // to write, there's probably still an empty file there). savedFileToBBS = true; } else errorMsg = "\1h\1g* \1n\1cFailed to save " + quoteStrWithSpaces(fileInfo.filename) + "!"; } else errorMsg = "\1h\1g* \1n\1cFailed to create temporary directory on the BBS!"; } else errorMsg = "\1h\1g* \1n\1cCan't send " + quoteStrWithSpaces(fileInfo.filename) + " because it doesn't exist or wasn't encoded in a known format"; } // If we can send the file, then prompt the user for confirmation, and if they // answer yes, then send it. // Note that we needed to save the file first in order to get the file's size // to display in the confirmation prompt. if (errorMsg.length == 0) { // Print the file number console.print("\1n\1cFile \1g" + fileNum + "\1c of \1g" + pAttachments.length + "\1n"); console.crlf(); // Prompt the user to confirm whether they want to download the // file. If the user chooses yes, then send it. var fileSize = Math.round(file_size(fileInfo.fullyPathedFilename)); var DLPromptText = format(DLPromptTextOrig, fileInfo.filename, fileSize); if (console.yesno(DLPromptText)) bbs.send_file(fileInfo.fullyPathedFilename); // If the file was base64-decoded and saved to the BBS machine (as opposed to // being in the user's mailbox), then delete the file. if (savedFileToBBS) file_remove(fileInfo.fullyPathedFilename); } else { // There was an error creating the temporary attachment directory, etc., so // display the error and pause to let the user read it. //console.print(errorMsg); //console.putmsg(word_wrap(errorMsg, console.screen_columns-1, errorMsg.length, false)); //console.crlf(); var errMsgLines = lfexpand(word_wrap(errorMsg, console.screen_columns-1, errorMsg.length, false)).split("\r\n"); console.print("\1n"); for (var errorIdx = 0; errorIdx < errMsgLines.length; ++errorIdx) { console.print(errMsgLines[errorIdx]); console.crlf(); } console.pause(); } ++fileNum; }); // If the temporary attachments directory exists, then delete it. if (file_exists(gFileAttachDir)) deltree(gFileAttachDir); } // This function recursively removes a directory and all of its contents. Returns // whether or not the directory was removed. // // Parameters: // pDir: The directory to remove (with trailing slash). // // Return value: Boolean - Whether or not the directory was removed. function deltree(pDir) { if ((pDir == null) || (pDir == undefined)) return false; if (typeof(pDir) != "string") return false; if (pDir.length == 0) return false; // Make sure pDir actually specifies a directory. if (!file_isdir(pDir)) return false; // Don't wipe out a root directory. if ((pDir == "/") || (pDir == "\\") || (/:\\$/.test(pDir)) || (/:\/$/.test(pDir)) || (/:$/.test(pDir))) return false; // If we're on Windows, then use the "RD /S /Q" command to delete // the directory. Otherwise, assume *nix and use "rm -rf" to // delete the directory. if (deltree.inWindows == undefined) deltree.inWindows = (/^WIN/.test(system.platform.toUpperCase())); if (deltree.inWindows) system.exec("RD " + withoutTrailingSlash(pDir) + " /s /q"); else system.exec("rm -rf " + withoutTrailingSlash(pDir)); // The directory should be gone, so we should return true. I'd like to verify that the // directory really is gone, but file_exists() seems to return false for directories, // even if the directory does exist. So I test to make sure no files are seen in the dir. return (directory(pDir + "*").length == 0); /* // Recursively deleting each file & dir using JavaScript: var retval = true; // Open the directory and delete each entry. var files = directory(pDir + "*"); for (var i = 0; i < files.length; ++i) { // If the entry is a directory, then deltree it (Note: The entry // should have a trailing slash). Otherwise, delete the file. // If the directory/file couldn't be removed, then break out // of the loop. if (file_isdir(files[i])) { retval = deltree(files[i]); if (!retval) break; } else { retval = file_remove(files[i]); if (!retval) break; } } // Delete the directory specified by pDir. if (retval) retval = rmdir(pDir); return retval; */ } // Removes a trailing (back)slash from a path. // // Parameters: // pPath: A directory path // // Return value: The path without a trailing (back)slash. function withoutTrailingSlash(pPath) { if ((pPath == null) || (pPath == undefined)) return ""; var retval = pPath; if (retval.length > 0) { var lastIndex = retval.length - 1; var lastChar = retval.charAt(lastIndex); if ((lastChar == "\\") || (lastChar == "/")) retval = retval.substr(0, lastIndex); } return retval; } // Adds double-quotes around a string if the string contains spaces. // // Parameters: // pStr: A string to add double-quotes around if it has spaces // // Return value: The string with double-quotes if it contains spaces. If the // string doesn't contain spaces, then the same string will be // returned. function quoteStrWithSpaces(pStr) { if (typeof(pStr) != "string") return ""; var strCopy = pStr; if (pStr.indexOf(" ") > -1) strCopy = "\"" + pStr + "\""; return strCopy; } // Given a message header field list type number (i.e., the 'type' property for an // entry in the field_list array in a message header), this returns a text label // to be used for outputting the field. // // Parameters: // pFieldListType: A field_list entry type (numeric) // pIncludeTrailingColon: Optional boolean - Whether or not to include a trailing ":" // at the end of the returned string. Defaults to true. // // Return value: A text label for the field (a string) function msgHdrFieldListTypeToLabel(pFieldListType, pIncludeTrailingColon) { // The page at this URL lists the header field types: // http://synchro.net/docs/smb.html#Header Field Types: var fieldTypeLabel = "Unknown (" + pFieldListType.toString() + ")"; switch (pFieldListType) { case 0: // Sender fieldTypeLabel = "Sender"; break; case 1: // Sender Agent fieldTypeLabel = "Sender Agent"; break; case 2: // Sender net type fieldTypeLabel = "Sender Net Type"; break; case 3: // Sender Net Address fieldTypeLabel = "Sender Net Address"; break; case 4: // Sender Agent Extension fieldTypeLabel = "Sender Agent Extension"; break; case 5: // Sending agent (Sender POS) fieldTypeLabel = "Sender Agent"; break; case 6: // Sender organization fieldTypeLabel = "Sender Organization"; break; case 16: // Author fieldTypeLabel = "Author"; break; case 17: // Author Agent fieldTypeLabel = "Author Agent"; break; case 18: // Author Net Type fieldTypeLabel = "Author Net Type"; break; case 19: // Author Net Address fieldTypeLabel = "Author Net Address"; break; case 20: // Author Extension fieldTypeLabel = "Author Extension"; break; case 21: // Author Agent (Author POS) fieldTypeLabel = "Author Agent"; break; case 22: // Author Organization fieldTypeLabel = "Author Organization"; break; case 32: // Reply To fieldTypeLabel = "Reply To"; break; case 33: // Reply To agent fieldTypeLabel = "Reply To Agent"; break; case 34: // Reply To net type fieldTypeLabel = "Reply To net type"; break; case 35: // Reply To net address fieldTypeLabel = "Reply To net address"; break; case 36: // Reply To extension fieldTypeLabel = "Reply To (extended)"; break; case 37: // Reply To position fieldTypeLabel = "Reply To position"; break; case 38: // Reply To organization (0x26 hex) fieldTypeLabel = "Reply To organization"; break; case 48: // Recipient (0x30 hex) fieldTypeLabel = "Recipient"; break; case 162: // Seen-by fieldTypeLabel = "Seen-by"; break; case 163: // Path fieldTypeLabel = "Path"; break; case 176: // RFCC822 Header fieldTypeLabel = "RFCC822 Header"; break; case 177: // RFC822 MSGID fieldTypeLabel = "RFC822 MSGID"; break; case 178: // RFC822 REPLYID fieldTypeLabel = "RFC822 REPLYID"; break; case 240: // UNKNOWN fieldTypeLabel = "UNKNOWN"; break; case 241: // UNKNOWNASCII fieldTypeLabel = "UNKNOWN (ASCII)"; break; case 255: fieldTypeLabel = "UNUSED"; break; default: fieldTypeLabel = "Unknown (" + pFieldListType.toString() + ")"; break; } var includeTrailingColon = (typeof(pIncludeTrailingColon) == "boolean" ? pIncludeTrailingColon : true); if (includeTrailingColon) fieldTypeLabel += ":"; return fieldTypeLabel; } // Capitalizes the first character of a string. // // Parameters: // pStr: The string to capitalize // // Return value: A version of the sting with the first character capitalized function capitalizeFirstChar(pStr) { var retStr = ""; if (typeof(pStr) == "string") { if (pStr.length > 0) retStr = pStr.charAt(0).toUpperCase() + pStr.slice(1); } return retStr; } // Parses a list of numbers (separated by commas or spaces), which may contain // ranges separated by dashes. Returns an array of the individual numbers. // // Parameters: // pList: A comma-separated list of numbers, some which may contain // 2 numbers separated by a dash denoting a range of numbers. // // Return value: An array of the individual numbers from the list function parseNumberList(pList) { if (typeof(pList) != "string") return []; var numberList = []; // Split pList on commas or spaces var commaOrSpaceSepArray = pList.split(/[\s,]+/); if (commaOrSpaceSepArray.length > 0) { // Go through the comma-separated array - If the element is a // single number, then append it to the number list to be returned. // If there is a range (2 numbers separated by a dash), then // append each number in the range individually to the array to be // returned. for (var i = 0; i < commaOrSpaceSepArray.length; ++i) { // If it's a single number, append it to numberList. if (/^[0-9]+$/.test(commaOrSpaceSepArray[i])) numberList.push(+commaOrSpaceSepArray[i]); // If there are 2 numbers separated by a dash, then split it on the // dash and generate the intermediate numbers. else if (/^[0-9]+-[0-9]+$/.test(commaOrSpaceSepArray[i])) { var twoNumbers = commaOrSpaceSepArray[i].split("-"); if (twoNumbers.length == 2) { var num1 = +twoNumbers[0]; var num2 = +twoNumbers[1]; // If the 1st number is bigger than the 2nd, then swap them. if (num1 > num2) { var temp = num1; num1 = num2; num2 = temp; } // Append each individual number in the range to numberList. for (var number = num1; number <= num2; ++number) numberList.push(number); } } } } return numberList; } // Inputs a single keypress from the user from a list of valid keys, allowing // input modes (see K_* in sbbsdefs.js for mode bits). This is similar to // console.getkeys(), except that this allows mode bits (such as K_NOCRLF, etc.). // // Parameters: // pAllowedKeys: A list of allowed keys (string) // pMode: Mode bits (see K_* in sbbsdefs.js) // // Return value: The user's inputted keypress function getAllowedKeyWithMode(pAllowedKeys, pMode) { var userInput = ""; var keypress = ""; var i = 0; var matchedKeypress = false; while (!matchedKeypress) { keypress = console.getkey(K_NOECHO|pMode); // Check to see if the keypress is one of the allowed keys for (i = 0; i < pAllowedKeys.length; ++i) { if (keypress == pAllowedKeys[i]) userInput = keypress; else if (keypress.toUpperCase() == pAllowedKeys[i]) userInput = keypress.toUpperCase(); else if (keypress.toLowerCase() == pAllowedKeys[i]) userInput = keypress.toLowerCase(); if (userInput.length > 0) { matchedKeypress = true; // If K_NOECHO is not in pMode, then output the user's keypress if ((pMode & K_NOECHO) == 0) console.print(userInput); // If K_NOCRLF is not in pMode, then output a CRLF if ((pMode & K_NOCRLF) == 0) console.crlf(); break; } } } return userInput; } ///////////////////////////////////////////////////////////////////////// // Debug helper & error output function // Writes some text on the screen at a given location with a given pause. // // Parameters: // pX: The column number on the screen at which to write the message // pY: The row number on the screen at which to write the message // pText: The text to write // pPauseMS: The pause time, in milliseconds // pClearLineAttrib: Optional - The color/attribute to clear the line with. // If not specified or null is specified, defaults to normal attribute. // pClearLineAfter: Whether or not to clear the line again after the message is dispayed and // the pause occurred. This is optional. function writeWithPause(pX, pY, pText, pPauseMS, pClearLineAttrib, pClearLineAfter) { var clearLineAttrib = "\1n"; if ((pClearLineAttrib != null) && (typeof(pClearLineAttrib) == "string")) clearLineAttrib = pClearLineAttrib; console.gotoxy(pX, pY); console.cleartoeol(clearLineAttrib); console.print(pText); if (pPauseMS > 0) mswait(pPauseMS); if (pClearLineAfter) { console.gotoxy(pX, pY); console.cleartoeol(clearLineAttrib); } }